ptraceシステムコール入門 ― プロセスの出力を覗き見してみよう!

他のプロセスを中断せずに、その出力をミラーリングして新しくパイプで繋ぐ、そんなことはできるのでしょうか。 straceやgdbといったコマンドは一体どういう仕組みで動いているのでしょうか。 ptraceシステムコールを使い、プロセスが呼ぶシステムコールを調べて出力を覗き見するコマンドを実装してみたいと思います。

ptraceシステムコール

Linuxを触っていると、いかにプロセスを組み合わせるか、組み合わせる方法をどれだけ知っているかが重要になってきます。 パイプやリダイレクトを使ってプロセスの出力結果を制御したり、コードの中からコマンドを実行して、終了ステータスを取得したりします。 プロセスツリーやプロセスグループを理解し、シグナルやnohupコマンドを使ったりします。

プロセスの扱いに慣れると疑問に持つのがstracegdbの仕組みです。 プロセスの実行しているシステムコールを出力したり、メモリーを書き換えたりできるこれらのコマンドは、まるで魔法みたいです。 一体全体どんな仕組みで動いているのでしょうか。 誰に許しを得て他のプロセスのメモリーにアクセスしたりしているのでしょうか。 これらのコマンドは、標準出力を読んだりプロセスにシグナルを送ったりするのとは全く別の次元のことをやっているかのようです。

ptraceシステムコールは、straceやgdbのようなコマンドの実装の中で核となるシステムコールです。 このシステムコールを使うと、実行中の他のプロセスの動作を覗き見したり、メモリーを書き換えたりすることができます。 魔法を使っているんじゃなかったんですね。 ちょっぴり残念な気分です。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

  • 作者:武内 覚
  • 発売日: 2018/02/23
  • メディア: 単行本(ソフトカバー)

procoutコマンド

使っているシステムコールがわかれば、それを使って何かを作りたくなってきます。 ptraceシステムコールを使うと、原理上はstraceやgdbを自前で実装することができます。 しかし、最初から複雑なデバッガを目指そうとすると大変です。

私はまず手始めに、procoutというコマンドを作ってみました。

中断して途中から再開したりできないプロセスの出力が端末に流れている時に、後からgrepしたりteeしたりしたくなることがあると思います。 そんな時にプロセス自体を中断せず、またコードの変更も行わずに、別のターミナルからプロセスの出力を流すことができるコマンドです。 プロセスの出力を覗き見るという感じなので、procoutと名付けてみました。

次のように、引数にpidを渡して実行します。

 $ sudo procout [pid]

そうすると、対象となるプロセスにアタッチし、コマンドの標準出力をそのまま真似して出力してくれます。 エディタのプロセスに対して使うと、まるで端末がミラーリングされているかのような挙動になります。

f:id:itchyny:20170731003438g:plain

procoutコマンドは、コマンド自体の便利さやおもしろさよりも、それ自体を実装することに意義があります。 ptraceシステムコールの基礎に触れることができるからです。 ptraceの最初の練習問題として、そしてLinuxシステムコールがどのように呼ばれているかについて理解するために、このエントリーではprocoutコマンドを一緒に作っていきたいとおもいます。

免責: 私は一週間前にptraceシステムコールについて学びはじめた素人です。素人だからこそ、わからないところを一つずつ潰しながらこの記事を書きました。もし誤っている記述があれば、お気軽にコメントいただければと思います。

この記事のコード及び上記procoutコマンドは、以下の環境で動作確認をしています。macOSでは動きませんが、仮想マシン上でも簡単に試せますので、是非挑戦していただけたらと思います。

vagrant@vagrant-ubuntu-trusty-64:~/$ uname -srmo
Linux 3.13.0-125-generic x86_64 GNU/Linux

プロセスにアタッチしてみよう

ptraceシステムコールは、引数によって様々なことを行うことができます。 man 2 ptraceでマニュアルを引いてみましょう。

NAME
       ptrace - process trace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

第一引数にはプロセスに対してどういうptraceリクエストを行うか、第二引数にはプロセスのpidを指定します。 リクエストの内容によって、第三・四引数の意味は変わってくるので、これらについてはおいおい見ていきましょう。 他にはsys/ptrace.hをincludeすること、返り値がlongであることがわかりました。

まずは、ptraceの基本であるアタッチ・デタッチから始めましょう。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>

int main(int argc, char *argv[])
{
  long ret;
  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %d\n", pid);

  ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  if (ret < 0) {
    perror("failed to attach");
    exit(1);
  }
  printf("attached to %d (ret: %ld)\n", pid, ret);
  sleep(5);

  ret = ptrace(PTRACE_DETACH, pid, NULL, NULL);
  if (ret < 0) {
    perror("failed to detach");
    exit(1);
  }
  printf("detached from %d (ret: %ld)\n", pid, ret);

  return 0;
}

