デバッグ

コンパイルをするときに指摘されるエラーは、主に 文法的エラーであり、これは文法に合うように書き換えることで 比較的すぐに修正することができる。

いっぽう、コンピュータに与えるプログラムに含まれる論理的誤りのことを バグ という。

俗な言い方で、プログラムにバグがある状態 のことを 「バグっている」という。ゲーム少年が、ゲームをやっているときに 画面が壊れたりすると「あ、バグった」のように表現することがあるが、 そもそも元々バグのあるプログラムは常に「バグっている」ので この表現はおかしい。ということをゲーム少年に指摘したところで どうにもならない。

プログラムにひそんだバグを取り除く作業のことをデバッグ とい う。デバッグするためには、プログラムが動くときのことを頭の中で追いかける 必要があるので、文法エラーを取り除くのよりはるかに時間がかかる。プログラ ムを作る過程では、最初に書き進めて行く時間よりも後から間違いを直す(デバッ グする)時間の方が長いと言っても良い。熟練するにつれ、デバッグにかかる時 間が短くなり、同時に最初からバグのないプログラムが作れるようになる。

授業で作るプログラムも徐々に複雑になっていくので、プログラミングに掛 ける時間を節約する意味で、

を覚えておこう。

エラーを起こしにくい書き方

開けたらすぐ閉める!

関数本体や、制御構文のブロックを括る中括弧 {} は 閉じ忘れでエラーになることが最初のうちは多い。閉じ括弧を 後で入れるから忘れるのである。開き括弧を打ったらすぐ閉じ括弧も 書くようにすると忘れない。

if (y%3 == 3) {
    ←閉じ括弧を書いてから中味を書き始める
}

ファイルを開いて閉じる fopenとfcloseも先に fclose()を書いてしまうようにすると良い。

FILE *fp;
fp = fopen("important.data", "r");
 
fclose(fp);
変数名は分かりやすく

大事な意味を持つ変数を、xとかx2 無味乾燥な名前にせず、maxninzu, goukei といった 値の意味が想像しやすい名前にする。

また、変数名のスペルミスでエラーになるのは悲しいので、 積極的に補完を利用する。Emacsであれば単語を2〜3文字打った状態で M-/ を押すとバッファ内に同じ単語があったときに 単語の残り部分を補ってくれる。

大事なところにコメントをつける

制御構造や条件分岐にはかならず意味があるはずである。 意味を表すようなコメントを書いておく。

無意味なコメントの例

int point[100];			/* point配列を100個用意 */
for (i=0; i<maxninzu; i++) {    /* 0からmaxninzu-1 までくり返す */
  p = point[i];			/* p に point[i] を代入する */
}

意味のあるコメントの例

int point[100];			/* 得点を記憶する変数(最大100人分) */
for (i=0; i<maxninzu; i++) {    /* 生徒全員に対してくり返す */
  p = point[i];			/* i 番目の学生の得点 */
}

Emacsであれば、M-;(メタセミコロン)で簡単に /* */ が入れられる。

デバッグのしかた

printfデバッグ

プログラミングの初歩の段階では、変数の値が未定義だったり、 変数の値を変えずにループを作って暴走させてしまったりと、 変数の値が予期せぬ状態になっていることが原因のバグが多い。 これを確認するために、現在の変数の値を要所要所で出力する printf をさし込むと良い。

/* 数当てゲーム */
int answer;
int times=0;
answer = random();
printf("debug: 答は%d\n", answer);
printf("さあいくつでしょう!: ");

このように、printf を挟んで、そのときの変数状態を 表示してデバッグする方法を、printfデバッグ といい、初歩的ではあるが複雑なプログラミングのときにも 有効な方法である。

gdbによるデバッグ

プログラムのソースとの対応を取りながら、プログラムを 少しずつ動かしたり止めたりしつつデバッグを支援するツールのことを ソースレベルデバッガという。

101/102教室環境では、GDB(GNU Debugger)というソースレベルデバッガ が利用できる。コマンド名は gdb である。GDBを使うためには gccでコンパイルするときに -g オプションをつける。

% gcc -g -o aisatsu aisatsu.c

これで、デバッガに必要な情報が組み込まれた実行プログラム (aisatsu)が生成される。

gdbの操作(単純版)

