5.4 Debugging using ptrace

ptrace()システムコールはアーキテクチャ依存。 arch/i386/kernel/ptrace.c

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{

requestに応じて処理が変わる。

PTRACE_tRACEMEを指定すると、プロセスは親プロセスがptrace()を介してそのプロセスを制御できる。言い換えると、プロセスのトレースフラグ(PF_TRACE)がセットされる。

    if (request == PTRACE_TRACEME) {
        /* トレースする準備できてる? */
        if (current->flags & PF_PTRACED)
            return -EPERM;
        /* プロセスフラッグにptraceビットをセットする */
        current->flags |= PF_PTRACED;
        return 0;
    }

呼び出し元のプロセスはPTRACE_ATTACHを使い、全プロセスをそれの子プロセスにし、PF_PTRACEDフラグを設定できる。しかし、呼び出したプロセスのユーザIDとグループIDは、対象のプロセスのユーザIDとグループIDに一致していなければならない。新しい子プロセスにはSIGSTOPシグナルが送られ、停止する。その後、新しい親プロセスの管理下に置かれる。

    if (request == PTRACE_ATTACH) {
        if (child == current)
            return -EPERM;
        if ((!child->dumpable ||
            (current->uid != child->euid) ||
            (current->uid != child->uid) ||
             (current->gid != child->egid) ||
             (current->gid != child->gid)) && !suser())
            return -EPERM;
        /* 同じプロセスを何回も付けることはできない */
        if (child->flags & PF_PTRACED)
            return -EPERM;
        child->flags |= PF_PTRACED;
        if (child->p_pptr != current) {
            REMOVE_LINKS(child);
            child->p_pptr = current;
            SET_LINKS(child);
        }
        send_sig(SIGSTOP, child, 1);
        return 0;
    }

PTRACE_KILLを除き、以下の要求は子プロセスが停止している時点でのみptrace()によって処理される。

    /* 子プロセスのPF_PTRACEDフラグが立っているか? */
    if (!(child->flags & PF_PTRACED))
        return -ESRCH;
    /* 子プロセスは止まっているか? */
    if (child->state != TASK_STOPPED) {
        /* PTRACE_KILL要求か? */
        if (request != PTRACE_KILL)
            return -ESRCH;
    }
    /* カレントプロセスか? */
    if (child->p_pptr != current)
        return -ESRCH;

PTRACE_PEEKTEXTPTRACE_PEEKDATA要求は制御されたプロセスのユーザメモリ空間から32ビットの値を読み出すのに利用できる。Linuxはこれら2つの要求を区別しない。

    switch (request) {
    /* I と D 空間が区切られているとき、これらを固定する必要がある */
        case PTRACE_PEEKTEXT: /* addrのwordを読み取る */ 
        case PTRACE_PEEKDATA: {

PTRACE_PEEKTEXTPTRACE_PEEKDATAがdataの読み取りに利用できるあいだ、codeを読む。

            unsigned long tmp;
            int res;

            res = read_long(child, addr, &tmp);
            if (res < 0)
                return res;
            res = verify_area(VERIFY_WRITE, (void *) data, sizeof(long));
            if (!res)
                put_fs_long(tmp,(unsigned long *) data);
            return res;
        }

PTRACE_PEEKUSR要求はプロセスのuser構造体から、プロセスのデバッグ情報が格納されている場所を読む。プロセスのデバッグ情報は、デバッグトラップ後にプロセッサによって更新され、適切な処理ルーチンによってプロセステーブルに書き込まれる。user構造体は仮想的なものである。sys_ptrace()関数は読み取るべきアドレスを使って、返されるべき情報を決定し、それを与える。したがって、子プロセスのスタック上のレジスタと、プロセステーブルに格納されているデバッグレジスタはsys_ptrace()によって読み込まれるだろう。

    /* USER空間のaddrの位置からwordを読み取る */
        case PTRACE_PEEKUSR: {
            unsigned long tmp;
            int res;

            if ((addr & 3) || addr < 0 || 
                addr > sizeof(struct user) - 3)
                return -EIO;

            res = verify_area(VERIFY_WRITE, (void *) data, sizeof(long));
            if (res)
                return res;
            tmp = 0;  /* 標準の戻り値 */
            if(addr < 17*sizeof(long)) {
              addr = addr >> 2; /* 一時的なハック */

              tmp = get_stack_long(child, sizeof(long)*addr - MAGICNUMBER);
              if (addr == DS || addr == ES ||
                  addr == FS || addr == GS ||
                  addr == CS || addr == SS)
                tmp &= 0xffff;
            };
            if(addr >= (long) &dummy->u_debugreg[0] &&
               addr <= (long) &dummy->u_debugreg[7]){
                addr -= (long) &dummy->u_debugreg[0];
                addr = addr >> 2;
                tmp = child->debugreg[addr];
            };
            put_fs_long(tmp,(unsigned long *) data);
            return 0;
        }

PTRACE_POKEDATAPTRACE_POKETEXTは制御下のプロセスのユーザ空間を変更できるようにする。変更する領域が書き込み保護されているなら、関係のあるページはCopy-on-Writeによって保存される。たとえば、デバッグトラップが引き起こされたように、マシンコードの特定の位置に特別な命令を書き込むために使われる。そのコードは、トラップを動作させている命令が処理されるまで実行され、その時点でデバッグトラップ処理ルーチンはプロセスを中断し、親プロセスに通知する。

        /* I と D 空間が区切られているとき、これらを固定する必要がある */
        case PTRACE_POKETEXT: /* addrの位置のwordへ書き込む */
        case PTRACE_POKEDATA:
            return write_long(child,addr,data);

PTRACE_POKEUSRを使い、仮想のuser構造体を変更することもできる。主な用途は、プロセスのレジスタを変更することである

        case PTRACE_POKEUSR: /* write the word at location addr in the USER area */
            if ((addr & 3) || addr < 0 || 
                addr > sizeof(struct user) - 3)
                return -EIO;

            addr = addr >> 2; /* temporary hack. */

            if (addr == ORIG_EAX)
                return -EIO;
            if (addr == DS || addr == ES ||
                addr == FS || addr == GS ||
                addr == CS || addr == SS) {
                    data &= 0xffff;
                    if (data && (data & 3) != 3)
                    return -EIO;
            }
            if (addr == EFL) {   /* flags. */
                data &= FLAG_MASK;
                data |= get_stack_long(child, EFL*sizeof(long)-MAGICNUMBER)  & ~FLAG_MASK;
            }
          /* Do not allow the user to set the debug register for kernel
             address space */
          if(addr < 17){
              if (put_stack_long(child, sizeof(long)*addr-MAGICNUMBER, data))
                return -EIO;
            return 0;
            };

          /* We need to be very careful here.  We implicitly
             want to modify a portion of the task_struct, and we
             have to be selective about what portions we allow someone
             to modify. */

          addr = addr << 2;  /* Convert back again */
          if(addr >= (long) &dummy->u_debugreg[0] &&
             addr <= (long) &dummy->u_debugreg[7]){

              if(addr == (long) &dummy->u_debugreg[4]) return -EIO;
              if(addr == (long) &dummy->u_debugreg[5]) return -EIO;
              if(addr < (long) &dummy->u_debugreg[4] &&
                 ((unsigned long) data) >= 0xbffffffd) return -EIO;

              if(addr == (long) &dummy->u_debugreg[7]) {
                  data &= ~DR_CONTROL_RESERVED;
                  for(i=0; i<4; i++)
                      if ((0x5f54 >> ((data >> (16 + 4*i)) & 0xf)) & 1)
                          return -EIO;
              };

              addr -= (long) &dummy->u_debugreg;
              addr = addr >> 2;
              child->debugreg[addr] = data;
              return 0;
          };
          return -EIO;

シグナルによって割り込まれたあと、子プロセスはPTRACE_CONT要求によって続行することができる。引数dataは、プロセスが処理を再開するときに操作するシグナルを決めるために利用できる。シグナルを受け取ると、子プロセスは親プロセスに通知し停止する。親プロセスは子プロセスを続行させ、シグナルを処理する必要があるかどうかを判断できるようになった。引数dataがnullの場合、子プロセスはシグナルを処理しない。

PTRACE_SYSCALL要求により、子プロセスはPTRACE_CONTと同じ方法で再開されるが、sys_ptrace()関数はPF_TRACESYSフラグも設定する。子プロセスが次のシステムコールに到達すると、停止してSIGtRAPシグナルを受信する。この時点で、親プロセスはたとえばそのシステムコールの引数を検査できる。もしそのプロセスがPTRACE_SYSCALL要求で続行されると、そのプロセスはシステムコールを処理し終わったあとに停止し、親プロセスが処理結果とエラー変数を読み取れるようになる。

        case PTRACE_SYSCALL: /* continue and stop at next (return from) syscall */
        case PTRACE_CONT: { /* restart after signal. */
            long tmp;

            if ((unsigned long) data > NSIG)
                return -EIO;
            if (request == PTRACE_SYSCALL)
                child->flags |= PF_TRACESYS;
            else
                child->flags &= ~PF_TRACESYS;
            child->exit_code = data;
            child->state = TASK_RUNNING;
/* make sure the single step bit is not set. */
            tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) & ~TRAP_FLAG;
            put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);
            return 0;
        }