topコマンドを実行し、そのプロセスに対してアタッチしてみましょう。

f:id:itchyny:20170730182701g:plain

あら、アタッチできませんでした。 Operation not permittedと表示されていることから想像がつきますが、ptraceで他のプロセスにアタッチするには、root権限が必要です。 そうですよね、一般ユーザーで他のプロセスを自由に操作できたら怖いですよね。

f:id:itchyny:20170730182642g:plain

うまく動きました。 プロセスにアタッチしてsleepしている間、左側のtopコマンドが停止しているのがわかります。

なぜtopコマンドは止まってしまったのでしょうか。 man 2 ptraceでは次のように説明されています。

While being traced, the tracee will stop each time a signal is delivered, even if the signal is being ignored. (An exception is SIGKILL, which has its usual effect.) The tracer will be notified at its next call to waitpid(2) (or one of the related "wait" system calls); that call will return a status value containing information that indicates the cause of the stop in the tracee.

tracerがptraceするプロセスで、traceeがptraceされるプロセスです (employerとemployeeと同じ)。 ptraceされるプロセスはシグナル毎にいちいち止まるから、ptraceするプロセスはwaitpidを使ってねと書かれています。

straceのようなptraceの典型的な用途では、システムコールが呼ばれるところで処理を行います。 PTRACE_SYSCALLを使ってプロセスを再開すると、次のシステムコールでプロセスが停止し、ptraceするプロセスはwaitpidを使ってその停止を検知することができます。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>

