日記を書く [・w・] はやみずさん

2009-09-30

GDBで歴史をさかのぼれるように!なりました! GDB 7.0 の新機能Reverse Debuggingを使ってみた

Twitter上で、@alohakun が言及していた GDB の reverse debugging の機能を使ってみました。

GDBトレースと逆実行機能入ったのか。 http://www.gnu.org/software/gdb/news/reversible.html

Twitter / ?

まずは簡単な使い方を説明したあとに、インストール方法を説明します。

こんなときに便利

  • 「変なこと」が起きている大体の場所がわかっているとき

デバッグ中に、大体どこで変なことが起きているかはわかっているけど、細かい場所は特定できていないとき、reverse debuggingが効果を発揮します。

GDBでステップ実行をしていて、「しまった!行きすぎた!」という経験はよくあると思います。こういうとき、今まではプログラムの実行を最初からやり直してあげる必要があったのですが、reverse debuggingでは、next に対応する reverse-next や、stepに対応する reverse-step で前のステップや、前の行に戻って実行を繰り返すことができます。

  • Segmentation Faultで落ちた原因を調べる

Segmentation fault でプログラムが落ちたとき、これまでのgdbではプログラムが落ちた時点での変数の値しか調べることができませんでした。Reverse debugging では、segmentation faultで落ちた時点から、プログラムの実行を遡って変数の値の変遷を調べることができます。

簡単な使い方の説明

Reverse debugging は、以下の手順で使うことができます。

まずは gdb 7.0 で実行するプログラムを読み込みましょう。これは、いままでのGDBと同じです。

サンプルとして、こんなプログラムを使ってみました。

int
main(int argc, char **argv){
    int x;
    x = 0;
    x++;
    x++;

    return 0;
}

foo.c という名前で保存して、コンパイルして、gdb で読み込みます。

$ gdb ./foo 
GNU gdb 6.8-debian
... (中略) ...
This GDB was configured as "x86_64-linux-gnu"...
(gdb) 

Reverse debugging では、変数の値の変化などを逐一記録することで、その機能を実現しています。そのため、実行の途中で「ここから記録を始めます」と言ってあげなければいけません。逆に、それを言っておかないと歴史を遡ることはできません。

とりあえず、main関数ブレークポイントを設定して、ブレークポイントに到達したところで記録を始めましょう。

(gdb) target record 
Process record: the program is not being run.
(gdb) b main
Breakpoint 1 at 0x4005c4: file foo.c, line 14.
(gdb) run
Starting program: /tmp/foo 

Breakpoint 1, main (argc=1, argv=0x7fffffffe608) at foo.c:14
14	    x = 0;
(gdb) record
(gdb)

record という命令がでてきました。record 命令で、逆戻りできるよう記録を始めます。プログラムの実行を逆戻りできるのは、この記録を始めたポイントまでです。

さて、これで記録が始まりました。実行をすすめてみましょう。

(gdb) n
9	    x++;
(gdb) 
10	    x++;
(gdb) 
12	    return 0;

return文の実行の直前まできました。ここで、xの値をしらべてみると、まあ当然ですが2になっています。

(gdb) p x
$1 = 2

さて、いよいよ実行を遡って、昔の値を調べてみましょう! 逆方向への実行には step や next のプレフィックスに reverse- をつけたコマンドが使えます。

xの値の変化がわかりやすいよう、display x をしてからやってみます。

(gdb) display x
1: x = 2
(gdb) reverse-next
10	    x++;
1: x = 1
(gdb) 
9	    x++;
1: x = 0
(gdb)

No more reverse-execution history.
main (argc=1, argv=0x7fffffffe608) at foo.c:8
8	    x = 0;
1: x = 0

x の値が 2 → 1 → 0 と戻っていくのがわかりますね。たしかに逆戻りできているようです。これい以上戻れない、つまり record を始めた地点までくると、"No more reverse-execution history."といってもう記録がのこっていないことを教えてくれます。

ちなみに、record で記録を始めると、実行速度が無茶苦茶遅くなります。なので、問題としている範囲を通りすぎたら、記録をしないようにしたいものです。そんなときは、 record stop命令が使えます。

(gdb) record stop
Process record is stoped and all execution log is deleted.
(gdb) n
9	    x++;
1: x = 0
(gdb) reverse-next
Target child does not support this command.

record stop したあとは、reverse-next できなくなっていることがわかりますね。

使い方まとめ
  • record で記録を始める
  • reverse-* で実行を巻き戻す
    • reverse-next : 1行前へ
    • reverse-step : 1ステップ前へ
    • reverse-continue : 1つ前のブレークポイント
    • reverse-stepi : 1つ前の機械語命令へ
    • reverse-nexti : 1つ前の機械語命令へ。直前が関数呼び出しからのreturnの場合、その関数呼び出しの実行直前まで戻る
    • reverse-finish : 今実行している関数が呼び出される直前へ
  • record stop で記録をやめる

インストール方法

手元には Ubuntu Linux 9.04 しかすぐにできる環境がないので、それをベースにして説明します。おそらく Debian 系ではほとんど同じ手順でいけるでしょう。

