gcc+gdbによるプログラムのデバッグ 第2回 変数の監視、バックトレース、その他のコマンド

前回までに、デバッガを使用する上での最低限のことを覚えました。

今回は少しレベルを上げて、よりデバッガを使いこなすためのコマンドを紹介します。

ウォッチポイント

ウォッチポイントはブレークポイントに近いものですが、ブレークポイントのように「ある地点に遭遇したら停止」ではなく、「監視している変数を操作したら停止」という流れになります。

ファイル内から該当する変数名を探せばいいと考えるかもしれませんが、C言語ではポインタによる変数の別名を付けることが可能であるため、そう単純にはいきません。

書き込みの監視

あまりよいサンプルが思いつかなかったため、簡単で無意味な例を示します。

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 watchee
Hardware watchpoint 1: watchee

info watchpointsコマンドを使用して、ウォッチポイントの設定を確認します。

(gdb) info watchpoints
Num Type           Disp Enb Address    What
1   hw watchpoint  keep y              watchee

runコマンドによってプログラムを起動させましょう。

(gdb) run
Starting program: /***/a.out
Hardware watchpoint 1: watchee
Hardware watchpoint 1: watchee
Hardware watchpoint 1: watchee

しばらくすると、ウォッチポイントで監視している変数を操作して、プログラムが一時停止させられます。

Old value = 0
New value = 2
0x080483bb in set_counter (p=0x80495c0) at counter.c:18
18        *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) continue
Continuing.

これ以降ではwatcheeに書き込む部分が無いため、ウォッチポイントによる停止は行われません。

local = 1
watchee = 2

Program exited normally.

最後に、gdbを終了します。

(gdb) quit

読み込みの監視

先ほどのwatchコマンドは書き込みのみを監視しますが、rwatchコマンドを使用すると該当変数の読み込みを監視します。

$ gcc -g -O0 counter.c
$ gdb a.out
...

今回はwatchコマンドの変わりにrwatchコマンドを使用します。

(gdb) rwatch watchee
Hardware read watchpoint 1: watchee
(gdb) run
Starting program: /***/a.out
Hardware read watchpoint 1: watchee
Hardware read watchpoint 1: watchee
local = 1
Hardware read watchpoint 1: watchee

Value = 2

rwatchではwatchコマンドでは停止しなかった「watcheeの読み込み」に反応して、プログラムが一時停止します。

0x0804839a in main (argc=1, argv=0xbffc5b14) at counter.c:13
13        printf("watchee = %d\n", watchee);

確認し終えたら、最後まで実行して終了しましょう。

(gdb) continue
Continuing.
watchee = 2

Program exited normally.
(gdb) quit

呼び出し元のトレース

ブレークポイントなどを設定してある関数内でプログラムが停止した場合、その関数がどこから呼ばれたかなどの情報が必要になる場合があります。

gdbには呼び出し元のトレースを行う機能が備わっており、backtraceコマンドで行います。

例にはfactorial.cを使用します。

$ gcc -g -O0 factorial.c
$ gdb a.out
...

factorialの再帰呼び出しが一番深くなる地点が n=1 であるので、そこにブレークポイントを設定します。

(gdb) break factorial if n == 1
Breakpoint 1 at 0x80483ff: file factorial.c, line 21.

factorial(5)としてプログラムを起動すると、すぐにブレークポイントに遭遇します。

(gdb) run 5
Starting program: /***/a.out 5