int main(int argc, char *argv[])
{
  int status;

  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %d\n", pid);

  if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
    perror("failed to attach");
    exit(1);
  }

  while (1) {
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) {
      break;
    } else if (WIFSIGNALED(status)) {
      printf("terminated by signal %d\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
      printf("stopped by signal %d\n", WSTOPSIG(status));
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

  return 0;
}

f:id:itchyny:20170728000028g:plain

大量に表示されるsignal 5とはどういう意味でしょうか。 プロセスにシグナルを送るコマンドであるkillに聞いてみましょう。

 $ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
...

5番のシグナルがSIGTRAPだということがわかりました。 PTRACE_SYSCALLで再開するとSIGTRAPによって止まるという動作が確認できました。 これは期待されている動作なのでしょうか。 man 2 ptraceを引き、適当に検索しながら該当しそうな記述を探します。

Syscall-enter-stop and syscall-exit-stop are observed by the tracer as waitpid(2) returning with WIFSTOPPED(status) true, and WSTOPSIG(status) giving SIGTRAP.

From the tracer's perspective, the tracee will appear to have been stopped by receipt of a SIGTRAP.

実際の挙動を確かめながら、manを読み込み少しずつ知識を蓄えていくことは楽しいことですね。

レジスタの状態を取得してみよう

プロセスの出力を覗き見するには、プロセスが呼ぶwriteシステムコールの引数を解析する必要があります。 システムコールが呼ばれるときのレジスタの中身を見てみましょう。 ptraceの引数にPTRACE_GETREGSを使ってみます。

  struct user_regs_struct regs;

  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } else if (WIFSTOPPED(status)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      printf("%lld %lld %lld %lld\n", regs.orig_rax, regs.rsi, regs.rdx, regs.rdi);
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

f:id:itchyny:20170728000012g:plain

なんだか出力が賑やかになってきました。 他のプロセスにアタッチし、システムコールが呼ばれる時のレジスタを出力しているだけですが、これはすでに簡易straceのようなものです。

一番左に出力したorig_raxは、システムコールの番号を表します。 システムコール番号はsys/syscall.hで定義されています。

#include <stdio.h>
#include <sys/syscall.h>

int main(int argc, char const* argv[])
{
  printf("%d\n", SYS_write);
  return 0;
}

私の手元ではSYS_write1でした。

上記のコードでは、システムコール番号の他にrsi, rdx, rdiを表示しています。 システムコールが呼ばれる瞬間、各レジスタには何が入っているのでしょうか。 x86_64 syscall registersなどでググって調べてもいいのですが、簡単なコードのアセンブリを見るという方法もあります。

#include <stdio.h>
#include <unistd.h>

void main() {
  write(STDOUT_FILENO, "Hello, world!", 13);
}
 $ gcc -O0 -S write_regs.c
 $ cat write_regs.s
 .file "write_regs.c"
    .section  .rodata
.LC0:
    .string   "Hello, world!"
    .text
    .globl    main
    .type main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq %rsp, %rbp
    .cfi_def_cfa_register 6
    movl $13, %edx
    movl $.LC0, %esi
    movl $1, %edi
    call write
    popq %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size main, .-main
    .ident    "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
    .section  .note.GNU-stack,"",@progbits

文字列のアドレスはesiに、長さ (writeの第三引数) がedxに、出力先であるfd (=STDOUT_FILENO=1) はediに書かれていることがわかります。 siレジスタは文字列操作のためのSource index, diレジスタDestination indexであることからその名がついていることを思い出すと、それぞれに文字列のアドレスとfdが入ってるというのは自然な挙動です。

やりたいことは「プロセスの出力を覗き見する」だったので、regs.orig_rax == SYS_writeのときにプロセスのメモリーから文字列を読み取り、出力すれば完成です。

syscall-enter-stopとsyscall-exit-stop

これまで「システムコールが呼ばれる時」とごまかしてきましたが、実はこの言い方は正確ではありません。 レジスタの値を出力して様子を見てみましょう。

 $ sudo ./main 24358
attach to 24358
1 6545952 434 1
1 6545952 434 1
1 6545952 1140 1
1 6545952 1140 1
1 6545952 821 1
1 6545952 821 1
1 6545952 1136 1
1 6545952 1136 1
# orig_rax rsi rdx rdi

左からシステムコール番号 (SYS_write), 文字列のアドレス, 書き込んだバイト長, fdです。 同じ値の行が二回ずつ表示されていることがわかります。 これは同じ引数で二回システムコールが呼ばれているのではなく、システムコールが呼ばれる直前と直後の二回表示されているのです。

さらに理解を深めるために、orig_raxレジスタに加えてraxレジスタも表示してみます。

 $ sudo ./main 24358
attach to 24358
1 -38 6545952 1278 1
1 1278 6545952 1278 1
1 -38 6545952 1262 1
1 1262 6545952 1262 1
1 -38 6545952 821 1
1 821 6545952 821 1
1 -38 6545952 1122 1
1 1122 6545952 1122 1
# orig_rax rax rsi rdx rdi

システムコールが呼ばれると、その返り値がraxレジスタに入ります。 writeシステムコールの返り値は書き込んだバイト数ですから、writeの第三引数であるrdxraxが同じ行はシステムコールが呼ばれた後ということになります。 raxレジスタの値が-38となっている行は、システムコールが呼ばれる前の状態ということになります。 この-38が何の値なのかは後で説明します。

PTRACE_SYSCALLによりシステムコールをトラップすると、システムコールに入ったとき (syscall-enter-stop) と終わった時 (syscall-exit-stop) の二回停止するようになっています。 manを見てみましょう。

If the tracee was restarted by PTRACE_SYSCALL or PTRACE_SYSEMU, the tracee enters syscall-enter-stop just prior to entering any system call (中略). No matter which method caused the syscall-entry-stop, if the tracer restarts the tracee with PTRACE_SYSCALL, the tracee enters syscall-exit-stop when the system call is finished, or if it is interrupted by a signal. (That is, signal-delivery-stop never happens between syscall-enter-stop and syscall-exit-stop; it happens after syscall-exit-stop.).

プロセスの出力文字列を覗き見するのは、入ったときでも終わったときでもどっちでも構いません。 ただ二回表示されると困るので、ここではシステムコールに入った時だけ処理を行うようにしましょう。 では、syscall-enter-stopsyscall-exit-stopを区別するにはどうすればいいのでしょうか。 manを順番に読んでいくと、次のような記述に愕然とします。

Syscall-enter-stop and syscall-exit-stop are indistinguishable from each other by the tracer. The tracer needs to keep track of the sequence of ptrace-stops in order to not misinterpret syscall-enter-stop as syscall-exit-stop or vice versa.

関連する記述をmanから抜き出してまとめてみました。

  • syscall-enter-stopの直後はsyscall-exit-stopとは限らない。PTRACE_EVENTによる停止かもしれないし、終了しているかもしれない。
  • PTRACE_O_TRACESYSGOOD を使うと、syscall-{enter,exit}-stopかそれ以外かは区別できる。
  • x86において、syscall-enter-stopではraxレジスタ-ENOSYS (この値が-38) になる。しかし、何らかのシステムコールが同じ値を返すこともあり、rax == -ENOSYS だからといってsyscall-exit-stopではないとは言い切れない。
  • syscall-enter-stopとsyscall-exit-stopは単体で見た時に区別することはできない。前の状態を保持しておいて調べるしかない。

以上を踏まえて、syscall-enter-stopでのみレジスタ値を表示するように実装してみました。

  ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);

  int is_enter_stop = 0;
  long prev_orig_rax = -1;
  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } else if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      is_enter_stop = prev_orig_rax == regs.orig_rax ? !is_enter_stop : 1;
      prev_orig_rax = regs.orig_rax;
      if (is_enter_stop && regs.orig_rax == SYS_write) {
        printf("%lld %lld %lld %lld %lld\n", regs.orig_rax, regs.rax, regs.rsi, regs.rdx, regs.rdi);
      }
    }

