このページの目次

Valgrind と Sanitizers の実践

カバー範囲: Valgrind (memcheck/helgrind/cachegrind/massif) → ASAN/UBSAN/TSAN → 比較と選択 → 実際のデバッグ事例 対象: Linux ユーザー空間, C/C++

Valgrind: ダイナミックバイナリトランスレーション

Valgrind は本質的に JIT コンパイラです。プログラムの機械語を中間表現(VEX IR)にトランスレーションし、IR 上でインスツルメンテーションによる解析を行った後、再び機械語に戻して実行します。対象プログラムを再コンパイルする必要はありませんが、パフォーマンスのオーバーヘッドが大きくなります。

# メモリリークと境界外アクセスの検出:
valgrind --leak-check=full ./prog arg1 arg2

# リークの発生元を追跡:
valgrind --leak-check=full --track-origins=yes ./prog

# 出力の解釈:
#   ==12345== Invalid read of size 4          ← 境界外アクセス / use-after-free
#   ==12345==    at 0x4005A4: main (test.c:5)  ← 発生位置
#   ==12345==  Address 0x5204040 is 0 bytes after a block of size 40 alloc'd
#   ==12345==    at 0x4C2E80F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
#   ==12345==    by 0x40058F: main (test.c:3)  ← 割り当て位置

Valgrind ツール群

# memcheck (デフォルト): メモリエラー
#   検出対象: UAF, ダブルフリー, 境界外アクセス, 未初期化値, メモリリーク

# helgrind: データ競合
valgrind --tool=helgrind ./prog

# cachegrind: CPU キャッシュのシミュレーション
valgrind --tool=cachegrind ./prog
cg_annotate cachegrind.out.<pid>  # ソースレベルでのミス率

# massif: ヒープ解析
valgrind --tool=massif ./prog
ms_print massif.out.<pid>  # 時間軸: 誰がどのくらいのメモリを割り当てたか

Valgrind の制限

オーバーヘッド:
  memcheck: 約 20〜30 倍の低速化
  helgrind: 約 10〜20 倍の低速化
  cachegrind: 約 20〜50 倍の低速化
  massif: 約 10 倍の低速化

サポートされていないもの:
  - 静的リンクされたプログラム (ld.so と libc は置換可能である必要がある)
  - 一部の珍しいシステムコール
  - 一部の命令セット拡張 (AVX-512 は一部サポート)

Sanitizers: コンパイル時のインスツルメンテーション

Sanitizers はコンパイル時にチェックコードを挿入し、実行時にエラーを検出します。Valgrind よりも高速ですが、再コンパイルが必要です。

# ASAN: ヒープ境界外アクセス, UAF, スタックオーバーフロー, グローバル境界外アクセス
gcc -fsanitize=address -g -O1 -o prog prog.c
./prog
# → エラー発生時: 正確な位置 + 割り当て位置 + 解放位置を出力 (カラー表示!)

# UBSAN: 未定義動作
gcc -fsanitize=undefined -o prog prog.c
# 検出対象: 整数オーバーフロー, シフトの境界外, null 間参照, 型のアライメントエラー

# TSAN: データ競合 (64-bit のみ)
gcc -fsanitize=thread -g -O1 -o prog prog.c
# → 報告: "data race on variable x between thread T1 and T2"

# LSAN: メモリリーク (ASAN に含まれている)
ASAN_OPTIONS=detect_leaks=1 ./prog

# MSAN: 未初期化メモリへの読み取り (Clang のみ)
clang -fsanitize=memory -g -o prog prog.c

ASAN の原理

ASAN はシャドウメモリ (shadow memory) を使用します:
  8 バイトのメモリごとに 1 バイトのシャドウメモリ
  シャドウ値: 0=完全にアクセス可能, 1-7=部分的に有効, 負数=アクセス不可

メモリアクセスのたびに、コンパイラがシャドウチェックを挿入
  → シャドウ値がアクセス不可を示す場合 → ASAN がレポート
  → 割り当て領域の周囲に「レッドゾーン」(redzone) を配置 → 境界外アクセス検出
  → 解放後に「クォランティン」(quarantine) に配置 → UAF 検出

メモリオーバーヘッド: 約 2 倍 (シャドウメモリ + レッドゾーン + クォランティン)

Valgrind vs Sanitizers

ValgrindSanitizers
再コンパイルが必要かいいえ (任意のバイナリ)はい (-fsanitize=)
速度20〜50 倍の低速化1.5〜5 倍の低速化
メモリオーバーヘッド追加メモリなし2 倍 (ASAN)
検出精度高い (バイナリトランスレーションにより全てを可視化)高い (コンパイル時のインスツルメンテーション)
スレッド検出helgrindTSAN
使用シーンソースコードなしのブラックボックスソースコードあり、CI に統合

実践事例

# 事例 1: ヒープ境界外アクセス → ASAN
#   test.c:4: buf[100] = 0;  // buf は char[100]
gcc -fsanitize=address -g -o test test.c
./test
# ==ERROR: AddressSanitizer: heap-buffer-overflow
#     WRITE of size 1 at 0x602000000064 thread T0
#     #0 0x4007b3 in main test.c:4

# 事例 2: データ競合 → TSAN
gcc -fsanitize=thread -g -o test test.c
./test
# WARNING: ThreadSanitizer: data race
#   Write of size 4 at 0x7b0400000000 by thread T2:
#     #0 counter_thread test.c:8
#   Previous write of size 4 at 0x7b0400000000 by thread T1:
#     #0 counter_thread test.c:8

# 事例 3: 未初期化読み取り → Valgrind memcheck
valgrind ./prog
# Conditional jump or move depends on uninitialised value(s)

参考

  • ドキュメント⁠: https://github.com/google/sanitizers, https://valgrind.org/docs/manual
  • LWN: "Address Sanitizer", "Thread Sanitizer"
  • ツール⁠: heaptrack (ヒーププロファイラ、massif の代替)

キーワード: Valgrind, memcheck, helgrind, massif, ASAN, UBSAN, TSAN, MSAN, shadow memory, data race