PTRACE_KILL要求はSIGKILLシグナルをセットして子プロセスを続行させる。このプロセスはその後アボートするだろう。

    /*
     * make the child exit.  Best I can do is send it a sigkill. 
     * perhaps it should be put in the status that it wants to 
     * exit.
     */
        case PTRACE_KILL: {
            long tmp;

            child->state = TASK_RUNNING;
            child->exit_code = SIGKILL;
    /* make sure the single step bit is not set. */
            tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) & ~TRAP_FLAG;
            put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);
            return 0;
        }

PTRACE_SINGLESTEP要求は、プロセッサのトラップフラグを設定する点でPTRACE_CONTと異なる。したがって、プロセスはひとつのマシンコード命令のみを実行し、デバッグ割り込みを生成する。これは、その後再び割り込まれるプロセスに対し、SIGTRAPシグナルをセットする。言い換えれば、PTRACE_SINGLESTEP要求はマシンコードを1ステップごとに実行する。

        case PTRACE_SINGLESTEP: {  /* set the trap flag. */
            long tmp;

            if ((unsigned long) data > NSIG)
                return -EIO;
            child->flags &= ~PF_TRACESYS;
            tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) | TRAP_FLAG;
            put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);
            child->state = TASK_RUNNING;
            child->exit_code = data;
    /* give it a chance to run. */
            return 0;
        }