PTRACE_SETOPTIONSはptraceリクエストの1つで、PTRACE_O_TRACESYSGOODを指定することで、システムコールによる停止かどうかを正確に判定できるようになります。 システムコール番号が前から変化したときにenter-stopだと判定するようにしました。 ずっと同じシステムコールが呼ばれ続けるならば、ずっとexit-stopで出力する可能性は否定できませんが、多くの現実的なコマンドそういうことはなさそうですし、仮にそうなったとしても出力を覗き見するコマンドとしての動作には影響しません。 だんだん精度良くシステムコールをトレースできるようになってきましたね。

文字列をメモリーから読み取ろう

プロセスがwriteシステムコールを呼ぶ時の引数から、出力されているバイト列を読み取ることができます。 PTRACE_PEEKDATAを使ってptraceを呼ぶと、プロセスの管理しているメモリーの値を取得することができます。

        peek_and_output(pid, regs.rsi, regs.rdx, (int)regs.rdi);

/* ... */

void peek_and_output(pid_t pid, long long addr, long long size, int fd)
{
  if (fd != 1 && fd != 2) {
    return;
  }
  char* bytes = malloc(size + sizeof(long));
  int i;
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed to peek data\n");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  bytes[size] = '\0';
  write(fd == 2 ? 2 : 1, bytes, size);
  fflush(fd == 2 ? stderr : stdout);
  free(bytes);
}

ptrace(PTRACE_PEEKDATA, pid, {addr}, NULL)の返り値が、そのアドレスにある値です。 標準エラー出力ならエラー出力に出すように実装してみました。

さっそく実行してみましょう。

やったー! topコマンドのプロセスにアタッチすると、まるでこちらでもtopコマンドを打ったような動きになりました。 もちろん、topコマンドでなくてどんなコマンドに対しても使えます。 Vimのプロセスにアタッチしてみましょう。 Vimの画面がミラーリングされていておもしろい!

今回実装するprocoutコマンドはここまでとします。 ただ、ここから様々な発展したコマンドが実装できると思います。 上のコードでは諸事情でfd = 1, 2のみ扱っていますが、この処理の必然性はありません (こう制限しないとエディタにアタッチした時にゴミが出力される)。 open, readなど、対応するシステムコールを増やしていくと、より便利なデバッガとなるでしょう。

straceやgdbそのものをそっくり実装しようとする必要はありません。 既にこれらのコマンドはあるじゃないですか、うまく動いているじゃないですか。 それでもなお、これらのコマンドの仕組みを理解することは重要な意義があると考えています。 ptraceシステムコールについて学ぶこと、それを使って実際に動くコマンドを作ってみること、簡単なデバッガを書いてみること。 そして、何をどのように実装すればstraceやgdbなどを作れるかをイメージできるようになること。 コマンドやツールの使い方を学ぶだけで満足するのではなく、それらの仕組みを深く知り、実装方法をイメージできるようになると、技術者としての知識がより幅広くそして深くなり、エンジニアリングを楽しめるようになっていくのだと思います。

Rustで書き直そう

Linuxシステムコールについて学ぶためには、C言語が最適の言語だと思います。 しかし、それらを組み合わせて大きなプログラムを組んだり、複雑なTUIを作ったりする必要が生じたときにふさわしい言語であるかどうかはかなり疑わしいと思います。

Rustはシステムプログラミングを学ぶのに適した言語です。 冒頭でご紹介したprocoutコマンドも、Rustで実装しています。 実は、先にRust版を書いてから、ブログのためにCで書き直しているのが実情です。 ブログを書く時にCを選んだのは、システムコールについて学ぶ時にRustを選ぶことが時に遠回りになりうるとわかったからです。

Programming Rust: Fast, Safe Systems Development

Programming Rust: Fast, Safe Systems Development

