gcc+gdbによるプログラムのデバッグ 第3回 gdbの便利な機能、デバッグの例
This content is not available in your language yet.
前回までで、gdbの基本的な利用方法を紹介しました。
- 第1回
- ステップ実行
- 変数の表示、変更
- ブレークポイント
- 第2回
- 変数の監視
- 呼び出し元のトレース
- コアファイルの活用
- その他のコマンド 今回は最終回です。gdbの便利な機能の紹介と、総まとめとして簡単なプログラムのデバッグ例を紹介します。
gdbの便利な機能
Section titled “gdbの便利な機能”gdbで便利だと感じる細かい機能として、以下のようなものが挙げられます。
- ヘルプ
- オートコンプリート
- コマンド履歴
- 短縮コマンド
gdbを実行中にhelpコマンドを実行すると、コマンドの種類一覧が表示されます。
(gdb) helpList of classes of commands:
aliases -- Aliases of other commandsbreakpoints -- Making program stop at certain pointsdata -- Examining datafiles -- Specifying and examining filesinternals -- Maintenance commandsobscure -- Obscure featuresrunning -- Running the programstack -- Examining the stack...先ほど表示された種類一覧の中から、stack を help コマンドで実行します。スタックに関するコマンド一覧が表示されます。
(gdb) help stackExamining the stack....List of commands:
backtrace -- Print backtrace of all stack framesbt -- Print backtrace of all stack framesdown -- Select and print stack frame called by this oneframe -- Select and print a stack framereturn -- Make selected stack frame return to its callerselect-frame -- Select a stack frame without printing anythingup -- Select and print stack frame that called this one...さらに「help コマンド名」とやることにより、コマンドに関するヘルプ情報が表示されます。
(gdb) help backtracePrint 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.オートコンプリート
Section titled “オートコンプリート”コマンドや引数を入力している最中にTABキーを押すと、可能であれば文字列を自動補完して、入力している最中の文字列を最後まで勝手に入力してくれます。
これは、Unix系の一部のシェルに慣れた方々にとって非常に親しみやすいものだと思います。
例として、bubblesort.cのsort関数にオートコンプリート機能を使用してブレークポイントを設定します。
$ gcc -g -O0 bubblesort.c$ gdb a.out...目的のコマンドは「break sort」ですので、一文字目 b を入力してTABキーを2回押しましょう。
(gdb) b[TAB][TAB]backtrace break btbから始まるコマンドの一覧が表示されます。
brから始まるコマンドはbreakだけですので、brまで入力してTABキーを押します。
(gdb) br[TAB]すると、コマンドが自動補完されて break コマンドになります。
(gdb) breakまた、sort関数にブレークポイントを設定したい場合、break sまで入れてTABキーを2回押して一覧を表示させることができます。
(gdb) break sshort int signed char sortshort unsigned int size_t swap先ほどと同様にsoまで入力してTABキーを押すと、自動補完されてsort関数になります。
(gdb) break sortBreakpoint 1 at 0x8048405: file bubblesort.c, line 28.コマンド履歴の実行
Section titled “コマンド履歴の実行”(gdb)と表示されている状態で↑↓キーを押すことによって、過去に使用したコマンドの履歴を使用することができます。複雑なprint文などを、2度目以降は履歴から実行する、などの使用法が考えられます。
また、(gdb)と表示されている状態でそのままENTERキーを押すと、直前に実行したコマンドをもう一度実行してくれます。関数の内部をトレースする際に多用するnextやstepコマンドは、一度入力したらENTERキーを叩くだけで再度実行してくれるので手間が省けます。
コマンドの短縮
Section titled “コマンドの短縮”ここまでに紹介したコマンドは、短縮して入力してもコマンドとして受け付けてくれます。
短縮のルールは次のようになっています。
+よく使われるコマンドは1~2文字で表せる
- break -> b
- backtrace -> bt +途中まで入力したコマンドが他のコマンド名と衝突しなければ、コマンドとして扱う
- disp -> display
- ret -> return
| 元のコマンド | 短縮コマンド(最短) | 意味 |
|---|---|---|
| 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) 辺りを覚えておくと良いでしょう。
デバッグの例
Section titled “デバッグの例”いくつかのバグを含むツリーソートのプログラムを用意しました。
#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.outSegmentation faultセグメンテーション違反が発生しました。コアダンプを生成しなかったので、生成させましょう。
$ ulimit -c unlimited$ ulimit -cunlimited$ ./a.outSegmentation fault (core dumped)コアダンプが生成されました。
$ ls core.*core.7567コアダンプを使用したデバッグ
Section titled “コアダンプを使用したデバッグ”先ほど生成されたコアダンプを利用して、プログラムをデバッグしてみましょう。
$ 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:3939 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:2020 treeadd(&rootp, array[i]);現在位置を確認します。
(gdb) l15 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};1819 for (i = 0; i < 15; i++) {20 treeadd(&rootp, array[i]);21 }2223 treewalk(rootp);24 printf("\n");ループ内でtreeaddを呼び出しているため、何週目のループかを確認します。
(gdb) p i$1 = 0i=0なので、初めて要素を追加する際にエラーが発生していることが確認できました。
次に、treeaddをmainから呼び出した際のフレームに移動します。
(gdb) f 3#3 0x08048485 in treeadd (pp=0xbfead7ac, val=50) at treesort1.c:4040 treeadd(&(*pp)->left, val);初めて呼び出された際なのでまだツリーが存在しないにも関わらず、(*pp)->leftなどとやっています。
どうやらこの辺りがバグの原因であるようです。
初めて呼び出された際に、本来はどこへ行くべきか調査します。
(gdb) l treeadd, +1031 void treeadd(struct node **pp, int val) {3233 /* 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 }3839 else if ((*pp)->value > val) {40 treeadd(&(*pp)->left, val);41 }list treeadd, +10 という記法は、「関数treeaddの先頭から10行分を表示」という意味になります。
上のリストを眺めると、ノードが空のときにはmallocで領域を確保する必要がありそうです。
ではなぜ、初回に呼び出された際にこの部分へ行かないのでしょうか。
(gdb) p *pp$2 = (struct node *) 0xcfb238ppの指す先が0ではなかったため、この部分の判定を逃れていたようです。*ppの値を調べるため、子の関数の呼び出し元に戻ります。
現在のフレームを確認します。
(gdb) f#3 0x08048485 in treeadd (pp=0xbfead7ac, val=50) at treesort1.c:4040 treeadd(&(*pp)->left, val);フレーム番号3だったので、1を足して4番目のフレームへ行きます。
(gdb) f 4#4 0x080483f6 in main (argc=1, argv=0xbfead844) at treesort1.c:2020 treeadd(&rootp, array[i]);rootpが怪しそうです。確認してみましょう。
(gdb) p rootp$3 = (struct node *) 0xcfb238なぜ、rootpが初期化されていないのでしょうか。rootpの宣言位置を確認してみます。
(gdb) l rootp10 void treeadd(struct node**, int);11 void treewalk(struct node*);12 void treefree(struct node*);1314 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};1819 for (i = 0; i < 15; i++) {rootpは局所変数で、初期化をし忘れていました。これが一つ目のバグだと思われます。
続けてデバッグ(1)
Section titled “続けてデバッグ(1)”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) runStarting program: /***/a.outすると、セグメント違反でプログラムが停止してしまいました。先ほど潰したバグよりは先に進んだようです。
Program received signal SIGSEGV, Segmentation fault.0x080484dd in treewalk (p=0x0) at treesort2.c:5555 treewalk(p->left);とりあえずpの値を確認してみます。
(gdb) p p$1 = (struct node *) 0x0nodeの値が0であると言うことは、このノードは存在していません。そのようなノードをtreewalkする意味はないので、ノードをチェックするようなコードを挿入します。
これで、2個目のバグが潰れるはずです。
続けてデバッグ(2)
Section titled “続けてデバッグ(2)”treewalk関数を次のように書き換えて、デバッグを再開します。
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) runStarting 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:6969 treefree(p->left);プログラムを確認してみましょう。
(gdb) l64 treewalk(p->right);65 }66 }6768 void treefree(struct node *p) {69 treefree(p->left);70 treefree(p->right);71 free(p);72 return;73 }上記のリストを見ると、修正前のtreewalkに非常に似た構造をしています。
同じような方法で修正が可能ではないかと思われます。
これで、3個目のバグが潰れるはずです。
続けてデバッグ(3)
Section titled “続けてデバッグ(3)”treefree関数を次のように書き換えて、デバッグを再開します。
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) runStarting 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.out0 1 6 7 12 18 19 20 41 50 55 65 70 81 91特に問題なさそうに見えます。
ここまでで3つのバグを含む簡単なプログラムのバグ取りは終了です。
今までは「Segmentation fault」としか表示されなかったバグを含むプログラムが、比較的容易にデバッグできました。
デバッガは「バグの情報を表示できるソフトウェア」です。みなさんもLinuxなどでプログラムを組む必要が出てきた場合は、デバッガを活用してみて下さい。バグ取りの作業がそれほど苦痛でなくなるかもしれません。