「デバッガの理論と実装」というすばらしい(らしい)本があって、絶版だし僕は持っていないから読んだことはないのだけど、デバッガの作りかたって結構興味がある。他のプロセスのメモリをいじったりすることは、普通にプログラミングしている分にはそういう機会ってないので(僕はユーザタスクのトレーサを作るときにずいぶんといじったけど)。
コンピュータをある程度低いレイヤーから見ると、カーネルタスクとユーザータスクとではCPUの特権状態とかは当たり前だけど違うし、従ってそれぞれでのでバッギングの実現方法はやっぱり変わってくるんだろうと思う。
僕はカーネルのデバッグとかはどういう風に実現されているのかって知らないしあまり想像つかないし、CPUに結構昔から装備されているデバッギングレジスタとかも触ったことないからよくはわからない。
で、知っているのは、x86におけるユーザタスクのアプリケーションのブレークポイントの張り方一つだけなのだけど、この前それの話題になったときにずいぶん驚かれたので、まあ知っていると便利ですよ程度のお話。
ブレークポイントはCPUがそれを実行した瞬間に、自プロセスへのアタッチ元に制御を移すことなのだけど、x86_64ではこれは
int3(マシン語にすると0xCC)
という1バイト命令を埋め込むことで実現される。つまり、ブレークポイントを張りたい命令のアドレスにint3(0xCC)を埋めこめば、アタッチ元に制御が戻る。
これの何が便利かというと、自分がC/C++のプログラムを何度もコンパイルしては走らせを繰り返しているときなどに、
asm volatile("int3")
と書いてコンパイルして、できたバイナリをgdbの中でrunさせれば自動的にそこで止まってデバッガに制御を戻してくれるのだ。volatileは最適化によるコード削除から守るためのものなので、基本的には書いておいた方が無難だと思う。
実際の使い方を簡単に書くと、
#include <iostream> int main(void) { std::cout << "pre break" << std::endl; asm volatile("int3"); std::cout << "post break" << std::endl; return 0; }
というコードがあったとき、
/Users/hogehoge% g++ -g foo.cpp /Users/hogehoge% gdb ./a.out GNU gdb 6.3.50-20050815 (Apple version gdb-1824) (Thu Nov 15 10:42:43 UTC 2012) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries ... done (gdb) run Starting program: /Users/hogehoge/a.out Reading symbols for shared libraries ++.............................. done pre break Program received signal SIGTRAP, Trace/breakpoint trap. <=== 止まる main () at foo.cpp:6 6 std::cout << "post break" << std::endl; (gdb) continue Continuing. post break Program exited normally. (gdb)
という風にできて楽ちんなのだ。毎回同じところにブレークポイントを埋め込むのはめんどくさいし、関数の中のある部分だけを止めてみたいとか言うときにとても便利に使える。ちなみに、デバッグ情報はつけといた方が当たり前だけど便利だしいいですね。
ちなみに、簡単なデバッガを作ろうと思うと、アタッチ先のプロセスに対して、ptraceをつかってブレークしたい目的のアドレスに対してint3を埋め込んで(バイナリの場合0x90を書き込んで)、書き込む前にそこに書き込まれていたバイトを覚えておく。ブレークポイントにヒットして、デバッガ側に制御が戻ってきたら、さらにptraceを使って、命令ポインタ(IPレジスタ)を1つ巻き戻して、ブレークポイントのアドレスに元の命令を書き戻して、そこから1回だけシングルステップ実行して、再度int3を書き込んで、あとは普通にContinueさせればいいというのはちょっと考えれば想像がつくと思う。
実際には、Macだとptraceで他プロセスのメモリに任意のバイトを注入することはできなくて(Macのプロセス間通信を使う必要がある)、さらにLinuxでもMacでも最近はASLR(アドレススペースレイアウトのランダム化、簡単に言えば、バイナリ中のシンボルなどのアドレス配置をロード時にシャッフルすること)がローダーによって行われたりしているので、実際に動くものを作ろうと思うと、他にもたくさん考えることはあるのだけれど。
とりあえず、最近はいわゆるprintfデバッグと上記の方法、あとはプロセスが落ちたときのコアダンプをメインで使うようになりました。
ちなみに、Pythonでは(これもとてもよく知られたテクニックですが)
import pdb; pdb.set_trace()
でできますね。これも便利。僕はpdbの代わりにipdbを使う方が好きですが。