🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語 gdb実習ガイド

デバッガ gdb の基本操作を、サンプルプログラムで1セッション通して体験する。

gdbが解決する問題

printf デバッグも有効ですが、次のような場面では gdb の方が圧倒的に速い:

準備(-g オプション)

デバッグ情報付きでコンパイルするのが大前提。
$ gcc -g -O0 main.c -o app # -g で行番号・変数名を埋め込み $ gdb ./app # gdb を起動 (gdb)
なぜ -O0? 最適化を有効にすると、変数がレジスタに消えたり行が消えたりして、ステップ実行や print の結果が直感と合わなくなる。最初は必ず -O0。

サンプルプログラム

// sum.c: 1〜Nまでの総和を計算する(...はずが、バグあり)
#include <stdio.h>

int sum_to(int n) {
    int s = 0;
    for (int i = 1; i < n; i++) {   // バグ: i <= n が正しい
        s += i;
    }
    return s;
}

int main(void) {
    int r = sum_to(10);
    printf("sum = %d\n", r);   // 期待: 55、実際: 45
    return 0;
}

基本セッション: break / run / step / print

$ gcc -g sum.c -o sum $ gdb ./sum (gdb) break sum_to # 関数先頭にブレークポイント Breakpoint 1 at 0x1149: file sum.c, line 5. (gdb) run # 実行 Starting program: /path/sum Breakpoint 1, sum_to (n=10) at sum.c:5 5 int s = 0; (gdb) next # 次の行に進む(関数に入らない) 6 for (int i = 1; i < n; i++) { (gdb) print n # 変数nの値 $1 = 10 (gdb) print i No symbol "i" in current context. # まだ宣言されていない (gdb) next 7 s += i; (gdb) print i $2 = 1 # ループに入ってi=1 (gdb) continue # 次のブレークポイントまで sum = 45 # 期待は55なのにおかしい [Inferior 1 (process ...) exited normally]

条件付きブレークポイント

(gdb) break sum.c:7 if i == 5 # i==5 のときだけ止まる (gdb) run Breakpoint 2, sum_to at sum.c:7 (gdb) print s $3 = 10 # 0+1+2+3+4 = 10 ✓
step vs next: step(s) は関数の中に入る、next(n) は関数を1行として飛ばす。ライブラリ関数に誤って step で潜らないよう注意。

printf スタイル出力(display)

(gdb) display i # 停止ごとに i を自動表示 (gdb) display s (gdb) next ... 1: i = 3 2: s = 3

スタックトレース(bt / frame)

関数が深く呼ばれている途中でバグを見つけたとき、誰がその関数を呼んだかを知りたい。
(gdb) backtrace # bt と略せる #0 sum_to (n=10) at sum.c:7 #1 main () at sum.c:13 (gdb) frame 1 # 呼び出し元(main)のフレームへ #1 main () at sum.c:13 13 int r = sum_to(10); (gdb) print r $4 = 0 # まだ呼び出し中なので初期値 (gdb) frame 0 # 元に戻る
再帰関数のデバッグ: 再帰呼び出し中のどの階層にいるかを bt で一覧できる。frame n で各階層に行き来し、引数の値を確認できる。

ウォッチポイント(変数が書き換えられた瞬間に止まる)

「この変数がいつの間にかおかしな値になっている」というバグを追うのに最強。
(gdb) break main (gdb) run (gdb) watch r # 変数rへの書き込み監視 Hardware watchpoint 2: r (gdb) continue Hardware watchpoint 2: r Old value = 0 New value = 45 main () at sum.c:13 # r に書き込まれた瞬間に停止
スコープ注意: ローカル変数を watch するとその関数を抜けるときに自動解除される。グローバル変数やポインタ経由のメモリ領域には watch *ptrwatch -location を使う。

Segmentation fault のデバッグ

gdb 内でプログラムを実行すると、クラッシュ時にその場で止まり、どの行でクラッシュしたかが分かります。
// crash.c
#include <stdio.h>
#include <string.h>

int main(void) {
    char *p = NULL;
    strcpy(p, "hello");    // NULLポインタ経由で書き込み → crash
    return 0;
}
$ gcc -g crash.c -o crash $ gdb ./crash (gdb) run Program received signal SIGSEGV, Segmentation fault. __strcpy_avx2 () at .. # libc の中 (gdb) bt #0 __strcpy_avx2 () from /lib/libc.so.6 #1 0x... in main () at crash.c:6 # ← ここが自分のコード! (gdb) frame 1 #1 main () at crash.c:6 6 strcpy(p, "hello"); (gdb) print p $1 = 0x0 # NULLだと判明
coreダンプから調査: 実行時にcore ファイルが作られる設定なら gdb ./app core で事後解析できる。ulimit -c unlimited で有効化。

コマンド早見表

コマンド省略意味
break 関数名 / ファイル:行bブレークポイント設定
info breakpointsi b一覧
delete NdN番目を削除
run [引数]rプログラム開始
continuec次のブレークまで続行
nextn次の行(関数に入らない)
steps次の行(関数に入る)
finishfin現在の関数を抜けるまで実行
print 式p値を表示(p *p, p a[5], p arr@10)
display 式disp停止ごとに自動表示
backtracebtスタックトレース
frame NfN番目のフレームへ
watch 式-式の値が変わったら止まる
listlソースを10行表示
set variable x=5set varデバッグ中に変数を変更
quitq終了

TUI モード(ソース同時表示)

$ gdb -tui ./app # 上にソース、下にコマンド行が表示される # Ctrl+X → A でモード切替
VS Code / CLion: どちらも内部的に gdb を使ってくれる。マウスで break を置き、変数ウォッチもGUIで見られるので、コマンドに慣れたら GUI を使うのが実用的。

チャレンジ課題

課題1: sum_to のバグを直す
上のサンプル sum_to(10) が 55 でなく 45 を返す原因を gdb で特定し、修正せよ。
課題2: watchpoint 実習
グローバル変数 int g = 0; を複数の関数から書き換えるプログラムを作り、watch g で「いつ値が変わるか」を順番に観察せよ。
課題3: 再帰のスタック確認
fact(5) を再帰で計算するプログラムで、n==2 の時にブレークして bt で呼び出し階層を確認。各フレームで n の値を frame N + print n で読み取る。
課題4: Segfault の原因特定
配列外アクセスする小さなプログラムを書いて gdb で実行。btprint i でどのインデックスで落ちたか特定せよ。