Rustを書いていると、実行時のメモリー安全性や、型チェックの厳格さから、「コードの正しさ」をコンパイルのチェックに委ねがちになります。 しかし、低レイヤーを触るとこれはかなり様子が変わってくるのがわかります。 当然のことですが、コンパイルが通っても、システムコールの使い方が適切でなければ全く動きません。 システムコールの呼ぶ順番が間違っていたら、コンパイルが通ったとしても、やってることは全くトンチンカンかもしれません。 また、Rustで書くこととコードがportableかどうかもイコールではありません。 結局のところ、誰かがportabilityの高い素晴らしいライブラリ (ただし中身は涙ぐましい努力で書かれている) を用意しないといけないのです。

Rustで書くことは、メモリー管理と型チェックに関してコードの安全性と大きな安心感をもたらしてくれます。 例えば、ptraceの2つの引数を誤って逆に書いてしまい、数分悩むといったことは起こりえないでしょう (記事を書く過程でやらかしました…)。 Rustの洗練されたエラーのハンドリングや、豊富なライブラリなどエコシステムの恩恵も受けられます。 ただ、低レイヤーを触るときは「正しくないコードはコンパイルが通ったとしても正しく動かない」という当たり前のことを教えてくれます。

さて、procoutのRust実装ですが、これは「読者の課題」としたいと思います。 大した行数じゃないので、簡単に移植できると思います。 私もコードをGitHubにあげていますので、実装できたらコードを見比べてみるとおもしろいかもしれません。 ここではRustで書いたことで得られた知見を簡単に書いておきます。

  • rust-lang/libcnix-rust/nixを使えばだいたいのことはできる。
  • nixパッケージにもmacOSで動くptraceは実装されていない。システムプログラミングでportableにすることは難しい。システムコールの番号すらプラットフォームによって異なる。
  • nixパッケージのptraceはまだ機能が揃っていない。getregsなんかは欲しい。まだ未熟なので、プルリクエストを送ったら簡単に取り込まれるかもしれません。

まとめ

ptraceシステムコールを使い、他のプロセスが呼ぶシステムコールを調べたり、メモリーを読み取ることができるのを確認しました。 プロセスの呼ぶwriteシステムコールの引数を使い、プロセスの出力を覗き見するコマンドprocoutをRustで実装しました。

当初は、普段使っているmacOS上で動くものを作ろうとしたのですが、nixライブラリのptraceのコードを見た時に諦めました。 VagrantUbuntuを立てて、その中で動作確認を行なっています。 普段Cを書くことがほとんどないので、自分にとって貴重な経験になりました。 /usr/includeから素早くファイルを開いて実装を確認するのにも慣れました。 portableなstraceを作るにはかなり大変だということもわかりました。

Rustは書いていてとても楽しい言語です。 今回だと、メモリーからバイト列を読み取って結合するコードはきれいに書けたと思います。 低レイヤーを触るときは何よりもまず、システムコール自体を正しく理解していることが大事です。 実際に動作するCのコードを書けることを確認しておくとよいでしょう。

straceやgdbってどうやって動いているのだろう。 この小さな疑問が浮かんだのが、一週間前のことです。 システムコールについて学び、それを使ったコマンドツールを作るのは、とても楽しい経験でした。 既存のツールの仕組みを調べることで「何を使えば何ができる」というレパートリーを増やし、それらがアイディアの源泉となって、便利なコマンドラインツールを作っていけたらいいなと思います。

参考にしたサイトは以下のとおりです。勉強させていただきました、ありがとうございます。

おまけ: ptraceを使いこなせるようになると、GitHub - nelhage/reptyr: Reparent a running program to a new terminalのような発想が出てくるわけです。いやはや、これはすごいですね。

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>

void peek_and_output(pid_t, long long, long long, int fd);

int main(int argc, char *argv[])
{
  int status;
  struct user_regs_struct regs;

  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %i\n", pid);

  if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
    fprintf(stderr, "failed to attach\n");
    exit(1);
  }
  ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);

  int is_enter_stop = 0;
  long prev_orig_rax = -1;
  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } else if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      is_enter_stop = prev_orig_rax == regs.orig_rax ? !is_enter_stop : 1;
      prev_orig_rax = regs.orig_rax;
      if (is_enter_stop && regs.orig_rax == SYS_write) {
        peek_and_output(pid, regs.rsi, regs.rdx, (int)regs.rdi);
      }
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

  return 0;
}

void peek_and_output(pid_t pid, long long addr, long long size, int fd)
{
  if (fd != 1 && fd != 2) {
    return;
  }
  char* bytes = malloc(size + sizeof(long));
  int i;
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed to peek data\n");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  bytes[size] = '\0';
  write(fd == 2 ? 2 : 1, bytes, size);
  fflush(fd == 2 ? stderr : stdout);
  free(bytes);
}