5.3 内存罪证:AddressSanitizer 与 Valgrind


5.3 内存罪证:AddressSanitizer 与 Valgrind

内存错误检测器专门抓越界读写、释放后使用(use-after-free)、重复释放这类"现场即焚"的缺陷。AddressSanitizer(编译器插桩,快)与 Valgrind(二进制翻译,免重编译)是两代主流仪器,机制不同、适用场景互补。

内存错误为什么难抓

内存越界的可怕在于案发与案发现场分离

int *scores = malloc(100 * sizeof(int)); scores[100] = 0; // 越界写:踩了相邻内存

这一行执行时什么都不发生。被踩的可能是另一个对象的字段,几分钟后程序在完全无关的地方崩溃或行为诡异。回头看崩溃栈,与真凶相距十万八千里。更糟的是堆分配会四舍五入对齐,越界几个字节可能恰好踩进填充区——测试环境永远没事,换个分配序列就爆炸。

所以这类案件不能等报案,要主动布控:让每一次越界在发生当场就被抓住。

AddressSanitizer:埋在分配器周围的哨兵

ASan 的机制三件套:

  1. 影子内存:把整个地址空间按 1:8 映射成一张"毒化表",每个字节标注它是否可访问;
  2. 插桩检查:编译时在每次内存访问前插入"查表"代码,访问毒化区立刻报警;
  3. 红区陷阱:分配器在每个对象前后垫红色禁区,越界第一步就踩雷。
clang -fsanitize=address -g -O1 -o myapp myapp.c ./myapp

案发当场输出:

==3121==ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 4 at 0x6020000001d0 #0 update_scores scores.c:44 #1 process_batch batch.c:120 allocated by thread T0 here: #0 malloc #1 init_scores scores.c:30 ← 这块内存是谁申请的也有

肇事访问、完整栈、甚至这块内存的出生证明(分配栈)一并列出。ASan 平均减速 2 倍左右、内存涨 2–3 倍——重但可用,很多团队把它开在部分生产灰度实例上,专门抓低概率内存案。

Valgrind:不用重编译的老法医

Valgrind 走另一条路:把目标二进制动态翻译成中间表示,逐指令插桩,模拟一个"知道一切"的虚拟机。优点是无需重编译,拿来任何二进制就能验;缺点是慢 10–50 倍,只能离线用。

valgrind --leak-check=full --show-leak-kinds=all ./myapp

泄漏报告:

8 bytes in 1 blocks are definitely lost in loss record 1 of 12 at 0x4C2: malloc (vg_replace_malloc.c) by 0x4012: create_session (session.c:71) by 0x4088: main (main.c:22)

definitely lost 表示连指针都没人保存了,无法回收也无法使用——确定泄漏。Valgrind 的泄漏分类(definitely/indirectly/possibly lost)比"内存涨了"这种体感证据精确得多,是 C/C++ 项目发布前的例行验尸项目。

两代仪器怎么选

维度 ASan Valgrind
接入方式 重编译加参数 直接跑二进制
速度代价 2 倍 10–50 倍
内存代价 2–3 倍 显著
越界/use-after-free 强(红区+影子)
泄漏 LSan 子工具 强且分类细
生产灰度 部分团队可用 不可

我的取舍:源码在手选 ASan(快、能进 CI 和灰度),第三方闭源二进制选 Valgrind(免编译是唯一选项),发布前的泄漏审计可以用 Valgrind 的全量报告做终审。两者还有配对的姊妹工具:MSan 查未初始化读取、UBSan 查未定义行为(整型溢出、除零),与 ASan 一样是编译器开关。

检测器机制对比

检测器机制对比

⚠️ 常见坑:ASan 开着就修改了内存布局,"带 ASan 能复现、关掉不能复现"是常态而非矛盾——本来就越界踩填充区的案子,布局一变就现形或隐身。这正是第 1 章观察者效应在调试域的又一次显形。

本节要点回顾

  • 内存案案发与现场分离,不能等报案,要在测试环节主动布控;
  • ASan = 影子内存 + 红区 + 插桩,慢 2 倍,可进 CI 与灰度实例;
  • Valgrind = 二进制翻译模拟,免重编译,慢一个量级,适合闭源组件与发布终审;
  • 报告自带出生证明:肇事栈 + 分配栈双双在案,直接指向修复点;
  • 布局敏感是特性不是 bug:检测器改变布局让潜伏案现行,属正常现象。

延伸:ASan 的开销账与部署形态

ASan 常速 2 倍、内存 3–4 倍,比 Valgrind(10–50 倍)便宜一个量级,因此有"测试环境全量常开、生产按比例灰度"两种形态。生产灰度要开回收与采样开关,控制内存上限:

# 测试/CI: 全量检测 clang++ -fsanitize=address -fno-omit-frame-pointer -O1 -g app.cpp ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 ./a.out # 生产灰度: 低开销形态 ASAN_OPTIONS=quarantine_size_mb=16:thread_local_quarantine_size_kb=64 \ :max_uar_stack_size_log=16:allocator_may_return_null=1 ./svc

灰度形态对 UAF/越界的检出能力不变,损失的主要是泄漏检测的完备性——这是"检出能力换常驻成本"的明确交易,比拍脑袋开关更可辩护。

延伸:从报错地址到代码行

ASan 报错给出的是影子内存判定的访问类型与地址,定位到行的关键参数是编译时 -g 与运行时 symbolizer。读报告先看三段:ERROR 类型(heap-use-after-free 等)、访问栈(谁踩的)、分配与释放栈(对象的一生)。heap-use-after-free 的修复证据链要求同时看"释放栈为什么早退"——把 free 提前或把访问延后,选错方向会引入更隐蔽的竞争。Valgrind 在这条链上的增量价值是不需重编译即可检查第三方闭源库,以及 memcheck 之外 helgrind/drd 两个并发检查器,作为 ASan 覆盖不到场景的备选实验室。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U