强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

LLVM 开发指南 / 第 15 章:Sanitizers

第 15 章:Sanitizers

“Sanitizers 是 C/C++ 程序员的安全网——在运行时捕获那些未定义行为。”


15.1 Sanitizers 概述

Sanitizers 是 LLVM/Clang 提供的运行时错误检测工具,通过编译器插桩实现。

Sanitizer检测目标编译选项运行时库
ASan内存错误-fsanitize=addresslibasan
MSan未初始化内存-fsanitize=memorylibmsan
TSan数据竞争-fsanitize=threadlibtsan
UBSan未定义行为-fsanitize=undefinedlibubsan
HWASan内存错误 (ARM64)-fsanitize=hwaddresslibhwasan
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=nullNULL 解引用
数组越界-fsanitize=boundsVLA/指针越界
对齐-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内存错误2x2x日常开发
MSan未初始化2x3x特定问题
TSan数据竞争5-10x5-15x多线程调试
UBSan未定义行为5%5%总是启用
HWASan内存错误15%15%ARM64 生产

扩展阅读

  1. AddressSanitizer — ASan 文档
  2. ThreadSanitizer — TSan 文档
  3. MemorySanitizer — MSan 文档
  4. Sanitizer Wiki — Sanitizer 项目 Wiki

下一章: 第 16 章:LLDB 调试器 — 学习 LLDB 调试器的架构、脚本化和扩展开发。