[C言語のはじめ方] Part40: gdbでのデバッグ

C言語

デバッグって何? gdbって何? 🤔

プログラムを書いていると、意図した通りに動かないことがあります。思った結果と違う値が出たり、途中でエラーが出て止まってしまったり… こういったプログラムの誤り(バグ)を見つけて修正する作業をデバッグと呼びます。

簡単なバグなら、ソースコードをじっくり読んだり、printf などで変数の値を表示させたりして見つけられるかもしれません。でも、プログラムが複雑になってくると、それだけでは原因を特定するのが難しくなります。

そこで活躍するのがデバッガというツールです。デバッガを使うと、プログラムを一行ずつ実行したり、特定の場所で一時停止させたり、変数の中身を好きなタイミングで確認したりできます。これにより、バグの原因を効率的に突き止めることができるのです。

今回紹介する gdb (GNU Debugger) は、C言語をはじめ、多くのプログラミング言語で利用できる、非常に強力で有名なデバッガです。コマンドライン(黒い画面)で操作するのが基本ですが、慣れればとても頼りになる相棒になりますよ!💪

gdbを使うことで、以下のようなことができるようになります:

  • プログラムを指定した条件で停止させる(ブレークポイント)
  • プログラムを一行ずつ実行する(ステップ実行)
  • プログラムが停止した時点で、変数の中身を確認・変更する
  • プログラムがどのように呼び出されてきたか(コールスタック)を確認する
  • プログラムがクラッシュした原因を調査する

デバッグの準備: コンパイルオプション ⚙️

gdbでプログラムを効果的にデバッグするには、コンパイル時に特別なオプションを付ける必要があります。それは -g オプションです。

-g オプションを付けてコンパイルすると、実行ファイルの中にデバッグ情報が埋め込まれます。この情報には、ソースコードの行番号と機械語の対応、変数名、型情報などが含まれており、gdbがこれを利用してソースコードレベルでのデバッグを可能にします。

また、多くの場合、コンパイラによる最適化はデバッグをしにくくすることがあります。最適化によってコードの実行順序が変わったり、変数が消えてしまったりすることがあるためです。デバッグ中は、最適化レベルを下げる -O0 (オー・ゼロ) オプションも併せて指定するのが一般的です。

例えば、my_program.c というソースファイルをコンパイルする場合、以下のようにします。

gcc -g -O0 my_program.c -o my_program

これで、デバッグ情報が含まれ、最適化が抑制された実行ファイル my_program が生成されます。これでgdbを使う準備が整いました!

Tips: -g オプションを付けなくても gdb で実行ファイルをデバッグすることは可能ですが、ソースコードレベルでの情報(変数名や行番号など)が得られないため、デバッグの難易度が上がります。基本的には -g オプションを付けてコンパイルしましょう。

gdbの起動と終了 🚀

gdbを起動するには、ターミナルで gdb コマンドに続けて、デバッグしたい実行ファイル名を指定します。

gdb ./my_program

起動すると、gdbのバージョン情報などが表示され、(gdb) というプロンプトが表示されます。ここからgdbのコマンドを入力していきます。

gdbを終了するには、quit (または省略形の q) コマンドを入力するか、Ctrl+d を押します。

(gdb) quit

基本的なgdbコマンド集 🛠️

gdbにはたくさんのコマンドがありますが、まずは基本的なものを覚えましょう。多くのコマンドは省略形(例えば breakb)が使えます。

コマンド (省略形)機能
run (r)プログラムの実行を開始する。引数も指定可能。(gdb) run
(gdb) r argument1 argument2
break (b)ブレークポイント(プログラムを一時停止させる場所)を設定する。(gdb) b main (main関数で停止)
(gdb) b 15 (現在のファイルの15行目で停止)
(gdb) b my_program.c:20 (my_program.cの20行目で停止)
info break (i b)設定されているブレークポイントの一覧を表示する。(gdb) info break
delete (d)ブレークポイントを削除する。番号は info break で確認。(gdb) delete 1 (番号1のブレークポイントを削除)
(gdb) d (すべてのブレークポイントを削除、確認あり)
next (n)現在停止している行を実行し、次の行に進む(ステップオーバー)。関数呼び出しがあっても関数の中には入らない。(gdb) next
step (s)現在停止している行を実行し、次の行に進む(ステップイン)。関数呼び出しがあれば、その関数の中に入る。(gdb) step
continue (c)プログラムの実行を再開する。次のブレークポイントかプログラム終了まで実行される。(gdb) continue
print (p)変数や式の値を表示する。(gdb) print count (変数countの値を表示)
(gdb) p i * 2 (式 i * 2 の値を表示)
(gdb) p/x status (変数statusを16進数で表示)
(gdb) p array@5 (配列arrayの先頭から5要素を表示)
list (l)ソースコードを表示する。引数なしだと現在位置周辺、行番号や関数名を指定可能。(gdb) list
(gdb) l 10,20 (10行目から20行目を表示)
(gdb) l main (main関数周辺を表示)
backtrace (bt)現在の関数呼び出しの履歴(コールスタック)を表示する。どこから呼び出されてきたかが分かる。(gdb) backtrace
watchウォッチポイントを設定する。指定した変数の値が変更されたときにプログラムを停止させる。(gdb) watch counter (変数counterの値が変わったら停止)
info locals現在の関数スコープ内のローカル変数を表示する。(gdb) info locals
helpgdbのヘルプを表示する。コマンド名を指定するとそのコマンドの詳細を表示。(gdb) help
(gdb) help print
quit (q)gdbを終了する。(gdb) quit
注意: コマンドによっては、プログラムが実行中(または一時停止中)でないと意味がないものがあります (例: next, step, print など)。

