プログラムをデバッグする際、特定の場所でプログラムの実行を停止することができたら便利。そうすれば、その場所で変数やメモリ等の内容を調べることができるから。ブレークポイントはこれを可能にしてくれる。

<ブレークポイントの設定コマンド>
b line
行番号line にブレークポイントを設定
b func
関数func にブレークポイントを設定。
b file:line
ファイルfile の行番号line にブレークポイントを設定。分割ソースプログラムに有効。

<ウォッチポイントの設定コマンド>
ウォッチポイントは、指定した変数や式の値が変更した時に実行を停止する機能。使いこなせるととても便利。
watch exp
式exp の値が変化した時に停止。(例: watch count==10)
awatch v
変数 v に値が書き込まれた時に停止。

<ブレークポイント、ウォッチポイントの操作コマンド>
info br
ブレークポイントおよびウォッチポイントの一覧表示
del i
info brで表示される一覧の中のi番目のブレーク(ウォッチ)ポイントを解除
clear {func|line}
関数funcまたは行番号lineに設定されたブレーク(ウォッチ)ポイントを解除
enable i
i番目のブレーク(ウォッチ)ポイントを有効にする
diable i
i番目のブレーク(ウォッチ)ポイントを(一時的に)無効にする

gdbとはGNUが提供するデバッガのこと。プログラムからバクを検出する手助けとなるツール。

今回はつぎのC言語ソースプログラム scanf.c をもって、gdbの使い方を説明していく。プログラムの内容はは標準入力から、整数、文字、整数、という3つの入力データをもらい、それらを標準出力に印字するという、ごく単純なもの。

プログラム自身に全くバグがないと思うが、考えていた動きにならず、gdbの世話になるわけだ。

#include
int main(void)
{
    int n1, n2;
    int c;

    scanf("1673837824   -2085769632", &n1, &c, &n2);
    printf("n1=1673837824, c= (83ADAE60), n2=794150656\n", n1, c, c, n2);
    return 0;
}

このプログラムを、Linux (Fedora Core 1) OSの元で、Gnu C コンパイラ: GCC、 (GNU) 3.3.2 20031022 (Red Hat Linux 3.3.2-1) でコンパイルし実行する。

コンパイルの仕方や実行結果は以下の通り。なお、% はコンソールのプロンプトであり、入力される部分ではない。

% cc -o scanf scanf.c
% ./scanf
n1=12, c=A(AD6241), n2=345 <span style="color: #ff0000;">

文字 A の入力に対し、その出力は16進数では、AD6241 になってしまい、考えていた 41 ではないのだ。

では、ソースプログラム scanf.c がgdbでデバッグできるよう、-g オプション で、デバッグ情報を埋め込む形として再コンパイルする。

% cc -o scanf -g scanf.c

gdb を起動して、上のプログラムに対し、次のことを行う。

  • ブレークポイントを設定して、好きな場所で実行を止める。
    <コマンド> break (省略形 b) 関数名
  • 変数の内容を確認する。
    <コマンド> print (省略形 p) 変数名
  • 一旦止めた実行を再開させる。
    <次の1行を実行するコマンド> next (省略形 n)
    <次の1ステップを実行するコマンド> step (省略形 s)
    <次のブレークポイントまで実行するコマンド> continue (省略形 c)
% gdb scanf
(gdb) b main  ← main関数のところでブレークするようにセット
Breakpoint 1 at 0x804838c: file scanf.c, line 8.
(gdb) run  ← ブレークポイントまで実行させる
Starting program: /home/yuki/scanf
Breakpoint 1, main () at scanf.c:8  ← それでソースの8行目で止まった
8           scanf("1673837824   -2085769632", &n1, &c, &n2);
(gdb) p n1  ← 変数 n1 の内容を表示する
$1 = 134513680
(gdb) p/x c  ← 変数 c の内容を16進数で表示
$2 = 0xad6238
(gdb) n  ← つぎの1行を実行
23 A 456  ← scanf関数に対する入力
9           printf("n1=1673837824, c= (83ADAE60), n2=794150656\n", n1, c, c, n2);
(gdb) p n1  ← 変数 n1 の内容をもう一度表示する
$3 = 23
(gdb) p/x c  ← 変数 c の内容を16進数でもう一度表示
$4 = 0xad6241
(gdb) whatis c  ← 変数c の型(タイプ)を調べる
type = int
(gdb) quit    ← gdbの終了

一通りgdbのごく初歩的な使い方を見てきたが、結局のところscanf 関数の動きはこれではまだ解明していない。自分のつくったソースの問題ではなく、scanf ライブラリは違う動きにしているようだ。

それはさておき、変数に関連して以下のコマンドがgdbで利用できる。

printコマンドに /format を付加することで、出力書式を指定することができる。

文字 意味 文字 意味
o 8進表示 d 符号付き10進表示
x 16進表示 u 符号なし10進表示
t 2進表示 c 文字表示
f 浮動小数点表示 a アドレス

コマンド x でメモリの内容を表示することができる。

(gdb) x 0xffff1110  メモリ 0xffff1110番地の内容を表示
0xffff1110:     0x59604989
(gdb)

コマンド set variable で変数の値をセットする。

(gdb) set variable c=0xff
(gdb) p/x c
$6 = 0xff
(gdb)

プログラムにバグのないことを証明するのは、一般的に決定不可能問題として知られている。つまり原理的に、自分自身も含めたあらゆるプログラムからバグを検出することは、不可能というわけだ。

しかしそうは言っても、個々のプログラムに対して、バグを減らし、なくす努力は当然必要。そういう作業に、プログラマ自身が行うデバッグ作業と、他人がプログラムの動作や結果からバグを見つけ出す検出作業に大別される。

プログラマ自身のデバッグ能力はそのひとの経験に大きく左右されるが、大抵のプログラマは自分の能力を信じているので、デバッグ作業にそんなに熱心でないのは確か。勘違いや考慮不足等のことも大いにあるわけで、本人のデバッグにあまり期待しないほうがいいだろう。

外部から、バグを検出する方法はいくつもあるが、もっとも大事なのは、テストする前に、入力データをできるだけ多く用意し、しかも、必ず正解を一緒に用意すること。プログラムの実行結果をみながら、適当に入力データをその場で考えたり、出力をみて、その場で検証したりしてはいけない!

入力データは、とくに境界条件のところで厳しく設定する。ありそうもない入力データも仕様書に反しない限り用意すること。

また、入力に使ったテストデータは必ずすべて保存すること。プログラムのテストはその場その場で適当にやるのではなく、プログラマと合意の上、プログラマが立会いのもと、大々的に行う。プログラマの直したとか、絶対に動くとかの言葉を信用せず、いままで使った入力データすべてを再テストする。新しいバグを直したことで古いバグが再発することは大いにあるから。