目的
システムコール ptrace(2) を使用してデバッグする。 ptrace を使用して簡易的なデバッガを作成することで、GDB などの汎用デバッガより細かい制御を行うことも可能になる。
環境
- OS: CentOS 5.5
- Kernel: 2.6.18-194.el5 x86_64
- CPU: Intel® Xeon® CPU E5345 (Virtual Machine on VMware)
前提知識
システムコール ptrace(2)
別のプロセスにアタッチしたり、別のプロセスのメモリやレジスタを参照、書込み等を行うことができるシステムコール。 デバッガを実装するために使用される。
1 2 3 4 |
|
request
にアタッチしたプロセスに何を行うかを指定する。
例えば、以下が指定できる。(参考: man ptrace
)
PTRACE_ATTACH
pid
で指定したプロセスにアタッチする。アタッチしたプロセスは子プロセスとしてトレースできるようにする。(引数addr
とdata
は無視される。)PTRACE_PEEKTEXT, PTRACE_PEEKDATA
メモリの
addr
の位置を参照する。(引数data
は無視される。)PTRACE_PEEKUSR
USER 領域のオフセット
addr
の位置を参照する。(引数data
は無視される。)PTRACE_POKETEXT, PTRACE_POKEDATA
data
をメモリのaddr
の位置に書込む。PTRACE_POKEUSR
data
を USER 領域のオフセットaddr
の位置に書き込む。PTRACE_GETREGS
レジスタの値を
data
にコピーする。data
は user_regs_struct 構造体(<linux/user.h>)。(引数addr
は無視される。)PTRACE_SETREGS
data
をレジスタにコピーする。data
は user_regs_struct 構造体(<linux/user.h>)。(引数addr
は無視される。)
インストラクション INT 3
INT X
(X は 0~255 の数値) は x86, x86-64 プロセッサのインストラクションで、ソフトウェア割り込みを発生させる。
特に INT 3
(オペコード: 0xCC
) により SIGTRAP シグナルが発生し、処理が中断される。これは、デバッガがブレイクポイントを設定するために使用される。
レジスタ IP
x86-64 プロセッサのレジスタ RIP (インストラクションポインタ)には次に実行される命令のアドレスが格納されている。(プログラムカウンタとも呼ばれる。)
ptrace によるブレイクポイントの設定
ptrace では、アタッチしたプロセスのメモリやレジスタを参照したり、書込むといった原始的なことしかできない。
したがって、ptrace によってブレイクポイントを設定してデバッグするには、以下のような手順が必要。
PTRACE_ATTACH でターゲットのプロセスにアタッチ。
PTRACE_PEEKTEXT でブレイクポイントを設定したいアドレスの命令を一時的に保存しておく。
PTRACE_POKETEXT で 2. のアドレスの命令を INT 3 (0xCC)に書換える。
PTRACE_CONT でプロセスを再開させる。
waitpid(2) でブレイクするのを待つ。
プロセスが INT 3 で書換えたアドレスでブレイクする。
次に実行する命令のアドレスを示すレジスタ IP は INT 3 で書換えたアドレス(ブレイクポイントを設定したアドレス)の次の位置(+1バイトの位置)を指しているので、PTRACE_GETREGS/PTRACE_SETREGS で IP をブレイクポイントを設定したアドレスに書換えて戻す。
PTRACE_POKETEXT で 3. で書換えた命令を 2. で保存していたオリジナルに戻す。(ブレイクポイントを設定したアドレスのメモリを元の命令に戻す。)
これでブレイクしたい位置で中断されているので、レジスタを見たり、メモリを参照したりして任意のデバッグを行う。
再度ブレイクさせたかったら 2. に戻る。 ただし、同じ命令のアドレスでブレイクさせたかったら、PTRACE_SINGLESTEP で 1 命令だけ進めてから、2. に戻る。(そうでないと現在の IP 位置を INT 3 で書換えてしまい、すぐにブレイクして処理が進まなくなってしまう。)
デバッグが終わったら、PTRACE_DETACH でプロセスからデタッチする。
具体例
上述のような方法でブレイクポイントを設定してデバッグする簡易的なデバッガを作成する。
簡易デバッガ
引数としてアタッチしたいプロセスの PID と、ブレイクするアドレスを指定して、ブレイクポイントでレジスタ RDI の値を表示する簡易的なデバッガ。
ブレイクするアドレスとして関数の先頭アドレスを指定すれば、レジスタ RDI には第 1 引数が入っているはずなので、このプログラムを使って対象の関数が実行される度に第 1 引数を表示することができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
|
なお、レジスタの書換えは以下のように PTRACE_GETREGS/PTRACE_SETREGS でなくて、PTRACE_POKEUSER でも行けた。(43行目)
1
|
|
もちろん、PTRACE_GETREGS/PTRACE_SETREGS を使って以下のようにしてもよい。
1 2 3 4 5 |
|
あと、デバッグを止めるときはカレントディレクトリに quit
というファイルを置くことにした。($ touch ./quit
とかで作ればよい。)
コンパイルしておく。
$ gcc -o ptrace_demo ptrace_demo.c
デバッグ対象プログラム
デバッグ対象として以下の簡単なプログラムを用意。1秒毎に func 関数が呼ばれている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
コンパイルしておく
$ gcc -o sample sample.c
デバッグ例
上記の func 関数が呼ばれる度に第 1 引数(レジスタ RDI) の値を出力するデバッグを行う。
まず、func のアドレスを確認。
$ nm sample | grep func
00000000004004d8 T func
func のアドレスは 0x00000000004004d8 。
プログラムを実行。
$ ./sample
hello, world: 1
hello, world: 2
hello, world: 3
hello, world: 4
hello, world: 5
hello, world: 6
...<略>
PID, アドレスを指定してこの簡易デバッガを実行してみる。
$ ps -ef | grep sample
hashi 21250 32254 0 17:33 pts/2 00:00:00 ./sample
$ ./ptrace_demo 21250 0x00000000004004d8
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
arg 1: 27
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
arg 1: 28
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
arg 1: 29
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
...<略>
func でブレイクして、第 1 引数(レジスタ RDI) の値が出力できている。
止めるときは同じディレクトリに quit という名前のファイルを作れば、デタッチしてデバッグ終了する。
$ touch quit
GDB で実行した場合
上述の方法で GDB で以下のようにしたときと同じようにデバッグすることができた。
$ gdb ./sample 21250
(gdb) b func
Breakpoint 1 at 0x4004dc
(gdb) command
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>p $rdi
>c
>end
(gdb) c
Continuing.
Breakpoint 1, 0x00000000004004dc in func ()
$1 = 27
Breakpoint 1, 0x00000000004004dc in func ()
$2 = 28
Breakpoint 1, 0x00000000004004dc in func ()
$3 = 29
...<略>
ptrace を使用したデバッガを作るとで GDB より柔軟に情報採取をすることも可能になる。