実践!gdbでデバッグしてみよう🐞

簡単な例でgdbを使ったデバッグの流れを見てみましょう。以下のコードは、1から5までの合計を計算するつもりが、なぜか結果がおかしくなるプログラムです。

buggy_sum.c:

#include <stdio.h>

int main() {
    int sum = 0;
    int i;

    // 1から5までの合計を計算するはず...?
    for (i = 1; i <= 5; ++i) {
        sum = i; // おっと!ここでバグが! += ではなく = になっている
    }

    printf("Sum = %d\n", sum); // 期待する値は 1+2+3+4+5 = 15 だが...

    return 0;
}

1. コンパイル

まず、デバッグ情報付きでコンパイルします。

gcc -g -O0 buggy_sum.c -o buggy_sum

2. gdb起動

gdb ./buggy_sum

3. ブレークポイント設定

ループの中で何が起こっているか知りたいので、for ループの中の行 (sum = i; の行、ここでは10行目とします) にブレークポイントを設定します。

(gdb) break 10
Breakpoint 1 at 0x1149: file buggy_sum.c, line 10.

または、main 関数の開始地点に設定しても良いでしょう。

(gdb) break main
Breakpoint 1 at 0x1135: file buggy_sum.c, line 4.

4. プログラム実行

run コマンドでプログラムを実行します。

(gdb) run
Starting program: /path/to/buggy_sum

Breakpoint 1, main () at buggy_sum.c:10
10	        sum = i; // おっと!ここでバグが! += ではなく = になっている

ブレークポイントを設定した10行目でプログラムが停止しました。

5. 変数確認とステップ実行

この時点での変数 isum の値を確認してみましょう。

(gdb) print i
$1 = 1
(gdb) print sum
$2 = 0

ループの初回なので、i は 1、sum は 0 ですね。次に next コマンドで1行進めます。

(gdb) next
10	        sum = i; // おっと!ここでバグが! += ではなく = になっている

ループの次の繰り返しで再び10行目で停止しました。再度、変数を確認します。

(gdb) print i
$3 = 2
(gdb) print sum
$4 = 1

i は 2 になりましたが、sum は 0 + 2 = 2 ではなく、1 になっています。ここで sum = i の代入が行われたため、前の値が上書きされてしまったことがわかります。

6. デバッグ継続

さらに continue コマンドで実行を再開し、何度か停止させて変数の値の変化を追うことで、sum += i とすべきところが sum = i になっているバグを発見できます。

(gdb) continue
Continuing.

Breakpoint 1, main () at buggy_sum.c:10
10	        sum = i; // おっと!ここでバグが! += ではなく = になっている
(gdb) p i
$5 = 3
(gdb) p sum
$6 = 2
(gdb) c
Continuing.

Breakpoint 1, main () at buggy_sum.c:10
10	        sum = i; // おっと!ここでバグが! += ではなく = になっている
(gdb) p i
$7 = 4
(gdb) p sum
$8 = 3
(gdb) c
Continuing.

Breakpoint 1, main () at buggy_sum.c:10
10	        sum = i; // おっと!ここでバグが! += ではなく = になっている
(gdb) p i
$9 = 5
(gdb) p sum
$10 = 4
(gdb) c
Continuing.
Sum = 5
[Inferior 1 (process 12345) exited normally]

最終的に sum が 5 と表示されてプログラムが終了しました。これでバグの原因が特定できましたね!あとはソースコードを修正して再コンパイルすればOKです。

7. gdb終了

(gdb) quit

まとめ ✨

今回は、C言語のデバッグに不可欠なツールである gdb の基本的な使い方を紹介しました。

  • -g オプションをつけてコンパイルする。
  • gdb <実行ファイル名> で起動する。
  • break で止めたい場所を指定する。
  • run で実行を開始する。
  • nextstep で一行ずつ実行する。
  • print で変数の中身を確認する。
  • continue で実行を再開する。
  • quit で終了する。

最初はコマンドが多くて難しく感じるかもしれませんが、基本的なコマンドだけでもデバッグ効率は格段に向上します。特に、複雑な条件分岐やループ、ポインタ操作などが絡むバグの原因究明には、デバッガが非常に役立ちます。

gdbには、ここで紹介した以外にもたくさんの便利な機能があります(条件付きブレークポイント、ウォッチポイント、メモリ内容の表示、マルチスレッドデバッグなど)。ぜひ、少しずつ使いこなせるようになって、快適なC言語プログラミングライフを送ってください! 😉

参考情報 📚

コメント

タイトルとURLをコピーしました