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

GCC 完全指南 / 14 - Sanitizers

14 - Sanitizers

学习使用 GCC 的 Sanitizers——ASan、TSan、UBSan 进行运行时内存错误和未定义行为检测。


14.1 Sanitizers 概述

Sanitizers 是编译器插桩的运行时错误检测工具,在程序运行时检查各种错误。

Sanitizer全称检测的问题
ASanAddressSanitizer内存越界、Use-After-Free、内存泄漏
TSanThreadSanitizer数据竞争、死锁
UBSanUndefinedBehaviorSanitizer未定义行为(溢出、空指针等)
MSanMemorySanitizer使用未初始化内存(仅 Clang 支持)
LSanLeakSanitizer内存泄漏(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 使其立即终止。


扩展阅读


下一步

15 - 性能分析:学习使用 gprof、perf 和火焰图进行程序性能分析。