RISC-V Linuxでptrace使う場合の注意点
「たのしいバイナリの歩き方」の本やバイナリの歩き方に掲載されているptraceのサンプルプログラムが、RISC-V QEMU上で動いているLinuxではどうもうまく動かない。その原因を調査していた。
うまく動かないと悩んでいたコードはバイナリの歩き方に掲載されていたもので以下の通り。PTRACE_SINGLESTEP
で実行が進まない上、PTRACE_GETREGS
でそれっぽいレジスタの値を取得できないという問題に遭遇していた。(ちなみに、手元のx86実機マシンでは正しく以下のサンプルコードは動くことは確認済み。)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/syscall.h> #include <sys/user.h> #include <errno.h> void err(char *str) { fprintf(stderr, "ERROR: %s\n", str); } void target(char *argv[], char *argp[]) { if(ptrace(PTRACE_TRACEME, 0, NULL, NULL) != -1) execve(argv[0], argv, argp); else err("PTRACE_TRACEME"); exit(0); } void controler(int pid) { int status; struct user_regs_struct regs; while(1){ waitpid(pid, &status, 0); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS, pid, 0, ®s); printf("%08x: \n", (unsigned int)regs.eip); ptrace(PTRACE_SINGLESTEP, pid, 0, NULL); } } int exec_prog(char *argv[], char *argp[]) { int pid; switch(pid = fork()) { case 0: target(argv, argp); break; case -1: err("FORK"); break; default: controler(pid); break; } return 0; } int main(int argc, char *argv[], char *argp[]) { if(argc < 2){ fprintf(stderr, "%s <args>\n", argv[0]); return 1; } argv++; exec_prog(argv, argp); return 0; }
結論だけ述べると
PTRACE_GETREGS
PTRACE_SETREGS
PTRACE_SINGLESTEP
を指定してptrace
を呼び出しても、そもそも実装されていないので必ずEIO
がエラーとして返る。- アタッチしているプロセスのレジスタの情報を取得/設定する場合には
PTRACE_GETREGSET
PTRACE_SETREGSET
を用いる必要がある。 PTRACE_SINGLESTEP
は使えず、代替手段もない。PTRACE_SYSCALL
やPTRACE_CONT
を使い、特定の場所で止めたいのであれば、止めたい箇所にebreak
を埋め込む。
である。
調査概要
ここからLinux kernelのソースコードを落としてくる。今回5.2.3を落としてきた。
落としてきたLinux kernelのソースコードを展開し、arch/riscv/kernel/ptrace.c
のソースコードをみる。該当する箇所は以下の通り。
long arch_ptrace(struct task_struct *child, long request, unsigned long addr, unsigned long data) { long ret = -EIO; switch (request) { default: ret = ptrace_request(child, request, addr, data); break; } return ret; }
問答無用でptrace_request
へ飛ぶコードが書かれている。
ptrace_request
はkernel/ptrace.c
で定義されている。
int ptrace_request(struct task_struct *child, long request, unsigned long addr, unsigned long data) { bool seized = child->ptrace & PT_SEIZED; int ret = -EIO; kernel_siginfo_t siginfo, *si; void __user *datavp = (void __user *) data; unsigned long __user *datalp = datavp; unsigned long flags; switch (request) { case PTRACE_PEEKTEXT: case PTRACE_PEEKDATA: return generic_ptrace_peekdata(child, addr, data); case PTRACE_POKETEXT: case PTRACE_POKEDATA: return generic_ptrace_pokedata(child, addr, data); #ifdef PTRACE_OLDSETOPTIONS case PTRACE_OLDSETOPTIONS: #endif case PTRACE_SETOPTIONS: ret = ptrace_setoptions(child, data); break; case PTRACE_GETEVENTMSG: ret = put_user(child->ptrace_message, datalp); break; // 長いので省略
この中にはPTRACE_GETREGS
が含まれていなかった。どうりでレジスタの値が取得できないわけだ。
ちなみに、x86の場合だとarch_ptrace
関数の中に、PTRACE_GETREGS
に該当する処理が記述されており、アタッチしているプロセスのレジスタを取得する処理が書かれている。
case PTRACE_GETREGS: /* Get all gp regs from the child. */ return copy_regset_to_user(child, task_user_regset_view(current), REGSET_GENERAL, 0, sizeof(struct user_regs_struct), datap);
同じようにPTRACE_SINGLESTEP
についても、該当するソースコードを見てみる。
int ptrace_request(struct task_struct *child, long request, unsigned long addr, unsigned long data) { bool seized = child->ptrace & PT_SEIZED; int ret = -EIO; kernel_siginfo_t siginfo, *si; void __user *datavp = (void __user *) data; unsigned long __user *datalp = datavp; unsigned long flags; switch (request) { case PTRACE_PEEKTEXT: case PTRACE_PEEKDATA: return generic_ptrace_peekdata(child, addr, data); case PTRACE_POKETEXT: case PTRACE_POKEDATA: // .. 長いので省略 #ifdef PTRACE_SINGLESTEP case PTRACE_SINGLESTEP: #endif #ifdef PTRACE_SINGLEBLOCK case PTRACE_SINGLEBLOCK: #endif #ifdef PTRACE_SYSEMU case PTRACE_SYSEMU: case PTRACE_SYSEMU_SINGLESTEP: #endif case PTRACE_SYSCALL: case PTRACE_CONT: return ptrace_resume(child, request, data); // .. 長いので省略
PTRACE_SINGLESTEP
が選択された際には、ptrace_resume
が呼ばれる。
static int ptrace_resume(struct task_struct *child, long request, unsigned long data) { bool need_siglock; if (!valid_signal(data)) return -EIO; if (request == PTRACE_SYSCALL) set_tsk_thread_flag(child, TIF_SYSCALL_TRACE); else clear_tsk_thread_flag(child, TIF_SYSCALL_TRACE); #ifdef TIF_SYSCALL_EMU if (request == PTRACE_SYSEMU || request == PTRACE_SYSEMU_SINGLESTEP) set_tsk_thread_flag(child, TIF_SYSCALL_EMU); else clear_tsk_thread_flag(child, TIF_SYSCALL_EMU); #endif if (is_singleblock(request)) { if (unlikely(!arch_has_block_step())) return -EIO; user_enable_block_step(child); } else if (is_singlestep(request) || is_sysemu_singlestep(request)) { // <-- single stepの際に実行される部分 if (unlikely(!arch_has_single_step())) // arch_has_single_step マクロが asm/ptrace.h に定義されている場合には1となるが、それ以外は0になる。 return -EIO; user_enable_single_step(child); } else { user_disable_single_step(child); } // .. 省略
arch/riscv/include/asm/ptrace.h
を見てみると、arch_has_single_step
は定義されていない。つまり、PTRACE_SINGLESTEP
を引数に設定して実行したとしても、-EIO
のエラーが返ってくるだけである。
RISC-VのLinux、ptraceの実装に関してはまだまだ不完全なところが多い。
ちなみにアタッチしているプロセスのレジスタの情報を取得する場合には、
struct user_regs_struct regs = {0}; struct iovec iov; iov.iov_len = sizeof(regs); iov.iov_base = ®s; int ret = ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov); printf("%llx: \n", regs.pc);
のように書けばできる。