PTRACE_DETACH要求は、制御プロセスから制御下にあるプロセスを分離させる。前のプロセスには古い親プロセスとフラグが戻される。PF_PTRACEDPF_TRACESYSはプロセッサのトラップフラグとともに取り消される。

        case PTRACE_DETACH: { /* detach a process that was attached. */
            long tmp;

            if ((unsigned long) data > NSIG)
                return -EIO;
            child->flags &= ~(PF_PTRACED|PF_TRACESYS);
            child->state = TASK_RUNNING;
            child->exit_code = data;
            REMOVE_LINKS(child);
            child->p_pptr = child->p_opptr;
            SET_LINKS(child);
            /* make sure the single step bit is not set. */
            tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) & ~TRAP_FLAG;
            put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);
            return 0;
        }

        default:
            return -EIO;
    }
}

デバッガは以下のようにptraceを使う。forkシステムコールを実行し、子プロセスの関数をPTRACE_TRACEME付きで呼び出す。そこで、検査されるプログラムはexecveによって始まる。PF_PTRACEDフラグがセットされると、execveSIGTRAPシグナルを自身へ送る。ptraceが、Sビットがセットされているプログラムを処理することは許可されていない。もし許可してしまえば、ハッカーに都合がよいのは想像に難くない。execveから戻ると、SIGTRAPシグナルが処理され、プロセスは停止し、親プロセスはSIGCHLDシグナルの送信によって通知される。デバッガはwaitシステムコールによって待機する。これは子プロセスのメモリを検査し、修正してブレークポイントを設定できる。x86プロセッサでこれをおこなう最も単純な方法は、マシンコード内の適切なアドレスにint3命令を書き込むことである。この命令は1バイト長である。

デバッガがPTRACE_CONT要求でptrace()を呼ぶと、子プロセスはint3命令を処理するまで実行し、int3命令を処理した時点で、関連する割り込み処理ルーチンが子プロセスにSIGTRAPシグナルを起こり、子プロセスは割り込まれ、デバッガは再びユーザからの入力を待つようになる。これはたとえば、検査されるべきプログラムを簡単にアボートできる。

