GCC 完全指南 / 14 - Sanitizers
14 - Sanitizers
学习使用 GCC 的 Sanitizers——ASan、TSan、UBSan 进行运行时内存错误和未定义行为检测。
14.1 Sanitizers 概述
Sanitizers 是编译器插桩的运行时错误检测工具,在程序运行时检查各种错误。
| Sanitizer | 全称 | 检测的问题 |
|---|---|---|
| ASan | AddressSanitizer | 内存越界、Use-After-Free、内存泄漏 |
| TSan | ThreadSanitizer | 数据竞争、死锁 |
| UBSan | UndefinedBehaviorSanitizer | 未定义行为(溢出、空指针等) |
| MSan | MemorySanitizer | 使用未初始化内存(仅 Clang 支持) |
| LSan | LeakSanitizer | 内存泄漏(ASan 自带) |
# 启用 Sanitizer
gcc -fsanitize=address -g -o hello main.c # ASan
gcc -fsanitize=thread -g -o hello main.c # TSan
gcc -fsanitize=undefined -g -o hello main.c # UBSan
# 可以组合使用
gcc -fsanitize=address,undefined -g -o hello main.c
14.2 AddressSanitizer (ASan)
ASan 检测各种内存访问错误。
检测的问题类型
| 问题 | 说明 |
|---|---|
| Heap buffer overflow | 堆缓冲区越界读写 |
| Stack buffer overflow | 栈缓冲区越界读写 |
| Global buffer overflow | 全局缓冲区越界读写 |
| Use-After-Free | 使用已释放的内存 |
| Use-After-Return | 使用已返回的栈变量 |
| Use-After-Scope | 使用已离开作用域的栈变量 |
| Double-Free | 对同一内存释放两次 |
| Memory leaks | 内存泄漏 |
示例:堆缓冲区溢出
// asan_heap.c
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int *arr = (int *)malloc(5 * sizeof(int));
arr[5] = 42; // 堆缓冲区溢出!索引 5 超出 [0,4] 范围
free(arr);
return 0;
}
gcc -fsanitize=address -g -o asan_heap asan_heap.c
./asan_heap
ASan 输出:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014
WRITE of size 4 at 0x602000000014 thread T0
#0 0x55... in main asan_heap.c:6
0x602000000014 is located 0 bytes to the right of 20-byte region [0x602000000000,0x602000000014)
allocated by thread T0 here:
#0 0x7f... in malloc
#1 0x55... in main asan_heap.c:5
示例:Use-After-Free
// asan_uaf.c
#include <stdlib.h>
int main(void) {
int *p = (int *)malloc(sizeof(int));
*p = 42;
free(p);
return *p; // Use-After-Free!
}
gcc -fsanitize=address -g -o asan_uaf asan_uaf.c
./asan_uaf
# ==12345==ERROR: AddressSanitizer: heap-use-after-free on address ...
示例:栈缓冲区溢出
// asan_stack.c
#include <stdio.h>
int main(void) {
int arr[5];
arr[5] = 42; // 栈缓冲区溢出!
return 0;
}
示例:内存泄漏
// asan_leak.c
#include <stdlib.h>
void leak(void) {
int *p = (int *)malloc(100);
// 忘记 free(p)
}
int main(void) {
for (int i = 0; i < 3; i++) {
leak();
}
return 0;
}
gcc -fsanitize=address -g -o asan_leak asan_leak.c
./asan_leak
# ==12345==ERROR: LeakSanitizer: detected memory leaks
# Direct leak of 300 byte(s) in 3 object(s) allocated from:
# #0 ... in malloc
# #1 ... in leak asan_leak.c:4
ASan 运行时选项
# 通过环境变量控制 ASan
ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1" ./program
# 常用选项
ASAN_OPTIONS="detect_leaks=1" # 启用泄漏检测(默认开启)
ASAN_OPTIONS="detect_leaks=0" # 禁用泄漏检测
ASAN_OPTIONS="halt_on_error=1" # 遇到错误立即终止
ASAN_OPTIONS="print_stats=1" # 打印统计信息
ASAN_OPTIONS="detect_stack_use_after_return=1" # 检测栈 Use-After-Return
ASAN_OPTIONS="allocator_may_return_null=1" # malloc 可能返回 NULL
# 输出格式
ASAN_OPTIONS="log_path=/tmp/asan_log" # 输出到文件
ASan 的性能开销
| 指标 | 开销 |
|---|---|
| CPU 时间 | 约 2x 慢 |
| 内存 | 约 3x 更多内存 |
| 代码大小 | 约 2x 更大 |
14.3 ThreadSanitizer (TSan)
TSan 检测多线程程序中的数据竞争。
检测的问题
| 问题 | 说明 |
|---|---|
| 数据竞争 | 两个线程同时访问同一内存,至少一个是写入 |
| 死锁 | 线程互相等待对方持有的锁 |
示例:数据竞争
// tsan_race.c
#include <pthread.h>
#include <stdio.h>
int shared = 0;
void *thread_func(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) {
shared++; // 数据竞争!没有同步保护
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("shared = %d\n", shared);
return 0;
}
gcc -fsanitize=thread -g -pthread -o tsan_race tsan_race.c
./tsan_race
TSan 输出:
==================
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x... by thread T1:
#0 thread_func tsan_race.c:8
Previous write of size 4 at 0x... by thread T2:
#0 thread_func tsan_race.c:8
Location is global 'shared' at 0x...
==================
示例:死锁检测
// tsan_deadlock.c
#include <pthread.h>
pthread_mutex_t lock_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock_b = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) {
(void)arg;
pthread_mutex_lock(&lock_a);
pthread_mutex_lock(&lock_b); // 等待 thread2 持有的 lock_b
pthread_mutex_unlock(&lock_b);
pthread_mutex_unlock(&lock_a);
return NULL;
}
void *thread2(void *arg) {
(void)arg;
pthread_mutex_lock(&lock_b);
pthread_mutex_lock(&lock_a); // 等待 thread1 持有的 lock_a → 死锁!
pthread_mutex_unlock(&lock_a);
pthread_mutex_unlock(&lock_b);
return NULL;
}
gcc -fsanitize=thread -g -pthread -o tsan_deadlock tsan_deadlock.c
./tsan_deadlock
TSan 性能开销
| 指标 | 开销 |
|---|---|
| CPU 时间 | 约 5-15x 慢 |
| 内存 | 约 5-10x 更多 |
14.4 UndefinedBehaviorSanitizer (UBSan)
UBSan 检测 C/C++ 未定义行为。
检测的问题
| 问题 | 说明 |
|---|---|
| 整数溢出 | 有符号整数加减乘溢出 |
| 除以零 | 整数或浮点除以零 |
| 空指针解引用 | 对 NULL 指针操作 |
| 越界数组访问 | VLA 越界 |
| 类型转换错误 | 不合理类型转换 |
| 移位错误 | 负数移位或移位量超出类型宽度 |
| bool 类型错误 | 对 bool 赋值非 0/1 的值 |
| 对齐错误 | 未对齐的指针解引用 |
| VLA 边界 | 可变长度数组大小为负或过大 |
示例
// ubsan_test.c
#include <stdio.h>
#include <limits.h>
int main(void) {
// 整数溢出
int a = INT_MAX;
int b = a + 1; // 有符号整数溢出!
printf("b = %d\n", b);
// 除以零
int c = 1;
int d = 0;
int e = c / d; // 除以零!
printf("e = %d\n", e);
// 移位错误
int f = 1;
int g = f << 32; // 移位量超出类型宽度(int 是 32-bit)
return 0;
}
gcc -fsanitize=undefined -g -o ubsan_test ubsan_test.c
./ubsan_test
UBSan 输出:
ubsan_test.c:7:15: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
ubsan_test.c:12:15: runtime error: division by zero
ubsan_test.c:17:17: runtime error: shift exponent 32 is too large for 32-bit type 'int'
UBSan 子选项
# 启用特定检查
gcc -fsanitize=signed-integer-overflow # 有符号整数溢出
gcc -fsanitize=shift # 移位错误
gcc -fsanitize=divide-by-zero # 除以零
gcc -fsanitize=null # 空指针解引用
gcc -fsanitize=alignment # 对齐错误
gcc -fsanitize=bool # bool 类型错误
gcc -fsanitize=enum # 枚举值超出范围
gcc -fsanitize=bounds # 数组越界
# 启用所有 UBSan 检查
gcc -fsanitize=undefined -g -o test test.c
UBSan 运行时选项
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" ./test
# 检测到错误时继续运行(默认)
UBSAN_OPTIONS="halt_on_error=0" ./test
UBSan 的性能开销
| 指标 | 开销 |
|---|---|
| CPU 时间 | 约 1.5-2x 慢 |
| 内存 | 约 1.5x 更多 |
14.5 Sanitizer 组合使用
# ASan + UBSan(推荐开发使用)
gcc -fsanitize=address,undefined -g -fno-omit-frame-pointer -o test test.c
# TSan + UBSan(多线程项目)
gcc -fsanitize=thread,undefined -g -pthread -o test test.c
# 注意:ASan 和 TSan 不能同时使用!
# gcc -fsanitize=address,thread # 错误:不兼容
14.6 Sanitizer 在 Makefile 中的使用
CC = gcc
CFLAGS_COMMON = -Wall -Wextra -std=c17
# Debug 构建:启用 Sanitizers
ifdef ASAN
CFLAGS_COMMON += -fsanitize=address
LDFLAGS += -fsanitize=address
endif
ifdef TSAN
CFLAGS_COMMON += -fsanitize=thread
LDFLAGS += -fsanitize=thread
endif
ifdef UBSAN
CFLAGS_COMMON += -fsanitize=undefined
LDFLAGS += -fsanitize=undefined
endif
# 通用 Debug 标志
CFLAGS_DEBUG = -g3 -O0 -fno-omit-frame-pointer
CFLAGS_DEBUG += -fsanitize=address,undefined
# Release 标志
CFLAGS_RELEASE = -O2 -DNDEBUG
.PHONY: debug release test-asan test-tsan
debug:
$(CC) $(CFLAGS_COMMON) $(CFLAGS_DEBUG) -o hello main.c
release:
$(CC) $(CFLAGS_COMMON) $(CFLAGS_RELEASE) -o hello main.c
test-asan:
$(CC) $(CFLAGS_COMMON) -fsanitize=address -g -o test test.c
./test
test-tsan:
$(CC) $(CFLAGS_COMMON) -fsanitize=thread -g -pthread -o test test.c
./test
14.7 Sanitizer 抑制
# 创建抑制文件
cat > suppress.txt << 'EOF'
# 忽略已知的第三方库问题
interceptor_via_fun:third_party_function
race:known_benign_race_function
EOF
# 使用抑制文件
ASAN_OPTIONS="suppressions=suppress.txt" ./test
TSAN_OPTIONS="suppressions=suppress.txt" ./test
源码中抑制
// GCC 14+ 支持属性标记
__attribute__((no_sanitize("address")))
void known_unsafe_function(void) {
// ASan 不检查此函数
}
__attribute__((no_sanitize("undefined")))
void suppress_ubsan(void) {
// UBSan 不检查此函数
}
__attribute__((no_sanitize("thread")))
void suppress_tsan(void) {
// TSan 不检查此函数
}
要点回顾
| 要点 | 核心内容 |
|---|---|
| ASan | 内存越界、UAF、内存泄漏,约 2x 慢 |
| TSan | 数据竞争、死锁,约 5-15x 慢 |
| UBSan | 未定义行为,约 1.5x 慢 |
| 组合 | ASan+UBSan 推荐开发,但 ASan 和 TSan 不能同时使用 |
| 选项 | ASAN_OPTIONS / TSAN_OPTIONS / UBSAN_OPTIONS 控制行为 |
注意事项
ASan 和 TSan 不能同时使用: 两者使用相同的 shadow memory 空间,互斥。需要用不同的构建分别测试。
Sanitizer 不替代 Valgrind: Sanitizers 需要重新编译,Valgrind 无需重新编译。两者互补。
生产环境不要用 Sanitizer: 性能开销和 false positive 风险使 Sanitizer 不适合生产环境。
UBSan 检测到问题后默认继续运行: 使用
halt_on_error=1使其立即终止。
扩展阅读
- GCC Sanitizer Wiki — Sanitizer 项目 Wiki
- AddressSanitizer — ASan 文档
- ThreadSanitizer — TSan 文档
- UndefinedBehaviorSanitizer — UBSan 文档
下一步
→ 15 - 性能分析:学习使用 gprof、perf 和火焰图进行程序性能分析。