興味があって調べていたら、少しだけ分かったのでまとめておきます。当然間違った箇所もある、あと考慮が漏れている箇所もあるかと思いますのでツッコミをお願いします…
ptrace(2) システムコール
strace の核となるシステムコールは ptrace(2) である。ptrace(2)を用いることで、あるプロセスを別のプロセスから監視し、シグナルごとに停止してレジスタやメモリの状態を観察したり変更したりできる。gdbのようなデバッガのブレークポイント、あるいはまさにstraceのような目的で利用される。
大まかな利用方法としては、親プロセスの ptrace(PTRACE_ATTACH, pid, ...)
(または子プロセスの ptrace(PTRACE_TRACEME, 0...)
)によりトレースが開始し、wait()
などで停止を待ってから様々な設定を親から送り、 ptrace(PTRACE_CONT, pid, ...)
で実行を再開する。その後、シグナルなどのイベントごとに停止するので、 waitpid()
などで情報を取得して利用する。
簡単なシステムコールトラッキングのサンプルを上げておいた (オリジナルは こちら 。これをもとに x86_64 で動くようにしてある)。
これから実行するシステムコールの番号、引数を取得する。
システムコール直前、または直後に停止した状態*1で、 ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &loc_of_iovec)
という呼び出しをすると取得できる。 strace.c#L1090
// ifdefなどは適宜除外している。ライセンスはstrace本体に従う。以下同じ。 ptrace_getregset(pid_t pid) { ARCH_IOVEC_FOR_GETREGSET.iov_len = sizeof(ARCH_REGS_FOR_GETREGSET); return ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &ARCH_IOVEC_FOR_GETREGSET); }
この時、x86_64でのiovec構造体の定義は linux/x86_64/arch_regs.c にある。i386/x86の user_regs_struct
両方に対応した共用体のiovecがグローバルに x86_io
として定義され、 ptrace(PTRACE_GETREGSET, ...)
の結果もここに格納される。
static union { struct user_regs_struct x86_64_r; struct i386_user_regs_struct i386_r; } x86_regs_union; static struct iovec x86_io = { .iov_base = &x86_regs_union };
そのあとに システムコール番号を取得し 、 それぞれの引数をレジスタから取得する 。
static int arch_get_scno(struct tcb *tcp) { kernel_ulong_t scno = 0; scno = x86_64_regs.orig_rax; //... }
// http://www.mztn.org/lxasm64/amd04.html に詳しい static int get_syscall_args(struct tcb *tcp) { //... tcp->u_arg[0] = x86_64_regs.rdi; tcp->u_arg[1] = x86_64_regs.rsi; tcp->u_arg[2] = x86_64_regs.rdx; tcp->u_arg[3] = x86_64_regs.r10; tcp->u_arg[4] = x86_64_regs.r8; tcp->u_arg[5] = x86_64_regs.r9; //... }
レジスタの情報を人間たちに読めるようにするには
どうやらstraceは、概ね システムコールごと にプリント用の実装を用意しているようだ。圧勢を感じる。例えば uname(2) の表示の実装は uname.c にある。
uname(2) の引数はutsname構造体のポインタのみなので、「相手の」メモリ上にあるその構造体の様子を取得して表示できれば良い。「相手の」メモリにあるのがミソなのだが…。逆に自分のメモリに引っ張ってしまえばどうとでもなるとも言えるので、そこが肝となる。
どうやってやっているかというと、umove_or_printaddrという関数でコピーしているのだが、
SYS_FUNC(uname) { struct utsname uname; if (!umove_or_printaddr(tcp, tcp->u_arg[0], &uname)) { PRINT_UTS_MEMBER("{", sysname); PRINT_UTS_MEMBER(", ", nodename); //... } }
最終的に process_vm_readv(2) というシステムコールを呼び出している。 master.c#L1075-L1083
static ssize_t strace_process_vm_readv(pid_t pid, const struct iovec *lvec, unsigned long liovcnt, const struct iovec *rvec, unsigned long riovcnt, unsigned long flags) { return syscall(__NR_process_vm_readv, (long)pid, lvec, liovcnt, rvec, riovcnt, flags); } # define process_vm_readv strace_process_vm_readv // pid のプロセスのraddrからladdrへlenだけメモリの中身を取得する static ssize_t vm_read_mem(const pid_t pid, void *const laddr, const kernel_ulong_t raddr, const size_t len) { const unsigned long truncated_raddr = raddr; if (raddr != (kernel_ulong_t) truncated_raddr) { errno = EIO; return -1; } const struct iovec local = { .iov_base = laddr, .iov_len = len }; const struct iovec remote = { .iov_base = (void *) truncated_raddr, .iov_len = len }; return process_vm_readv(pid, &local, 1, &remote, 1, 0); } // ...
このシステムコールは、 呼び出し元プロセスと pid で指定されるプロセスのアドレス空間間でデータを転送する ことができるそうだ。そうなんですか… 勢いがありますね…。ということで、このシステムコールと ptrace(PTRACE_PEEKDATA, ...
を組み合わせて、手元にリモートの構造体を再現しているという感じのようだ。なんかもう、この辺から僕は。なぜ人は。やっていきましょう。
(いい感じにギリ人から使えそうなエンドポイントは umoven(3) という名前になっている)
int umoven(struct tcb *const tcp, kernel_ulong_t addr, unsigned int len, void *const our_addr)
思ったこと
最初、 mruby-strace とかそういう名前のものを作ろうと思っていたのだが、心が折れたので誰かやってください。
突然の宣伝
ところで、今日になりますが、「Engineer Lab. Fukuoka Pre Event」というところでお話をします。
FUKUOKA growth nextがエンジニアのたまり場になるように、 そして、成長することができ、様々な刺激を受けられる場となるように、 まずは完全フリーにて何回かプレイベントを開催する予定です!
という感じだそうです。あまりにも刺激を受けすぎてマニア向けのシステムコールを呼び出したりしたいエンジニアの方、あるいは普通に成長したいエンジニアの方、ぜひとも大名へGo!東京の人は、飛行機というものを使うと2時間ぐらいで行けるらしいですよ!
*1:manではそれぞれsyscall-enter-stop/syscall-exit-stopと呼ばれているようだ