本页目录
Valgrind 与 Sanitizers 实战
覆盖: Valgrind (memcheck/helgrind/cachegrind/massif) → ASAN/UBSAN/TSAN → 对比与选择 → 实际调试案例 适用: Linux 用户态, C/C++
Valgrind: 动态二进制翻译
Valgrind 本质上是一个 JIT 编译器——它将程序的机器码翻译为中间表示 (VEX IR),在 IR 上插桩分析,再翻译回机器码执行。不需要重编译目标程序,但性能开销大。
# 内存泄漏 + 越界检测:
# 追踪泄漏来源:
# 输出解读:
# ==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, double free, 越界, 未初始化值, 内存泄漏
# helgrind: 数据竞争
# cachegrind: CPU 缓存模拟
# massif: 堆分析
Valgrind 的限制
开销:
memcheck: ~20-30x 慢
helgrind: ~10-20x 慢
cachegrind: ~20-50x 慢
massif: ~10x 慢
不支持:
- 静态链接的程序 (ld.so 和 libc 必须可替换)
- 某些不常见的系统调用
- 某些指令集扩展 (AVX-512 部分支持)
Sanitizers: 编译时插桩
Sanitizers 在编译时插入检查代码 → 运行时检测错误。比 Valgrind 快很多,但需要重编译。
# ASAN: 堆越界, UAF, 栈溢出, 全局越界
# → 触发错误时: 打印精确位置 + 分配位置 + 释放位置 (彩色!)
# UBSAN: 未定义行为
# 检测: 整数溢出, 移位越界, null 解引用, 类型对齐错误
# TSAN: 数据竞争 (仅 64-bit)
# → 报告: "data race on variable x between thread T1 and T2"
# LSAN: 内存泄漏 (已包含在 ASAN 中)
ASAN_OPTIONS=detect_leaks=1
# MSAN: 未初始化内存读 (Clang only)
ASAN 原理
ASAN 使用影子内存 (shadow memory):
每 8 bytes 应用内存 → 1 byte 影子内存
影子值: 0=完全可寻址, 1-7=部分有效, 负数=不可寻址
每次内存访问: 编译器插入影子检查
→ 如果影子值说不可寻址 → ASAN report
→ 分配周围放 "红区" (redzone) → 越界检测
→ 释放后放 "隔离区" (quarantine) → UAF 检测
内存开销: ~2x (影子内存 + 红区 + 隔离区)
Valgrind vs Sanitizers
| Valgrind | Sanitizers | |
|---|---|---|
| 需要重编译 | 否 (任意 binary) | 是 (-fsanitize=) |
| 速度 | 20-50x 慢 | 1.5-5x 慢 |
| 内存开销 | 无额外内存 | 2x (ASAN) |
| 检测精度 | 高 (二进制翻译看所有) | 高 (编译时插桩) |
| 多线程检测 | helgrind | TSAN |
| 使用场景 | 不带源码的 black-box | 带源码, 集成到 CI |
实战案例
# 案例 1: 堆越界 → ASAN
# test.c:4: buf[100] = 0; // buf is char[100]
# ==ERROR: AddressSanitizer: heap-buffer-overflow
# WRITE of size 1 at 0x602000000064 thread T0
# #0 0x4007b3 in main test.c:4
# 案例 2: 数据竞争 → TSAN
# 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
# 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 (堆 profiler, alternative to massif)
关键词: Valgrind, memcheck, helgrind, massif, ASAN, UBSAN, TSAN, MSAN, shadow memory, data race