Breakpoint 1, factorial (n=1) at factorial.c:21
21        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) list
16        return 0;
17      }
18
19      /* 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 = 1

factorialの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:28
28          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:13

factorial関数を呼び出す以前の、main関数まで戻ってみます。

(gdb) frame 5
#5  0x080483d1 in main (argc=2, argv=0xbfef6274) at factorial.c:13
13        n = factorial(m);

ここで、factorial関数を呼び出した際の実引数mの値を調査します。

(gdb) print m
$3 = 5

factorial(5)が呼び出されていることが確認できました。

最後まで実行した後、終了します。

(gdb) c
Continuing.
factorial(5) = 120

Program exited normally.
(gdb) quit

コアファイルの活用

この項は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);
  }
}

このプログラムはフィボナッチ数列を計算するためのものです。
フィボナッチ数列とは、次のような一般式で表される数列です。

このプログラムにはバグがあるため、バグがあるため実行時にエラーが発生します。

$ gcc -g -O0 fibonacci1.c
$ ./a.out 10
Segmentation fault

本来ならばここでコアダンプを生成するのですが、最近のOSではコアダンプを生成しません。
そこで、次のようなコマンドでコアダンプを生成できるようにします。

$ ulimit -c unlimited

この状態で、次のコマンドを実行した際に次のように表示されれば生成されるようになります。

$ ulimit -c
unlimited

もう一度プログラムを実行してみましょう。

$ gcc -g -O0 fibonacci1.c
$ ./a.out 10
Segmentation fault (core dumped)

今度はコアダンプを出力した模様です。

$ ls
a.out core.6828 fibonacci1.c

上のファイル名は一例です。毎回変わるため、コアファイル名は読み替えてください。

バグがあるプログラムを実行すると、次のような症状が見られることが多いです。

  1. 正常終了するが、内部の挙動がおかしい
  2. 無限ループする
  3. 異常終了する

バグの質にもよりますが、デバッガを使用しない場合は下に挙げたものほど厄介なバグであることが多いです。

しかし、デバッガを使用すると、コアダンプを生成してくれる異常終了ほどありがたいバグはありません。

コアダンプには、異常終了した瞬間の全てのデータが格納されています。つまり、コアダンプを使用することによって、

異常終了したのか見当が付きます。

前置きが長くなってしまいましたが、実際に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.6
Reading 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:27
27          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---q
Quit

この状態を見ると、無限の再帰呼び出しを行っていると推測できます。
さらに、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 10
fibonacci(10) = 55

とりあえず大きなバグは潰れたように思われます。

個人的に良く使うと思われるその他のコマンド

display

printコマンドを自動的に毎回行ってくれるコマンドです。printをdisplayに変えるだけで文法はほとんど同じです。

$ gcc -g -O0 bubblesort.c
$ gdb a.out
...

(gdb) break main
Breakpoint 1 at 0x8048358: file bubblesort.c, line 9.
(gdb) run
Starting program: /***/a.out

Breakpoint 1, main (argc=1, argv=0xbfef0f94) at bubblesort.c:9
9         int array[4] = {4, 1, 3, 2};
(gdb) display array
1: array = {0, 13611576, 0, 13611576}
(gdb) next
10        printarray(&array[0], 4);
1: array = {4, 1, 3, 2}
(gdb) next
4 1 3 2
11        sort(array, 4);
1: array = {4, 1, 3, 2}
(gdb) next
12        printarray(&array[0], 4);
1: array = {1, 2, 3, 4}
(gdb) next
1 2 3 4
13        return 0;
1: array = {1, 2, 3, 4}
(gdb) c
Continuing.

Program exited normally.
(gdb) quit

なお、info displayコマンドで現在のdisplayの状態を表示し、delete displayコマンドでdisplayの設定を削除できます。

finish

現在実行している関数が終了するまでプログラムを再開し、関数が終了したらプログラムを一時停止させます。

$ gcc -g -O0 bubblesort.c
$ gdb a.out
...

(gdb) break sort
Breakpoint 1 at 0x8048405: file bubblesort.c, line 28.
(gdb) run
Starting program: /***/a.out
4 1 3 2

Breakpoint 1, sort (array=0xbfe6faf0, length=4) at bubblesort.c:28
28        for (i = 0; i < length - 1; i++) {
(gdb) finish
Run till exit from #0  sort (array=0xbfe6faf0, length=4) at bubblesort.c:28
0x08048393 in main (argc=1, argv=0xbfe6fb94) at bubblesort.c:11
11        sort(array, 4);
(gdb) c
Continuing.
1 2 3 4

Program exited normally.
(gdb) quit

上の例では sort 関数を終了するまでプログラムを再開し、現在いるsort関数の終了時にプログラムを一時停止します、

return

この関数のこれ以降の行を実行せずに、関数を抜け出します。

$ gdb a.out
...

(gdb) break sort
Breakpoint 1 at 0x8048405: file bubblesort.c, line 28.
(gdb) run
Starting program: /***/a.out
4 1 3 2

Breakpoint 1, sort (array=0xbfecbe30, length=4) at bubblesort.c:28
28        for (i = 0; i < length - 1; i++) {
(gdb) return
Make sort return now? (y or n) y
#0  0x08048393 in main (argc=1, argv=0xbfecbed4) at bubblesort.c:11
11        sort(array, 4);
(gdb) c
Continuing.
4 1 3 2

Program exited normally.
(gdb) quit

注目して欲しいのが、sort関数を抜け出したために、最後の出力がソートされずに表示されている点です。先述のfinishコマンドと見比べてみてください。

戻り値がvoidでない関数は return 0 のように引数に戻り値を設定することができます。

今回は以下のようなことを紹介しました。

次回は、gdbの便利な機能の紹介と、まとめとしてgdbを使ったデバッグの例を紹介します。