27、Sanitizer与调试:AddressSanitizer、UndefinedBehaviorSanitizer、ThreadSanitizer、调试符号设置
调试这件事,说白了就是跟 bug 斗智斗勇。我做了这么多年跨平台开发,最怕的不是逻辑复杂,而是内存越界、未定义行为这种「幽灵 bug」——它们不一定会立刻崩溃,但会在某个深夜突然炸掉你的发布版本。
嗯,今天要聊的 Sanitizer 工具集,就是专门对付这些幽灵的利器。它们不是玄学,是实实在在的编译器插桩技术。
27.1 调试符号:-g 到底在干什么?
先说说最基础的调试符号设置。很多新手以为加个 -g 就完事了,其实这里头有讲究。
在 CMake 里,我们通常这样设置:
# 设置调试符号
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g")
# 或者更推荐的方式
set(CMAKE_BUILD_TYPE Debug)
# CMake 会自动添加 -g
我个人习惯在 Release 版本也保留部分调试符号:
# Release 版也生成符号,方便线上问题定位
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g1")
# -g0: 无符号
# -g1: 最小符号(函数名、行号)
# -g2: 完整符号(默认)
# -g3: 包含宏定义
我的经验:线上环境用
-g1 就够了。既能定位崩溃堆栈,又不会让二进制膨胀太多。我曾经见过一个项目开了 -g3,二进制体积直接翻了三倍。
27.2 AddressSanitizer:内存错误的照妖镜
AddressSanitizer(简称 ASan)是我最常用的工具。它能在运行时检测内存越界、释放后使用、双重释放等问题。
在 CMake 中启用 ASan:
# 方法一:直接设置编译和链接标志
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
# 方法二:使用 CMake 3.13+ 的 target 属性
target_compile_options(my_target PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(my_target PRIVATE -fsanitize=address)
来看一个典型的例子:
#include <cstring>
#include <iostream>
int main() {
char* buffer = new char[10];
strcpy(buffer, "Hello, World!"); // 越界了!
std::cout << buffer << std::endl;
delete[] buffer;
return 0;
}
编译运行后,ASan 会直接告诉你:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001f
WRITE of size 14 at 0x60200000001f thread T0
#0 0x4a1b2c in main /path/to/main.cpp:6
注意:ASan 会显著增加内存占用(约 2-3 倍)和运行时间(约 2 倍)。所以只在 Debug 或测试环境开启,千万别带到生产环境。
27.3 UndefinedBehaviorSanitizer:未定义行为的侦探
UndefinedBehaviorSanitizer(UBSan)专门抓那些「编译器觉得你错了但不会报错」的行为。比如整数溢出、空指针解引用、类型双关等。
启用方式:
# 启用 UBSan
target_compile_options(my_target PRIVATE -fsanitize=undefined)
target_link_options(my_target PRIVATE -fsanitize=undefined)
# 也可以组合使用
target_compile_options(my_target PRIVATE
-fsanitize=undefined
-fsanitize=address
-fno-sanitize-recover=undefined # 遇到 UB 直接终止
)
举个例子:
int main() {
int x = INT_MAX;
int y = x + 1; // 整数溢出,未定义行为
return 0;
}
UBSan 会输出:
runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
避坑指南:我曾经在一个图像处理项目里,因为整数溢出导致某些图片出现诡异的条纹。当时排查了两天,最后用 UBSan 一跑就现原形了。从那以后,我所有涉及数学运算的模块都会开 UBSan。
27.4 ThreadSanitizer:多线程的照妖镜
ThreadSanitizer(TSan)专门检测数据竞争和死锁。它比 ASan 更「重」,但对付多线程问题非常有效。
启用方式:
# TSan 不能和 ASan 同时使用
target_compile_options(my_target PRIVATE -fsanitize=thread -fno-omit-frame-pointer)
target_link_options(my_target PRIVATE -fsanitize=thread)
来看一个经典的数据竞争例子:
#include <thread>
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 数据竞争!
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return 0;
}
TSan 会报告:
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x7f1234567890 by thread T1:
#0 increment() /path/to/main.cpp:6
Previous write of size 4 at 0x7f1234567890 by thread T2:
#0 increment() /path/to/main.cpp:6
我的建议:TSan 会大幅拖慢程序(5-10 倍),所以只在单元测试或压力测试时开启。另外,记得加上
-fsanitize=thread 的链接标志,否则可能链接失败。
27.5 知识体系总览
下面这张图总结了 Sanitizer 工具集的核心逻辑:
27.6 实战:在 CMake 中统一管理 Sanitizer
我个人习惯写一个 CMake 函数来统一管理这些选项:
function(enable_sanitizer target mode)
if(mode STREQUAL "asan")
target_compile_options(${target} PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(${target} PRIVATE -fsanitize=address)
elseif(mode STREQUAL "ubsan")
target_compile_options(${target} PRIVATE -fsanitize=undefined -fno-sanitize-recover=undefined)
target_link_options(${target} PRIVATE -fsanitize=undefined)
elseif(mode STREQUAL "tsan")
target_compile_options(${target} PRIVATE -fsanitize=thread -fno-omit-frame-pointer)
target_link_options(${target} PRIVATE -fsanitize=thread)
elseif(mode STREQUAL "all")
# 注意:TSan 不能和 ASan 同时使用
target_compile_options(${target} PRIVATE
-fsanitize=address
-fsanitize=undefined
-fno-omit-frame-pointer
-fno-sanitize-recover=undefined
)
target_link_options(${target} PRIVATE
-fsanitize=address
-fsanitize=undefined
)
endif()
endfunction()
# 使用示例
add_executable(my_app main.cpp)
enable_sanitizer(my_app "asan")
核心要点:
- 调试符号
-g是基础,建议 Debug 用-g2,Release 用-g1 - ASan 抓内存问题,UBSan 抓未定义行为,TSan 抓线程问题
- ASan 和 TSan 不能同时启用,但 ASan 可以和 UBSan 组合
- 所有 Sanitizer 都会降低性能,只适合调试和测试环境
嗯,这些工具用好了,能省下你大量排查 bug 的时间。我到现在还记得第一次用 ASan 抓到那个隐藏了两周的内存泄漏时的快感——就像在黑暗里突然开了灯。