gcc+gdbによるプログラムのデバッグ 第2回 変数の監視、バックトレース、その他のコマンド
前回までに、デバッガを使用する上での最低限のことを覚えました。
- ステップ実行
- 変数の表示、変更
- ブレークポイント
今回は少しレベルを上げて、よりデバッガを使いこなすためのコマンドを紹介します。
ウォッチポイント
Section titled “ウォッチポイント”ウォッチポイントはブレークポイントに近いものですが、ブレークポイントのように「ある地点に遭遇したら停止」ではなく、「監視している変数を操作したら停止」という流れになります。
ファイル内から該当する変数名を探せばいいと考えるかもしれませんが、C言語ではポインタによる変数の別名を付けることが可能であるため、そう単純にはいきません。
書き込みの監視
Section titled “書き込みの監視”あまりよいサンプルが思いつかなかったため、簡単で無意味な例を示します。
counter.c
#include <stdio.h>
void set_counter(int *);
int count = 1;int watchee = 0;
int main(int argc, char **argv) { int local = 0; set_counter(&local); set_counter(&watchee); printf("local = %d\n", local); printf("watchee = %d\n", watchee); return 0;}
void set_counter(int *p) { *p = count++;}今回は、「watchee」という変数について変更を加える箇所を調査します。
watcheeの変更を調査するには、set_counter関数にブレークポイントを設定するだけでは不十分で(条件付ブレークポイントでいろいろやれば可能ですが…)、watchee自体にウォッチポイントを設定する必要がありそうです。
まずはこのプログラムをコンパイルし、デバッガを起動します。
$ gcc -g -O0 counter.c$ gdb a.out...watchコマンドを使用して、変数watcheeにウォッチポイントを設定します。
設定方法は「watch 変数名」です。
(gdb) watch watcheeHardware watchpoint 1: watcheeinfo watchpointsコマンドを使用して、ウォッチポイントの設定を確認します。
(gdb) info watchpointsNum Type Disp Enb Address What1 hw watchpoint keep y watcheerunコマンドによってプログラムを起動させましょう。
(gdb) runStarting program: /***/a.outHardware watchpoint 1: watcheeHardware watchpoint 1: watcheeHardware watchpoint 1: watcheeしばらくすると、ウォッチポイントで監視している変数を操作して、プログラムが一時停止させられます。
Old value = 0New value = 20x080483bb in set_counter (p=0x80495c0) at counter.c:1818 *p = count++;(gdb)ここで注意するのは、ウォッチポイントは変更後にプログラムが停止します。
逆に、ブレークポイントはブレークポイントの行を実行前にプログラムが停止します。
(gdb) print *p$1 = 2(gdb) print count$2 = 2本当に変更されているのがwatcheeなのか、変数のアドレスで調査します。
(gdb) print p$3 = (int *) 0x80495c0(gdb) print &watchee$4 = (int *) 0x80495c0(p == &watchee) なので、pが指す先はwatcheeであることが確認できました。
プログラムを再開します。
(gdb) continueContinuing.これ以降ではwatcheeに書き込む部分が無いため、ウォッチポイントによる停止は行われません。
local = 1watchee = 2
Program exited normally.最後に、gdbを終了します。
(gdb) quit読み込みの監視
Section titled “読み込みの監視”先ほどのwatchコマンドは書き込みのみを監視しますが、rwatchコマンドを使用すると該当変数の読み込みを監視します。
$ gcc -g -O0 counter.c$ gdb a.out...今回はwatchコマンドの変わりにrwatchコマンドを使用します。
(gdb) rwatch watcheeHardware read watchpoint 1: watchee(gdb) runStarting program: /***/a.outHardware read watchpoint 1: watcheeHardware read watchpoint 1: watcheelocal = 1Hardware read watchpoint 1: watchee
Value = 2rwatchではwatchコマンドでは停止しなかった「watcheeの読み込み」に反応して、プログラムが一時停止します。
0x0804839a in main (argc=1, argv=0xbffc5b14) at counter.c:1313 printf("watchee = %d\n", watchee);確認し終えたら、最後まで実行して終了しましょう。
(gdb) continueContinuing.watchee = 2
Program exited normally.(gdb) quit呼び出し元のトレース
Section titled “呼び出し元のトレース”ブレークポイントなどを設定してある関数内でプログラムが停止した場合、その関数がどこから呼ばれたかなどの情報が必要になる場合があります。
gdbには呼び出し元のトレースを行う機能が備わっており、backtraceコマンドで行います。
例にはfactorial.cを使用します。
$ gcc -g -O0 factorial.c$ gdb a.out...factorialの再帰呼び出しが一番深くなる地点が n=1 であるので、そこにブレークポイントを設定します。
(gdb) break factorial if n == 1Breakpoint 1 at 0x80483ff: file factorial.c, line 21.factorial(5)としてプログラムを起動すると、すぐにブレークポイントに遭遇します。
(gdb) run 5Starting program: /***/a.out 5
Breakpoint 1, factorial (n=1) at factorial.c:2121 if (n < 1) {この状態で、backtraceコマンドを実行することによって、関数の呼び出し元を全てトレースすることができます。
(gdb) backtrace#0 factorial (n=1) at factorial.c:21#1 0x0804842a in factorial (n=2) at factorial.c:28#2 0x0804842a in factorial (n=3) at factorial.c:28#3 0x0804842a in factorial (n=4) at factorial.c:28#4 0x0804842a in factorial (n=5) at factorial.c:28#5 0x080483d1 in main (argc=2, argv=0xbfef6274) at factorial.c:13現在位置を確認して、現在のfactorial関数上で実引数の値を調査します。
(gdb) list16 return 0;17 }1819 /* factorial */20 int factorial(int n) {21 if (n < 1) {22 return 0;23 }24 else if (n == 1) {25 return 1;(gdb) print n$1 = 1factorialのn=1にブレークポイントを設定しており、この位置では引数nの値が1になっていることが確認できます。
現状ではfactorialの引数の値は、factorial(1)上での値しか確認できません。
そこで、frameコマンドを使用して、呼び出しもとの関数についての情報を取得します。
frameコマンドは呼び出し元の関数に移動するためのコマンドで、「frame フレーム番号」という形式で指定します。
ここでのフレーム番号は、backtraceコマンド実行時に左側に表示される数値です。
(gdb) backtrace#0 factorial (n=1) at factorial.c:21#1 0x0804842a in factorial (n=2) at factorial.c:28#2 0x0804842a in factorial (n=3) at factorial.c:28#3 0x0804842a in factorial (n=4) at factorial.c:28#4 0x0804842a in factorial (n=5) at factorial.c:28#5 0x080483d1 in main (argc=2, argv=0xbfef6274) at factorial.c:13それでは、#3のフレーム(関数内での状態)に移動した後に、関数の情報を取得しましょう。
(gdb) frame 3#3 0x0804842a in factorial (n=4) at factorial.c:2828 int m = factorial(n - 1);(gdb) print n$2 = 4続けて違うフレームに移動します。
(gdb) backtrace#0 factorial (n=1) at factorial.c:21#1 0x0804842a in factorial (n=2) at factorial.c:28#2 0x0804842a in factorial (n=3) at factorial.c:28#3 0x0804842a in factorial (n=4) at factorial.c:28#4 0x0804842a in factorial (n=5) at factorial.c:28#5 0x080483d1 in main (argc=2, argv=0xbfef6274) at factorial.c:13factorial関数を呼び出す以前の、main関数まで戻ってみます。
(gdb) frame 5#5 0x080483d1 in main (argc=2, argv=0xbfef6274) at factorial.c:1313 n = factorial(m);ここで、factorial関数を呼び出した際の実引数mの値を調査します。
(gdb) print m$3 = 5factorial(5)が呼び出されていることが確認できました。
最後まで実行した後、終了します。
(gdb) cContinuing.factorial(5) = 120
Program exited normally.(gdb) quitコアファイルの活用
Section titled “コアファイルの活用”この項はCygwinなどの環境で正しく動作しません。
以下のコードを見てください。
fibonacci1.c
#include <stdio.h>#include <stdlib.h>
int fibonacci(int);
int main(int argc, char **argv) { int k, result; if (argc != 2) { printf("usage: %s n\n",argv[0]); return 1; } k = atoi(argv[1]); if (k < 1) { printf("illegal argument k=%d\n",k); return 1; } result = fibonacci(k); printf("fibonacci(%d) = %d\n", k, result); return 0;}
int fibonacci(int k) { if (k == 1) { return 1; } else { return fibonacci(k-1) + fibonacci(k-2); }}このプログラムはフィボナッチ数列を計算するためのものです。
フィボナッチ数列とは、次のような一般式で表される数列です。
- fibonacci(1) = 1
- fibonacci(2) = 1
- fibonacci(k) = fibonacci(k-1) + fibonacci(k-2), where k = 2, 3, …
このプログラムにはバグがあるため、バグがあるため実行時にエラーが発生します。
$ gcc -g -O0 fibonacci1.c$ ./a.out 10Segmentation fault本来ならばここでコアダンプを生成するのですが、最近のOSではコアダンプを生成しません。
そこで、次のようなコマンドでコアダンプを生成できるようにします。
$ ulimit -c unlimitedこの状態で、次のコマンドを実行した際に次のように表示されれば生成されるようになります。
$ ulimit -cunlimitedもう一度プログラムを実行してみましょう。
$ gcc -g -O0 fibonacci1.c$ ./a.out 10Segmentation fault (core dumped)今度はコアダンプを出力した模様です。
$ lsa.out core.6828 fibonacci1.c上のファイル名は一例です。毎回変わるため、コアファイル名は読み替えてください。
バグがあるプログラムを実行すると、次のような症状が見られることが多いです。
+正常終了するが、内部の挙動がおかしい
+無限ループする
+異常終了する
バグの質にもよりますが、デバッガを使用しない場合は下に挙げたものほど厄介なバグであることが多いです。
しかし、デバッガを使用すると、コアダンプを生成してくれる異常終了ほどありがたいバグはありません。
コアダンプには、異常終了した瞬間の全てのデータが格納されています。つまり、コアダンプを使用することによって、
- どの時点で
- どのような状態で 異常終了したのか見当が付きます。
前置きが長くなってしまいましたが、実際にgdbで異常終了した瞬間を追ってみましょう。gdbの起動引数に、実行ファイルのほかにコアダンプを指定して起動します。
$ gdb a.out core.6828...すると、コアダンプを生成した時点の環境を再現して、プログラムが停止されます。
Core was generated by `./a.out 10'.Program terminated with signal 11, Segmentation fault.Reading symbols from /lib/tls/libc.so.6...done.Loaded symbols for /lib/tls/libc.so.6Reading symbols from /lib/ld-linux.so.2...done.Loaded symbols for /lib/ld-linux.so.2#0 0x08048439 in fibonacci (k=-307072) at fibonacci1.c:2727 return fibonacci(k-1) + fibonacci(k-2);(gdb)現在位置を把握するために、backtraceコマンドを実行します。
(gdb) backtrace#0 0x08048439 in fibonacci (k=-307072) at fibonacci1.c:27#1 0x0804843e in fibonacci (k=-307071) at fibonacci1.c:27#2 0x0804843e in fibonacci (k=-307070) at fibonacci1.c:27...(省略)#27 0x0804843e in fibonacci (k=-307045) at fibonacci1.c:27#28 0x0804843e in fibonacci (k=-307044) at fibonacci1.c:27---Type <return> to continue, or q <return> to quit---長すぎて酷いことになりました。
リストを停止させるため、“q”と打ってENTERキーを押します。
---Type <return> to continue, or q <return> to quit---qQuitこの状態を見ると、無限の再帰呼び出しを行っていると推測できます。
さらに、kの値がマイナスになっています。
(gdb) print k$1 = -307072そこで、再帰の終了条件を調べてみると、以下のようになっていました。
if (k == 1) { return 1;}fibonacci数列は k=1 と k=2 の時に特殊な解を持ちますが、ここではk=1の判定しか行っていません。
そこで、プログラムを書き直してk=2の判定を行います。
fibonacci2.c
#include <stdio.h>#include <stdlib.h>
int fibonacci(int);
int main(int argc, char **argv) { int k, result; if (argc != 2) { printf("usage: %s n\n",argv[0]); return 1; } k = atoi(argv[1]); if (k < 1) { printf("illegal argument k=%d\n",k); return 1; } result = fibonacci(k); printf("fibonacci(%d) = %d\n", k, result); return 0;}
int fibonacci(int k) { if (k == 1) { return 1; } else if (k == 2) { return 1; } else { return fibonacci(k-1) + fibonacci(k-2); }}このプログラムを検証してみましょう。
$ gcc -g -O0 fibonacci2.c$ ./a.out 10fibonacci(10) = 55とりあえず大きなバグは潰れたように思われます。
個人的に良く使うと思われるその他のコマンド
Section titled “個人的に良く使うと思われるその他のコマンド”display
Section titled “display”printコマンドを自動的に毎回行ってくれるコマンドです。printをdisplayに変えるだけで文法はほとんど同じです。
$ gcc -g -O0 bubblesort.c$ gdb a.out...
(gdb) break mainBreakpoint 1 at 0x8048358: file bubblesort.c, line 9.(gdb) runStarting program: /***/a.out
Breakpoint 1, main (argc=1, argv=0xbfef0f94) at bubblesort.c:99 int array[4] = {4, 1, 3, 2};(gdb) display array1: array = {0, 13611576, 0, 13611576}(gdb) next10 printarray(&array[0], 4);1: array = {4, 1, 3, 2}(gdb) next4 1 3 211 sort(array, 4);1: array = {4, 1, 3, 2}(gdb) next12 printarray(&array[0], 4);1: array = {1, 2, 3, 4}(gdb) next1 2 3 413 return 0;1: array = {1, 2, 3, 4}(gdb) cContinuing.
Program exited normally.(gdb) quitなお、info displayコマンドで現在のdisplayの状態を表示し、delete displayコマンドでdisplayの設定を削除できます。
finish
Section titled “finish”現在実行している関数が終了するまでプログラムを再開し、関数が終了したらプログラムを一時停止させます。
$ gcc -g -O0 bubblesort.c$ gdb a.out...
(gdb) break sortBreakpoint 1 at 0x8048405: file bubblesort.c, line 28.(gdb) runStarting program: /***/a.out4 1 3 2
Breakpoint 1, sort (array=0xbfe6faf0, length=4) at bubblesort.c:2828 for (i = 0; i < length - 1; i++) {(gdb) finishRun till exit from #0 sort (array=0xbfe6faf0, length=4) at bubblesort.c:280x08048393 in main (argc=1, argv=0xbfe6fb94) at bubblesort.c:1111 sort(array, 4);(gdb) cContinuing.1 2 3 4
Program exited normally.(gdb) quit上の例では sort 関数を終了するまでプログラムを再開し、現在いるsort関数の終了時にプログラムを一時停止します、
return
Section titled “return”この関数のこれ以降の行を実行せずに、関数を抜け出します。
$ gdb a.out...
(gdb) break sortBreakpoint 1 at 0x8048405: file bubblesort.c, line 28.(gdb) runStarting program: /***/a.out4 1 3 2
Breakpoint 1, sort (array=0xbfecbe30, length=4) at bubblesort.c:2828 for (i = 0; i < length - 1; i++) {(gdb) returnMake sort return now? (y or n) y#0 0x08048393 in main (argc=1, argv=0xbfecbed4) at bubblesort.c:1111 sort(array, 4);(gdb) cContinuing.4 1 3 2
Program exited normally.(gdb) quit注目して欲しいのが、sort関数を抜け出したために、最後の出力がソートされずに表示されている点です。先述のfinishコマンドと見比べてみてください。
戻り値がvoidでない関数は return 0 のように引数に戻り値を設定することができます。
今回は以下のようなことを紹介しました。
- 変数の監視
- 呼び出し元のトレース
- コアファイルの活用
- その他のコマンド 次回は、gdbの便利な機能の紹介と、まとめとしてgdbを使ったデバッグの例を紹介します。