gdb hacks - ハードウェアのデバッグ機能を使う (後編)
gdb hacks 第 6 回。前回は IA-32 で gdb がデバッグレジスタをどのように活用しているかを見ました。今回はデバッグレジスタをプロセスが自分自身で利用する方法について考えてみます。
IA-32 のデバッグレジスタ DR0-DR7 へのアクセスは、リアルモードまたは特権順位 0 のプロテクトモードでしか許されていません。Linux では ptrace システムコールを使ってカーネルに読み書きしてもらうことになります。
しかしながらプロセスは自分自身に対して ptrace することはできません。そこで、自分のデバッグレジスタにアクセスしたい場合は fork を使って子プロセスに ptrace をしてもらいます。
とりあえずデバッグレジスタに値を設定する関数を書いてみます (エラーチェックなどはいいかげんですので注意してください)。
#include <sys/user.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/ptrace.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> void set_debugregs(unsigned long *v) { if (!fork()) { int i, *p = ((struct user *)0)->u_debugreg; pid_t ppid = getppid(); ptrace(PTRACE_ATTACH, ppid, NULL, NULL); waitpid(ppid, NULL, 0); for (i = 0; i < 8; i++, p++, v++) { if (i == 4 || i == 5) continue; if (ptrace(PTRACE_POKEUSER, ppid, p, *v) < 0) fprintf(stderr, "ptrace failed: dr%d = %08lx\n", i, *v); } ptrace(PTRACE_DETACH, ppid, NULL, NULL); exit(0); } wait(NULL); }
このように値をセットする関数はわりと簡単に書けますが、逆に値を読み出したい場合は、子プロセスが行った ptrace の結果を得るためになんらかのプロセス間通信を行う必要があるでしょう。
以下はテストプログラムです。
#include <signal.h> #include <asm/ucontext.h> #define TRY(x) do { fputs("Trying " #x "\n", stderr); x; } while (0) static void trap_handler(int n, siginfo_t *si, struct ucontext *uc) { fprintf(stderr, " Trapped at 0x%08lx\n", uc->uc_mcontext.eip); uc->uc_mcontext.eflags |= 1<<16; /* Set RF; helpless */ } int tmp, data0, data1; void func(void) {} int main(void) { unsigned long regs[] = { (unsigned long)&data0, (unsigned long)&data1, (unsigned long)func, 0, /* unused */ 0, /* reserved */ 0, /* reserved */ 0, /* cant read */ 0x00fd013f, /* Trap conditions 0: write, 1: read/write, 2: exec, 3: unused */ }; struct sigaction sa = { .sa_sigaction = (void *)trap_handler, .sa_flags = SA_RESTART | SA_SIGINFO, }; sigemptyset(&sa.sa_mask); sigaction(SIGTRAP, &sa, NULL); set_debugregs(regs); TRY(tmp = data0); TRY(tmp = data1); TRY(data0 = 1); TRY(data1 = 1); TRY(func()); return 0; }
上記コードをまとめたファイル debugregs.c を用意してあります。これをコンパイルして実行すると以下のようになります (無限ループになりますので ^C で止めてください)。
% gcc -g -Wall -O2 debugregs.c % ./a.out Trying tmp = data0 Trying tmp = data1 Trapped at 0x080487f5 Trying data0 = 1 Trapped at 0x0804882f Trying data1 = 1 Trapped at 0x0804885b Trying func() Trapped at 0x080486f0 Trapped at 0x080486f0 Trapped at 0x080486f0 ...
データアクセスをテストする最初の 4 つの TRY() は意図した通りに動作しているようです。命令実行をテストする最後の TRY() で無限ループになるのはなぜでしょうか。
これはシグナルハンドラから復帰後、デバッグ例外を起こした同じ命令から実行再開しようとするためです。本来ならシグナルハンドラでステータスレジスタ (DR6) を調べ、設定を調整してから実行再開すべきなのですが、手を抜いているのでこうなります。
また EFLAGS の RF というビットをセットしておくと、一回だけ命令実行によるデバッグ例外の発生を抑止するという機能があり、これを使えばデバッグレジスタを書き換えることなく実行継続できるのですが、残念ながら Linux ではこれを活用する手段がないようです。
さてこのようにデバッグレジスタを使えば、一般的なページ単位のメモリ保護機構には不可能な、微細なメモリスポットのアクセスも感知できることがわかりました。設定できるのは最大 4 ワードというごく少量のメモリではありますが、うまく使えばなにかの役には立つでしょう。
つづく。
参考文献:
- IA-32 Intel® Architecture Software Developer's Manual
CHAPTER 18 Debugging and Performance Monitoring - 図解32ビットマイクロコンピュータ 80486の使い方 W. B. スルヤント
13. デバッグサポート
カテゴリ
gdb hacksトラックバック(0)
このブログ記事を参照しているブログ一覧: gdb hacks - ハードウェアのデバッグ機能を使う (後編)
このブログ記事に対するトラックバックURL: http://www.keshi.org/mt/mt-tb.cgi/37
コメントする