鬱猫のつぶやき

鬱猫が日々思っていることをつづります

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, &regs);
        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_SYSCALLPTRACE_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_requestkernel/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 = &regs;
    int ret = ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov);
    printf("%llx: \n", regs.pc);

のように書けばできる。