前回までで、gdbの基本的な利用方法を紹介しました。
今回は最終回です。gdbの便利な機能の紹介と、総まとめとして簡単なプログラムのデバッグ例を紹介します。
gdbで便利だと感じる細かい機能として、以下のようなものが挙げられます。
gdbを実行中にhelpコマンドを実行すると、コマンドの種類一覧が表示されます。
(gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points data -- Examining data files -- Specifying and examining files internals -- Maintenance commands obscure -- Obscure features running -- Running the program stack -- Examining the stack ...
先ほど表示された種類一覧の中から、stack を help コマンドで実行します。スタックに関するコマンド一覧が表示されます。
(gdb) help stack Examining the stack. ... List of commands: backtrace -- Print backtrace of all stack frames bt -- Print backtrace of all stack frames down -- Select and print stack frame called by this one frame -- Select and print a stack frame return -- Make selected stack frame return to its caller select-frame -- Select a stack frame without printing anything up -- Select and print stack frame that called this one ...
さらに「help コマンド名」とやることにより、コマンドに関するヘルプ情報が表示されます。
(gdb) help backtrace Print backtrace of all stack frames, or innermost COUNT frames. With a negative argument, print outermost -COUNT frames. Use of the 'full' qualifier also prints the values of the local variables.
コマンドや引数を入力している最中にTABキーを押すと、可能であれば文字列を自動補完して、入力している最中の文字列を最後まで勝手に入力してくれます。
これは、Unix系の一部のシェルに慣れた方々にとって非常に親しみやすいものだと思います。
例として、bubblesort.cのsort関数にオートコンプリート機能を使用してブレークポイントを設定します。
$ gcc -g -O0 bubblesort.c $ gdb a.out ...
目的のコマンドは「break sort」ですので、一文字目 b を入力してTABキーを2回押しましょう。
(gdb) b[TAB][TAB] backtrace break bt
bから始まるコマンドの一覧が表示されます。
brから始まるコマンドはbreakだけですので、brまで入力してTABキーを押します。
(gdb) br[TAB]
すると、コマンドが自動補完されて break コマンドになります。
(gdb) break
また、sort関数にブレークポイントを設定したい場合、break sまで入れてTABキーを2回押して一覧を表示させることができます。
(gdb) break s short int signed char sort short unsigned int size_t swap
先ほどと同様にsoまで入力してTABキーを押すと、自動補完されてsort関数になります。
(gdb) break sort Breakpoint 1 at 0x8048405: file bubblesort.c, line 28.
(gdb)と表示されている状態で↑↓キーを押すことによって、過去に使用したコマンドの履歴を使用することができます。複雑なprint文などを、2度目以降は履歴から実行する、などの使用法が考えられます。
また、(gdb)と表示されている状態でそのままENTERキーを押すと、直前に実行したコマンドをもう一度実行してくれます。関数の内部をトレースする際に多用するnextやstepコマンドは、一度入力したらENTERキーを叩くだけで再度実行してくれるので手間が省けます。
ここまでに紹介したコマンドは、短縮して入力してもコマンドとして受け付けてくれます。
短縮のルールは次のようになっています。
| 元のコマンド | 短縮コマンド(最短) | 意味 |
|---|---|---|
| backtrace | bt | 関数呼び出し情報のトレース |
| break | b | ブレークポイントの設定 |
| continue | c | プログラムの再開 |
| command | comm | ブレークポイントヒット時の動作の設定 |
| delete | d | ブレークポイントの削除 |
| delete display | d d | displayの削除 |
| display | disp | 常に表示 |
| finish | fin | 現在の関数の終了まで実行 |
| frame | f | 関数フレームの移動 |
| help | h | ヘルプを表示 |
| info | i | 各種情報のリストを表示 |
| info breakpoints | i b | ブレークポイント情報を表示 |
| info display | i di | displayの状態を表示 |
| info watchpoints | i wat | ウォッチポイント情報を表示 |
| list | l | ソースプログラムを表示 |
| next | n | ステップアウト実行 |
| p | 式を評価して結果を表示 | |
| printf | printf | printのフォーマット表示 |
| ptype | pt | 式の型を表示 (詳細) |
| quit | q | gdbの終了 |
| return | ret | 現在の関数をその場で終了 |
| run | r | プログラムの実行開始 |
| rwatch | rw | 読み出しウォッチポイントの設定 |
| set | set | 変数の値の設定など |
| step | s | ステップイン実行 |
| watch | wa | 書き込みウォッチポイントの設定 |
| whatis | wha | 式の型を表示 (簡易) |
b(break), n(next), s(step), c(continue), p(print), l(list) 辺りを覚えておくと良いでしょう。
いくつかのバグを含むツリーソートのプログラムを用意しました。
treesort1.c
#include <stdio.h>
#include <stdlib.h>
struct node {
int value;
struct node *left;
struct node *right;
};
void treeadd(struct node**, int);
void treewalk(struct node*);
void treefree(struct node*);
int main(int argc, char **argv) {
struct node *rootp;
int i;
int array[15] = {50, 12, 18, 70, 41, 19, 91, 1, 7, 6, 81, 65, 55, 20, 0};
for (i = 0; i < 15; i++) {
treeadd(&rootp, array[i]);
}
treewalk(rootp);
printf("\n");
treefree(rootp);
return 0;
}
void treeadd(struct node **pp, int val) {
/* create new node if *p is null */
if (*pp == (struct node *)0) {
*pp = (struct node *)malloc(sizeof(struct node));
(*pp)->value = val;
}
else if ((*pp)->value > val) {
treeadd(&(*pp)->left, val);
}
else if ((*pp)->value < val) {
treeadd(&(*pp)->right, val);
}
else /* (*pp)->value == val */ {
/* do nothing */
}
}
void treewalk(struct node *p) {
treewalk(p->left);
printf("%d ", p->value);
treewalk(p->right);
}
void treefree(struct node *p) {
treefree(p->left);
treefree(p->right);
free(p);
return;
}
今回はこのプログラムを実際にデバッグする流れを紹介します。
コンパイル後いきなりデバッガに掛けるのは個人的にどうかと思うので、とりあえず実行してみましょう。
$ gcc -g -O0 treesort1.c $ ./a.out Segmentation fault
セグメンテーション違反が発生しました。コアダンプを生成しなかったので、生成させましょう。
$ ulimit -c unlimited $ ulimit -c unlimited $ ./a.out Segmentation fault (core dumped)
コアダンプが生成されました。
$ ls core.* core.7567
先ほど生成されたコアダンプを利用して、プログラムをデバッグしてみましょう。
$ gdb a.out core.7567
...
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
...
#0 0x0804846a in treeadd (pp=0xcfb14c, val=50) at treesort1.c:39
39 else if ((*pp)->value > val) {
とりあえずバックトレース情報を調べます。
(gdb) bt #0 0x0804846a in treeadd (pp=0xcfb14c, val=50) at treesort1.c:39 #1 0x080484aa in treeadd (pp=0xbf526008, val=50) at treesort1.c:44 #2 0x080484aa in treeadd (pp=0xcfb23c, val=50) at treesort1.c:44 #3 0x08048485 in treeadd (pp=0xbfead7ac, val=50) at treesort1.c:40 #4 0x080483f6 in main (argc=1, argv=0xbfead844) at treesort1.c:20
プログラムの最初のほうの、ツリーに要素を追加する部分でエラーが発生しているようです。
プログラムの流れをつかむため、main関数までフレームを移動します。
(gdb) f 4 #4 0x080483f6 in main (argc=1, argv=0xbfead844) at treesort1.c:20 20 treeadd(&rootp, array[i]);
現在位置を確認します。
(gdb) l
15 struct node *rootp;
16 int i;
17 int array[15] = {50, 12, 18, 70, 41, 19, 91, 1, 7, 6, 81, 65, 55, 20, 0};
18
19 for (i = 0; i < 15; i++) {
20 treeadd(&rootp, array[i]);
21 }
22
23 treewalk(rootp);
24 printf("\n");
ループ内でtreeaddを呼び出しているため、何週目のループかを確認します。
(gdb) p i $1 = 0
i=0なので、初めて要素を追加する際にエラーが発生していることが確認できました。
次に、treeaddをmainから呼び出した際のフレームに移動します。
(gdb) f 3 #3 0x08048485 in treeadd (pp=0xbfead7ac, val=50) at treesort1.c:40 40 treeadd(&(*pp)->left, val);
初めて呼び出された際なのでまだツリーが存在しないにも関わらず、(*pp)->leftなどとやっています。
どうやらこの辺りがバグの原因であるようです。
初めて呼び出された際に、本来はどこへ行くべきか調査します。
(gdb) l treeadd, +10
31 void treeadd(struct node **pp, int val) {
32
33 /* create new node if *p is null */
34 if (*pp == (struct node *)0) {
35 *pp = (struct node *)malloc(sizeof(struct node));
36 (*pp)->value = val;
37 }
38
39 else if ((*pp)->value > val) {
40 treeadd(&(*pp)->left, val);
41 }
list treeadd, +10 という記法は、「関数treeaddの先頭から10行分を表示」という意味になります。
上のリストを眺めると、ノードが空のときにはmallocで領域を確保する必要がありそうです。
ではなぜ、初回に呼び出された際にこの部分へ行かないのでしょうか。
(gdb) p *pp $2 = (struct node *) 0xcfb238
ppの指す先が0ではなかったため、この部分の判定を逃れていたようです。*ppの値を調べるため、子の関数の呼び出し元に戻ります。
現在のフレームを確認します。
(gdb) f #3 0x08048485 in treeadd (pp=0xbfead7ac, val=50) at treesort1.c:40 40 treeadd(&(*pp)->left, val);
フレーム番号3だったので、1を足して4番目のフレームへ行きます。
(gdb) f 4 #4 0x080483f6 in main (argc=1, argv=0xbfead844) at treesort1.c:20 20 treeadd(&rootp, array[i]);
rootpが怪しそうです。確認してみましょう。
(gdb) p rootp $3 = (struct node *) 0xcfb238
なぜ、rootpが初期化されていないのでしょうか。rootpの宣言位置を確認してみます。
(gdb) l rootp
10 void treeadd(struct node**, int);
11 void treewalk(struct node*);
12 void treefree(struct node*);
13
14 int main(int argc, char **argv) {
15 struct node *rootp;
16 int i;
17 int array[15] = {50, 12, 18, 70, 41, 19, 91, 1, 7, 6, 81, 65, 55, 20, 0};
18
19 for (i = 0; i < 15; i++) {
rootpは局所変数で、初期化をし忘れていました。これが一つ目のバグだと思われます。
main関数の先頭部分を以下のように書き換えて、デバッグを再開します。
treesort2.c
int main(int argc, char **argv) {
struct node *rootp = (struct node *)0;
int i;
int array[15] = {50, 12, 18, 70, 41, 19, 91, 1, 7, 6, 81, 65, 55, 20, 0};
また、mallocで領域確保時にleftとrigitの値が保証されないため、明示的な初期化も行っておきます。
void treeadd(struct node **pp, int val) {
/* create new node if *p is null */
if (*pp == (struct node *)0) {
*pp = (struct node *)malloc(sizeof(struct node));
(*pp)->value = val;
(*pp)->left = (struct node *)0;
(*pp)->right = (struct node *)0;
}
コンパイルして、gdbから起動してみましょう。
$ gcc -g -O0 treesort2.c $ gdb a.out ...
今回はブレークポイントの設定は行わずに、いきなりrunで構いません。
(gdb) run Starting program: /***/a.out
すると、セグメント違反でプログラムが停止してしまいました。先ほど潰したバグよりは先に進んだようです。
Program received signal SIGSEGV, Segmentation fault. 0x080484dd in treewalk (p=0x0) at treesort2.c:55 55 treewalk(p->left);
とりあえずpの値を確認してみます。
(gdb) p p $1 = (struct node *) 0x0
nodeの値が0であると言うことは、このノードは存在していません。そのようなノードをtreewalkする意味はないので、ノードをチェックするようなコードを挿入します。
これで、2個目のバグが潰れるはずです。
treewalk関数を次のように書き換えて、デバッグを再開します。
treesort3.c
void treewalk(struct node *p) {
/* return if p is empty node.. */
if (p == (struct node *)0) {
return;
}
else {
treewalk(p->left);
printf("%d ", p->value);
treewalk(p->right);
}
}
コンパイルして、gdbから起動してみましょう。
$ gcc -g -O0 treesort3.c $ gdb a.out ...
今回もいきなりrunで構いません。
(gdb) run Starting program: /***/a.out
すると、ソートされた数時の列が表示された直後に、セグメント違反でプログラムが停止してしまいました。先ほど潰したバグよりは進歩しています。
0 1 6 7 12 18 19 20 41 50 55 65 70 81 91 Program received signal SIGSEGV, Segmentation fault. 0x08048524 in treefree (p=0x0) at treesort3.c:69 69 treefree(p->left);
プログラムを確認してみましょう。
(gdb) l
64 treewalk(p->right);
65 }
66 }
67
68 void treefree(struct node *p) {
69 treefree(p->left);
70 treefree(p->right);
71 free(p);
72 return;
73 }
上記のリストを見ると、修正前のtreewalkに非常に似た構造をしています。
同じような方法で修正が可能ではないかと思われます。
これで、3個目のバグが潰れるはずです。
treefree関数を次のように書き換えて、デバッグを再開します。
treesort4.c
void treefree(struct node *p) {
/* return if p is empty node.. */
if (p == (struct node *)0) {
return;
}
else {
treefree(p->left);
treefree(p->right);
free(p);
return;
}
}
コンパイルして、gdbから起動してみましょう。
$ gcc -g -O0 treesort4.c $ gdb a.out ...
今回もいきなりrunで構いません。
(gdb) run Starting program: /***/a.out
以下のように表示されました。
0 1 6 7 12 18 19 20 41 50 55 65 70 81 91 Program exited normally.
どうやら、正常に動いてるようです。gdbは終了しておきます。
(gdb) quit
一応、通常の実行方法でも試してみましょう。
$ ./a.out 0 1 6 7 12 18 19 20 41 50 55 65 70 81 91
特に問題なさそうに見えます。
ここまでで3つのバグを含む簡単なプログラムのバグ取りは終了です。
今までは「Segmentation fault」としか表示されなかったバグを含むプログラムが、比較的容易にデバッグできました。
デバッガは「バグの情報を表示できるソフトウェア」です。みなさんもLinuxなどでプログラムを組む必要が出てきた場合は、デバッガを活用してみて下さい。バグ取りの作業がそれほど苦痛でなくなるかもしれません。