Reverse debugging機能を供えた GDB 7.0 はまだリリースされていません(もうすぐリリースされる?)。なので、開発版のブランチからとってくることにします。

まずは、gdbビルドに必要なものをインストールします。ソースはCVSGitで取得できるので、それらも必要に応じてインストールしておきましょう。

Debian系のOSでは、apt-getで簡単にビルドに必要なものをインストールすることができます。

$ sudo apt-get build-dep gdb

これでインストールされるのは GDB 6.8 のビルドに必要なものですが、うちの環境では問題なくビルドできたので多分大丈夫でしょう。

さて、次は開発版のレポジトリをもってきます。ここでは Git を使います。CVSレポジトリは、Current GDBからもってくることができます。

$ git clone git://sourceware.org/git/gdb.git

いよいよビルドに入りましょう。gdbビルドは、標準的な ./configure && make &&

ake install でいけます。とりあえずホームディレクトリ以下に適当なディレクトリを作ってインストールするよう、prefixオプションをつけてみました。

$ ./configure --prefix=${HOME}/usr/gdb && make && make install

これで、うまくビルドされると ${HOME}/usr/gdb/bin/gdb に実行ファイルができます。今日自分で試してみた時点では、何のエラーもなくビルドが終了してインストールされました。

あとは、今インストールしたものにパスを通すだけで reverse debugging が使えるようになります。Enjoy!!!


参考

alohakunalohakun 2009/10/01 07:38 ちなみに僕はgreenteaさんの獲物で知りました。

http://www.jitu.org/~tko/cgi-bin/bakanoemono.rb

会社の先輩や社長に得意げに話してみたら「なんや、今頃知ったんかw」みたいな反応でした(苦笑) 既に97年のGCC Summitで論文が出ていて、社内では話題になっていたそうです。

http://ols.108.redhat.com/2007/GCC-Reprints/GCC2007-Proceedings.pdf

(69 pageからの"Reversible Debugging", Paul Brook Daniel Jacobowitzですね。GCC/GDB/binutilsなどのコミッタを何人も抱え、QEMUなどでも頑張っている、開発環境ベンダ CodeSourcery 社の人たちです。)

GDBの場合は、全命令にブレークポイントを張って、1命令実行するたびにデバッガに制御を戻してレコードしてるんでしょうね。(例えばx86のシングルステップモードなど、CPUの機能を使っているのか、ソフトウェアブレークを張りまくるのかは調べてませんけど。というか論文ちゃんと読んでない…)メチャクチャ遅くなって当然だと思います。

これがJTAGでリアルタイムトレースのデータで逆実行できるようになれば、速度の問題は解決すると思います。(いろいろ難しい問題もたくさんあるのですが。)ユーザプロセスのデバッグが中心であるGDBは、たぶんそこまでする気は無いのだと思いますが、組み込み業界では必要でしょうね。

alohakunalohakun 2009/10/01 07:39 97年じゃない、2007年です…

hayamizhayamiz 2009/10/01 11:02 > GDBの場合は、全命令にブレークポイントを張って、1命令実行するたびにデバッガに制御を戻してレコードしてるんでしょうね。(例えばx86のシングルステップモードなど、CPUの機能を使っているのか、ソフトウェアブレークを張りまくるのかは調べてませんけど。というか論文ちゃんと読んでない…)メチャクチャ遅くなって当然だと思います。

それだと確かにすごい遅そうですねえ。
ちょっと試してみた感じだと、各ステップのメモリの変化を記録していて、2回目以降の実行はそれを replay するだけ、という感じですね。なので、システムコールとかは replay 時には呼ばれていないようにみえます。

OCamlのデバッガだと、あれは定期的にforkしまくってcheckpointingをしてるのでそんなに遅くならないんですが、戻って再実行するのは replay ではないと思うので、システムコールは実行の度に呼ばれてしまいそうな予感。Reversible debuggingを実現する方法を色々調べてみて、まとめるのも面白そうです。KMCブログで是非w

alohakunalohakun 2009/10/01 11:48 GDBの今のやり方を、うっかりいつも社内で使ってる用語の延長として「トレース」や「逆実行」と言ってしまったのはちょっと誤解を招きそうでまずかったかも、と思っているところです。

どちらかというと、(ステップ実行の)ヒストリ実行に近いかも。
(普通にgoして止めて、そこから本当に逆向きに命令を解釈して実行しているわけではない、という意味。)

本物の逆実行は、シミュレータなどを使った本物の逆向き実行(デバッガ内臓のCPUシミュレータでも、QEMUなどのJITでも良い) + トレースデータ(例えばARMの場合は、ETM/ETBなどで取る)によってpcの動きを補正(割り込みなどで急にpcが変わる場合もあるから)という感じになるのかなと思います。
これなら実行中の効率は全く落ちません。

# あとJTAGデバッガの場合は、そもそもカーネル空間もユーザ空間もあんまり関係なく、普通にシステムコールというかOSの中まで遡れるかな。OCamlとかのデバッガは、面白そうだけどちょっとよくわからないな…

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。