取り敢えず「これだけ覚えておけば何とか使える」レベルでの、 gdbを用いたデバッグの流れは以下のようになる。デバッグしたいプログラム の名前を foo.c とする。

  1. デバッグオプションつきでコンパイル

    gcc-g オプションをつけてコンパイ ルする。

    % gcc -g -o foo foo.c
    
  2. gdbの起動

    gdb の後に実行プログラムを指定する。

    % gdb foo
    (gdb) 
    

    すると、メッセージが出た後で (gdb) のように gdbのプロンプトに変わる。ここにはgdbのコマンドを打って行く。 gdbのコマンドには、run, step, print, continue, ... など 様々なものがある。良く使うものは省略形が用意されている(たとえば、 step コマンドは s だけで良い)。 (gdb)プロンプトで、リターンキーだけを押すと直前に打っ たgdbコマンドがもう一度くり返される。

  3. gdbの終了

    quitコマンドで終了する(省略形 q)。 C-d をタイプしても良い。

  4. プログラムリストを見て行番号を調べる

    list コマンドを打つとプログラムが10行ずつ 表示されるので「ここでプログラムを止めたい」と思う行の 行番号を調べる。

    (gdb) list
    

    listコマンドの省略形は l (エル)。 list 3 のように表示し始める行番号も指定できる。

  5. ブレークポイントを設定する

    break コマンドでプログラムの動作を 止めたい行番号を指定する。ブレークポイントを設定しないで実行すると 一気に終わりまでいって終了してしまうので普通はどこかで実行を止めて そこから1行ずつ実行(ステップ実行)してデバッグする。 たとえば、7行目で止めたかったら 以下のようにする。break コマンドは b と省略しても良い。

    (gdb) break 7
      (または)
    (gdb) b 7
    

    ブレークポイントには、関数名を指定しても良い。main 関数の実行をしょっぱなから止めたいときは

    (gdb) break main
    

    とする。

  6. プログラムを走らせる

    runコマンドでデバッグ実行する。 run コマンドは r と省略できる。

    (gdb) run
      (または)
    (gdb) r
    

    もし、プログラムの起動に引数を与えたいときはrun コマンドに引数を与える。たとえば、直接コマンドを起動するとき

    % ./foo < data.txt
    

    とするのであれば、gdbのなかでは、

    (gdb) run < data.txt
    

    のようにする。

  7. b で設定したブレークポイントの行で プログラムが止まるのでそのあと「ステップ実行」、 「変数の値表示」などをして調査する
    (gdb) s (またはstep)
    (gdb) p 変数 (またはprint)
    
  8. 変数の状態が分かる! 直すところが分かる!
  9. gdbを終了する

    quitコマンド(省略形 q)でgdbを終了する。

    (gdb) q
    The program is running.  Exit anyway? (y or n) y
    

gdbの操作(ちょっと詳しい版)

Emacsのなかでgdbを動かすともっと分かりやすくなる(が、その分 覚えなければいけない操作も多い)。

まずプログラムをデバッガで動かすときに、ここで止めたいというところの 行番号を覚えておく。Emacsではモードラインに行番号が出ている

Line number in Modeline

  1. ファイルを保存してktermでコンパイルする(忘れがち)
    % gcc -g -o foo foo.c
    
  2. gdbモードの起動

    Emacsで、デバッグしたいCプログラムを開いている状態で M-x gdb として、gdb を起動する。

    Run gdb (like this): gdb 
    

    とgdbの起動方法を聞いて来るので、プログラム名を指定する。

    Run gdb (like this): gdb foo
    

    Emacsの中でgdbが起動し、gdbバッファに切り替わる。 *gdb-foo* というバッファになるのでこのバッファ名も覚 えておく。元のプログラムも表示させたい場合は C-x 4 b [Ret] と押すとウィンドウが2分割され元のソースが 選択される。C-x o で上下のウィンドウを行き来する(マウス で選択しても良い)。

  3. ブレークポイントの設定(単純版と同じ)

    覚えておいた行番号、あるいは関数名にブレークポイントを設定する。

    (gdb) b 7
      (または)
    (gdb) b main
    
  4. プログラムを動かす(単純版と同じ)
    (gdb) run
    

    隣のウィンドウにCのソースが現れて、実行中の行のところに △マークが表示される。

  5. ステップ実行・変数表示する

    単純版の説明と同様 s, p コマンドで それぞれステップ実行、変数表示する。

gdbモードでは、ソースプログラムにポイントを合わせて操作することもできる。 以下はrunしたあとで、ふたたびCのソースが表示されている ウィンドウに戻った後の操作である。

gdbでデバッグしてみよう

1から0.1刻みでカウントダウンし、0になったら終了する プログラムを作ってみよう。ちょ簡単。

プログラムは以下のように作る。

countdown.c

#include <stdio.h>

int main()
{
  float count=1.0;
  while (count != 0) {
    printf("%f です!\n", count);
    count -= 0.1;
  }
  puts("おしまい");
}

実際に実行してみよう。

このプログラムは暴走する! 暴走プログラムを止めるときは C-c をタイプして止める。

あとでgdbでデバッグするので、コンパイル時に -g オプショ ンをつけておく。

% gcc -g -o countdown countdown.c
% ./countdown
1.000000 です!
0.900000 です!
0.800000 です!
0.700000 です!
0.600000 です!
0.500000 です!
0.400000 です!
0.300000 です!
0.200000 です!
0.100000 です!
-0.000000 です!
-0.100000 です!
-0.200000 です!
-0.300000 です!
-0.400000 です!
-0.500000 です!
   :
   :
   :
