導入
第8章で、malloc・free を自分で対応させ忘れると起きるバグ(メモリリーク・二重解放・ダングリングポインタ)を学びました。こうしたバグは printf を挟んで目で追うだけでは見つけにくいものです。実務のC開発では、バグの種類に応じた専用の道具を使って機械的に見つけ出します。
説明
gdb ― 動きを1行ずつ止めて観察するデバッガ
gdb(GNU Debugger)は、プログラムを実行しながら好きな場所で一時停止し、変数の中身や呼び出しの経路を確認できるツールです。代表的な操作の流れは次のとおりです。
$ gcc -g program.c -o program # -g を付けてデバッグ情報つきでビルドする
$ gdb ./program
(gdb) break main # main関数の先頭にブレークポイントを置く
(gdb) run # 実行開始(ブレークポイントで自動的に止まる)
(gdb) next # 1行だけ実行して次の行へ(関数呼び出しはまたいで進む)
(gdb) step # 1行だけ実行(関数呼び出しがあれば中へ入る)
(gdb) print count # 変数 count の現在の値を表示
(gdb) backtrace # 今どの関数からどの関数が呼ばれてここまで来たかを表示
(gdb) continue # 次のブレークポイントか終了まで実行を再開
「怪しい場所の少し手前で止め、print で変数の中身を覗きながら1行ずつ進める」という使い方が基本です。backtrace は、クラッシュした瞬間に「どの関数がどの関数を呼んでここに来たか」を一覧できるので、原因の関数を絞り込むのに役立ちます。
valgrind ― メモリの使い方をまるごと監視する
valgrind は、プログラムを仮想的な環境の上で実行し、メモリの確保・解放・読み書きをすべて監視するツールです。特に --leak-check=full オプションを付けると、free し忘れたメモリ(リーク)を、確保した場所の行番号つきで教えてくれます。
$ gcc -g program.c -o program
$ valgrind --leak-check=full ./program
...
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 3 allocs, 2 frees, 120 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x...: malloc (...)
==12345== by 0x...: main (program.c:12)
definitely lost は「たしかにリークしている」という意味で、続く行に malloc を呼んだファイル名・行番号が出ます。実行速度はかなり遅くなりますが、その分「本当に漏れているか」を確実に教えてくれます。
AddressSanitizer ― コンパイル時に監視コードを埋め込む
AddressSanitizer(ASan)は、valgrind のように後から実行を監視するのではなく、コンパイル時にメモリアクセスをチェックするコードを埋め込んでおく方式のツールです。-fsanitize=address を付けてコンパイルするだけで使えます。
$ gcc -fsanitize=address -g program.c -o program
$ ./program
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
READ of size 4 at 0x... thread T0
#0 0x... in main program.c:15
...
配列の範囲外へのアクセスや、free したあとのメモリへのアクセス(use-after-free)を、その場で・実際に壊れた瞬間に検出して教えてくれます。valgrind に比べて実行速度がずっと速いので、開発中は常にASan付きでビルドしておく、という運用も広く行われています。
バグの種類と道具の対応
flowchart LR leak["freeし忘れ<br/>(メモリリーク)"] --> valgrind["valgrind --leak-check=full"] dfree["freeを2回呼ぶ<br/>(二重解放)"] --> asan["AddressSanitizer"] dangling["解放後のメモリを使う<br/>(ダングリングポインタ)"] --> asan outofbounds["配列の範囲外アクセス"] --> asan logic["値がおかしい<br/>(ロジックのバグ)"] --> gdb["gdb で1行ずつ観察"]
どれか1つを覚えておけば万能、というわけではありません。「値そのものがおかしい」ときは gdb で流れを追い、「メモリの使い方がおかしい」ときは valgrind や AddressSanitizer に頼る、という使い分けが実務でのデバッグの基本姿勢です。
まとめ
gdbは、実行を好きな場所で止めてprint/backtraceで中身と経路を観察するデバッガ。ロジックのバグ(値がおかしい)を追うのに向く。valgrind --leak-check=fullは、freeし忘れたメモリ(リーク)を確保した場所つきで教えてくれる。実行はかなり遅くなる。- AddressSanitizer(
-fsanitize=address -g)は、コンパイル時に監視コードを埋め込み、境界外アクセスや use-after-free をその場で検出する。valgrindより高速なので開発中に常用しやすい。 - どの道具も、第8章で学んだ
malloc/freeの対応漏れ・二重解放・ダングリングポインタといったバグを見つけるためのものです。バグの種類に応じて道具を使い分けましょう。