LLVM 开发指南 / 第 15 章:Sanitizers
第 15 章:Sanitizers
“Sanitizers 是 C/C++ 程序员的安全网——在运行时捕获那些未定义行为。”
15.1 Sanitizers 概述
Sanitizers 是 LLVM/Clang 提供的运行时错误检测工具,通过编译器插桩实现。
| Sanitizer | 检测目标 | 编译选项 | 运行时库 |
|---|---|---|---|
| ASan | 内存错误 | -fsanitize=address | libasan |
| MSan | 未初始化内存 | -fsanitize=memory | libmsan |
| TSan | 数据竞争 | -fsanitize=thread | libtsan |
| UBSan | 未定义行为 | -fsanitize=undefined | libubsan |
| HWASan | 内存错误 (ARM64) | -fsanitize=hwaddress | libhwasan |
| CFI | 控制流完整性 | -fsanitize=cfi | — |
15.1.1 性能开销
| Sanitizer | 内存开销 | CPU 开销 |
|---|---|---|
| ASan | ~2x | ~2x |
| MSan | ~2x | ~3x |
| TSan | ~5-10x | ~5-15x |
| UBSan | ~1.05x | ~1.05x |
| HWASan | ~1.15x | ~1.15x |
15.2 AddressSanitizer (ASan)
15.2.1 检测的错误类型
| 错误类型 | 说明 |
|---|---|
| 堆缓冲区溢出 | 堆上数组越界访问 |
| 栈缓冲区溢出 | 栈上数组越界访问 |
| 全局缓冲区溢出 | 全局数组越界访问 |
| use-after-free | 释放后使用 |
| use-after-return | 返回后使用栈变量 |
| double-free | 双重释放 |
| 内存泄漏 | 堆内存未释放 |
15.2.2 ASan 使用
// asan_example.cpp
#include <cstdlib>
#include <cstdio>
void heap_buffer_overflow() {
int *arr = new int[10];
arr[10] = 42; // 越界!
delete[] arr;
}
void use_after_free() {
int *p = new int(42);
delete p;
printf("%d\n", *p); // use-after-free!
}
void stack_buffer_overflow() {
int arr[10];
arr[10] = 42; // 栈越界!
}
int main() {
heap_buffer_overflow();
// use_after_free(); // 取消注释以测试
// stack_buffer_overflow();
return 0;
}
# 编译启用 ASan
clang++ -fsanitize=address -g -O1 asan_example.cpp -o asan_example
# 运行
./asan_example
ASan 错误报告示例:
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602...
WRITE of size 4 at 0x602... thread T0
#0 0x4a1b2c in heap_buffer_overflow() asan_example.cpp:7
#1 0x4a1c4d in main asan_example.cpp:18
0x602... is located 0 bytes to the right of 40-byte region [0x602..., 0x602...)
allocated by thread T0 here:
#0 0x49abc0 in operator new[](unsigned long)
#1 0x4a1b0a in heap_buffer_overflow() asan_example.cpp:5
15.2.3 ASan 实现原理
ASan 内存布局:
┌──────────────────────────────────────────┐
│ 程序内存 (Shadow Memory 每 8 字节映射 │
│ 到 1 字节 Shadow) │
│ │
│ [应用程序数据] [Shadow] [应用程序数据] │
│ 8字节 1字节 8字节 │
│ │
│ Shadow 编码: │
│ 0x00 — 8 字节全部可访问 │
│ 0x01-0x07 — 前 k 字节可访问 │
│ 0xfa-0xff — 全部不可访问 (redzone) │
└──────────────────────────────────────────┘
插桩后的代码:
*p = 42;
// 插桩后:
shadow_addr = (p >> 3) + SHADOW_OFFSET;
shadow_val = *shadow_addr;
if (shadow_val != 0) {
// 检查具体的偏移是否可访问
if (shadow_val < 0 || (p & 7) + size > shadow_val) {
__asan_report_store(p, size); // 报告错误
}
}
*p = 42; // 原始存储
15.3 MemorySanitizer (MSan)
15.3.1 检测未初始化内存使用
// msan_example.cpp
#include <cstdio>
int main() {
int x; // 未初始化
if (x > 0) { // 使用未初始化值
printf("positive\n");
}
return 0;
}
clang++ -fsanitize=memory -g -O1 msan_example.cpp -o msan_example
./msan_example
==12345==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x4a1b2c in main msan_example.cpp:5
Uninitialized value was stored to memory at
#0 0x4a1c4d in main msan_example.cpp:4
15.3.2 MSan 实现原理
MSan 使用 Shadow Memory 跟踪每个字节的初始化状态:
Shadow Memory:
每 1 字节应用内存 → 1 字节 Shadow (0 = 已初始化, 1 = 未初始化)
插桩后的代码:
int x; // 未初始化
// Shadow: shadow(x) = 0xFFFFFFFF (全部未初始化)
int y = x + 1;
// 传播 Shadow:
// shadow(y) = shadow(x) | shadow(1)
// = 0xFFFFFFFF | 0x00000000
// = 0xFFFFFFFF (y 也是未初始化的)
if (y > 0) {
// 检查 shadow(y) != 0 → 报告使用未初始化值
__msan_check(y);
}
15.4 ThreadSanitizer (TSan)
15.4.1 检测数据竞争
// tsan_example.cpp
#include <thread>
#include <cstdio>
int shared_data = 0;
void writer() {
shared_data = 42; // 竞争!
}
void reader() {
printf("%d\n", shared_data); // 竞争!
}
int main() {
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
return 0;
}
clang++ -fsanitize=thread -g -O1 -pthread tsan_example.cpp -o tsan_example
./tsan_example
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x... by thread T1:
#0 writer() tsan_example.cpp:7
Previous read of size 4 at 0x... by thread T2:
#0 reader() tsan_example.cpp:11
Location is global 'shared_data' at 0x...
15.4.2 TSan 实现原理
TSan 使用 向量时钟(Vector Clock) 算法:
每个线程维护一个逻辑时钟 (Vector Clock)
每个内存位置记录最后一次读/写的时间戳
检测规则:
如果两个线程访问同一内存位置
至少一个是写操作
且两者之间没有 happens-before 关事
→ 判定为数据竞争
15.5 UndefinedBehaviorSanitizer (UBSan)
15.5.1 检测的未定义行为
| 检查类型 | Flag | 说明 |
|---|---|---|
| 整数溢出 | -fsanitize=signed-integer-overflow | 有符号溢出 |
| 除零 | -fsanitize=integer-divide-by-zero | 除以零 |
| 空指针解引用 | -fsanitize=null | NULL 解引用 |
| 数组越界 | -fsanitize=bounds | VLA/指针越界 |
| 对齐 | -fsanitize=alignment | 未对齐访问 |
| bool 值 | -fsanitize=bool | 非法 bool 值 |
| enum 值 | -fsanitize=enum | 非法 enum 值 |
| float 转换 | -fsanitize=float-cast-overflow | 浮点溢出转换 |
# 启用所有 UBSan 检查
clang++ -fsanitize=undefined -g ubsan_example.cpp -o ubsan_example
# 仅启用特定检查
clang++ -fsanitize=signed-integer-overflow,null -g test.cpp -o test
// ubsan_example.cpp
#include <limits.h>
int overflow() {
int x = INT_MAX;
return x + 1; // 有符号溢出 — 未定义行为
}
int divide_by_zero(int x) {
return x / 0; // 除零
}
int main() {
overflow();
// divide_by_zero(42);
return 0;
}
15.6 HWASan (Hardware AddressSanitizer)
# HWASan 仅在 ARM64 上高效(利用 TBI 特性)
clang++ -fsanitize=hwaddress -g test.cpp -o test_hwasan
# 优势:内存开销极低 (~15%),适合生产环境
# 限制:目前仅 ARM64 高效支持
15.7 Sanitizer 组合使用
# 同时启用多个 Sanitizer(注意:ASan 和 TSan 不能同时使用)
clang++ -fsanitize=address,undefined -g test.cpp -o test # ✅ 可组合
clang++ -fsanitize=memory,undefined -g test.cpp -o test # ✅ 可组合
clang++ -fsanitize=address,thread -g test.cpp -o test # ❌ 不兼容
# 常见组合
clang++ -fsanitize=address,undefined -fno-omit-frame-pointer -g test.cpp
15.8 抑制和自定义
# 使用 suppressions 文件
# suppressions.txt
race:my_legacy_function
deadlock:third_party_lib*
# 设置环境变量
export TSAN_OPTIONS="suppressions=suppressions.txt"
export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0"
# ASan 常用选项
ASAN_OPTIONS="detect_leaks=1" # 启用泄漏检测
ASAN_OPTIONS="halt_on_error=0" # 不停止
ASAN_OPTIONS="print_stats=1" # 打印统计
ASAN_OPTIONS="detect_stack_use_after_return=1" # 检测栈 use-after-return
15.9 本章小结
| Sanitizer | 检测目标 | 内存开销 | CPU 开销 | 推荐使用 |
|---|---|---|---|---|
| ASan | 内存错误 | 2x | 2x | 日常开发 |
| MSan | 未初始化 | 2x | 3x | 特定问题 |
| TSan | 数据竞争 | 5-10x | 5-15x | 多线程调试 |
| UBSan | 未定义行为 | 5% | 5% | 总是启用 |
| HWASan | 内存错误 | 15% | 15% | ARM64 生产 |
扩展阅读
- AddressSanitizer — ASan 文档
- ThreadSanitizer — TSan 文档
- MemorySanitizer — MSan 文档
- Sanitizer Wiki — Sanitizer 项目 Wiki
下一章: 第 16 章:LLDB 调试器 — 学习 LLDB 调试器的架构、脚本化和扩展开发。