gcc+gdbによるプログラムのデバッグ 第3回 gdbの便利な機能、デバッグの例

前回までで、gdbの基本的な利用方法を紹介しました。

今回は最終回です。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キーを叩くだけで再度実行してくれるので手間が省けます。

コマンドの短縮

ここまでに紹介したコマンドは、短縮して入力してもコマンドとして受け付けてくれます。

短縮のルールは次のようになっています。

  1. よく使われるコマンドは1~2文字で表せる
    • break -> b
    • backtrace -> bt
  2. 途中まで入力したコマンドが他のコマンド名と衝突しなければ、コマンドとして扱う
    • 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 ステップアウト実行
print 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は局所変数で、初期化をし忘れていました。これが一つ目のバグだと思われます。

続けてデバッグ(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) 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個目のバグが潰れるはずです。

続けてデバッグ(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個目のバグが潰れるはずです。

続けてデバッグ(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などでプログラムを組む必要が出てきた場合は、デバッガを活用してみて下さい。バグ取りの作業がそれほど苦痛でなくなるかもしれません。