もちろん、このシステムコールを使う別の方法もある。straceプログラムは実行されたすべてのシステムコールについてのレポート(トレース)を提供する。以下はstrace lsの出力リストである。当然、stracePTRACE_SYSCALLを使う。

execve("/bin/ls", ["ls"], [/* 60 vars */]) = 0
brk(NULL)                               = 0x5584ed1c6000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5aa603d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=94700, ...}) = 0
mmap(NULL, 94700, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5aa6025000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20b\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=154832, ...}) = 0
mmap(NULL, 2259152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5aa5bf1000
mprotect(0x7f5aa5c16000, 2093056, PROT_NONE) = 0
mmap(0x7f5aa5e15000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x24000) = 0x7f5aa5e15000
mmap(0x7f5aa5e17000, 6352, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f5aa5e17000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\22\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1960656, ...}) = 0
mmap(NULL, 4061792, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5aa5811000
mprotect(0x7f5aa59e7000, 2097152, PROT_NONE) = 0
mmap(0x7f5aa5be7000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d6000) = 0x7f5aa5be7000
mmap(0x7f5aa5bed000, 14944, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f5aa5bed000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpcre.so.3", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \25\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=464824, ...}) = 0
mmap(NULL, 2560264, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5aa559f000
mprotect(0x7f5aa560f000, 2097152, PROT_NONE) = 0
mmap(0x7f5aa580f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x70000) = 0x7f5aa580f000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\16\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=14632, ...}) = 0
mmap(NULL, 2109712, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5aa539b000
mprotect(0x7f5aa539e000, 2093056, PROT_NONE) = 0
mmap(0x7f5aa559d000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f5aa559d000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360a\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=144776, ...}) = 0
mmap(NULL, 2221160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5aa517c000
mprotect(0x7f5aa5196000, 2093056, PROT_NONE) = 0
mmap(0x7f5aa5395000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19000) = 0x7f5aa5395000
mmap(0x7f5aa5397000, 13416, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f5aa5397000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5aa6023000
arch_prctl(ARCH_SET_FS, 0x7f5aa6024540) = 0
mprotect(0x7f5aa5be7000, 16384, PROT_READ) = 0
mprotect(0x7f5aa5395000, 4096, PROT_READ) = 0
mprotect(0x7f5aa559d000, 4096, PROT_READ) = 0
mprotect(0x7f5aa580f000, 4096, PROT_READ) = 0
mprotect(0x7f5aa5e15000, 4096, PROT_READ) = 0
mprotect(0x5584ecf1e000, 4096, PROT_READ) = 0
mprotect(0x7f5aa6040000, 4096, PROT_READ) = 0
munmap(0x7f5aa6025000, 94700)           = 0
set_tid_address(0x7f5aa6024810)         = 10097
set_robust_list(0x7f5aa6024820, 24)     = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7f5aa5181c70, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f5aa518f150}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7f5aa5181d00, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f5aa518f150}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
statfs("/sys/fs/selinux", 0x7ffe2455e660) = -1 ENOENT (No such file or directory)
statfs("/selinux", 0x7ffe2455e660)      = -1 ENOENT (No such file or directory)
brk(NULL)                               = 0x5584ed1c6000
brk(0x5584ed1e7000)                     = 0x5584ed1e7000
openat(AT_FDCWD, "/proc/filesystems", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, "nodev\tsysfs\nnodev\trootfs\nnodev\tr"..., 1024) = 383
read(3, "", 1024)                       = 0
close(3)                                = 0
access("/etc/selinux/config", F_OK)     = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=4045984, ...}) = 0
mmap(NULL, 4045984, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5aa4da0000
close(3)                                = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=42, ws_col=98, ws_xpixel=0, ws_ypixel=0}) = 0
open(".", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents(3, /* 7 entries */, 32768)     = 232
getdents(3, /* 0 entries */, 32768)     = 0
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
write(1, "interrupt.md  paging_under_linux"..., 77interrupt.md  paging_under_linux.md  pipes.md  ptrace.md  virtual_address.md
) = 77
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

ptrace()によって提供される関数の範囲は、マルチタスク環境でプログラムをデバッグするには十分である。悪い面は、ひとつのシステムコールを使ってアドレス空間に32ビットの値を読み書きするのはかなり非効率的である。