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

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

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

http://twitter.com/alohakun/status/4481139191

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

こんなときに便利

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

デバッグ中に、大体どこで変なことが起きているかはわかっているけど、細かい場所は特定できていないとき、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のビルドに必要なものをインストールします。ソースはCVSかGitで取得できるので、それらも必要に応じてインストールしておきましょう。

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!!!