(以下延々止まらない)

慌てず騒がず C-cで止めよう。 プログラムが止まらないのはなぜだろう。

while (count != 0) で止まらないということは、 count == 0 にならないということである。ならば、 引き算(count -= 1) がちゃんとできていないのではないか? と、代替の怪しいところを予想して、 gdbで実行してみよう(コンパイル時に -g をつけ忘れない)。

% gdb countdown
(gdb) list
1  #include 
2  
3  int main()
4  {
5    float count=1.0;
6    while (count != 0) {
7      printf("%f です!\n", count);
8      count -= 0.1;     ← この行で止めたい
9    }
10   puts("おしまい");

8行目にブレークポイントを設定する。

(gdb) b 8
Breakpoint 1 at 0x106bc: file countdown.c, line 8.

実行する。

(gdb) run
Starting program: /home/irhome/c101/ta01002/C-lang/countdown 

Breakpoint 1, main () at countdown.c:8
8           count -= 0.1;

count変数の値を確認してみる。

(gdb) p count    変数名は [TAB] キーで補完できる
$1 = 0.899999976

0.9であるはずの値が、0.899999976になっている。ステップ実行 (s)を繰り返してcount変数から 0.1を引いたときの値を表示すると予想外の値が表示される。

(gdb) s
9         }
(gdb) [RET]のみ
6         while (count != 0) {
(gdb) [RET]のみ
7      printf("%f です!\n", count);
(gdb) [RET]のみ
0.900000 です!
(gdb) [RET]のみ
8      count -= 0.1;
(gdb) p count
$2 = 0.899999976

0.8のときもやっぱりおかしい。継続実行(continue; 省略形c) をcountが0になるはずの10回目まで繰り返してみよう。

(gdb) c
Continuing.
0.800000 です!

Breakpoint 1, main () at countdown.c:8
8           count -= 0.1;
(gdb) [RET]のみ
0.700000 です!

Breakpoint 1, main () at countdown.c:8
8           count -= 0.1;

   :
   : (以下リターンのみを「-0.000 です!」まで繰り返す)
   :
-0.000000 です!

Breakpoint 1, main () at countdown.c:8
8           count -= 0.1;
(gdb) p count
$3 = -7.30156913e-08

この値は -7.30156913×10-8 という意味で、 小数点以下0が7個続いた後に7301...とくる小さな負の値である。 コンピュータの内部で小数を表す浮動小数点数では2進数を基準にするのだが 2進数で10進数の0.1を表すと

0.000110011001100110011001100....
(2進数)

という循環小数になり、小数では正確に表せない。したがって、10進数の 0.1を足したり引いたりという計算はコンピュータはとても苦手とする のである。

もし0.1刻みに数を進めるループを作りたいときは、1〜10で繰り返す ループを作り、ループの中でループ変数を10で割って利用する。

今回のプログラム countdown.c を直すには、 while の終了条件に不等号を利用する。

#include <stdio.h>

int main()
{
  float count=1.0;
  while (count >= 0) {
    printf("%f です!\n", count);
    count -= 0.1;
  }
  puts("おしまい");
}

こうすることで、すくなくとも負の数になったら絶対に止まる ようになる。一般的に、数値を変動させながら回すwhileループ の条件には、== や != よりも 不等号を利用した方が 安全な場合が多い。

gdbのその他のコマンド

知らなくても何とかなるが、覚えるとより効率的にデバッグできる。 以下は、gdbのプロンプトで打つコマンドである。gdbのプロンプトでは gdbコマンドや変数名を打つときに [Tab] キーによる 補完が効く。

gdbコマンド
()内は省略形
意味
info breakpoints
(i b)
ブレークポイント一覧表示
delete 番号
(d 番号)
指定した番号のブレークポイントを削除する
break 場所 if 条件式 指定した場所(行番号や関数名)で「条件式」が成り立つ場合のみ 停止する条件つきブレークポイントを設定する
tbreak 場所 breakと同じだが一度だけ停止する
set 変数=値変数の値を設定する
call 関数指定した関数を直接呼ぶ
where今どこで止まっているかを表示
step [数]
(s [数])
ステップ実行する。関数呼出しがあればその関数の中に入ってステップ実 行する; 「数」を指定するとその数だけ連続してステップ実行。「数」 を省略すると1行実行
next [数]
(n [数])
ステップ実行と同じだが関数呼出しは飛ばす
finish
(f)
関数の最後まで継続実行する
print 式
(p 式)
「式」の値を表示(変数の値を調べる)
display 式
(disp 式)
「式」の値を自動的に表示する。disp x*2 など 数式が書ける
info display
(i disp)
登録されているdisplay一覧表示
delete display 番号
(d disp 番号)
指定した番号のdisplayを削除する

目次