本文へスキップ
BecomeCoder

C言語コース · 第15章 モダンCとデバッグ · レッスン55

デバッグとメモリ検査 ― gdb・valgrind・AddressSanitizer

ローカル実施

導入

第8章で、mallocfree を自分で対応させ忘れると起きるバグメモリリーク・二重解放・ダングリングポインタ)を学びました。こうしたバグは 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 で流れを追い、「メモリの使い方がおかしい」ときは valgrindAddressSanitizer に頼る、という使い分けが実務でのデバッグの基本姿勢です。

まとめ

  • gdb は、実行を好きな場所で止めて printbacktrace で中身と経路を観察するデバッガ。ロジックのバグ(値がおかしい)を追うのに向く。
  • valgrind --leak-check=full は、free し忘れたメモリ(リーク)を確保した場所つきで教えてくれる。実行はかなり遅くなる。
  • AddressSanitizer(-fsanitize=address -g)は、コンパイル時に監視コードを埋め込み、境界外アクセスや use-after-free をその場で検出する。valgrind より高速なので開発中に常用しやすい。
  • どの道具も、第8章で学んだ mallocfree の対応漏れ・二重解放・ダングリングポインタといったバグを見つけるためのものです。バグの種類に応じて道具を使い分けましょう。