Sanitizer 与代码质量:集成 AddressSanitizer、UBSan、TSan
说实话,我见过太多 C++ 项目在线上跑着跑着就崩了。查了半天,最后发现是内存越界或者野指针。这种问题,靠人眼 review 很难发现。好在 CMake 里有一整套 Sanitizer 工具链,能帮我们在编译阶段就揪出这些隐患。
今天我们就聊聊怎么在 CMake 里集成 AddressSanitizer、UBSan 和 TSan。说白了,就是给代码装个「体检仪」。
Sanitizer 是什么?
Sanitizer 是 LLVM/GCC 编译器内置的动态分析工具。它会在你的代码里插入检测代码,运行时一旦发现异常,立刻报错并打印调用栈。
我个人习惯把 Sanitizer 分成三类:
- AddressSanitizer (ASan):检测内存错误,比如越界、释放后使用、双重释放
- UndefinedBehaviorSanitizer (UBSan):检测未定义行为,比如整数溢出、空指针解引用
- ThreadSanitizer (TSan):检测数据竞争和线程死锁
嗯,这里要注意:TSan 和 ASan 不能同时启用,它们会互相干扰。
核心原则:Sanitizer 只在 Debug 或 RelWithDebInfo 配置下使用。Release 版本不要带 Sanitizer,会有性能开销。
在 CMake 里集成 ASan
先看 AddressSanitizer。我在项目中遇到过最头疼的就是堆内存越界,ASan 几乎是救命稻草。
CMake 里集成 ASan 其实就两步:加编译标志和链接标志。
# 设置 ASan 编译标志
set(ASAN_FLAGS "-fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ASAN_FLAGS}")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ASAN_FLAGS}")
# 设置 ASan 链接标志
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} ${ASAN_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} ${ASAN_FLAGS}")
为什么要加 -fno-omit-frame-pointer?因为 ASan 报错时需要回溯调用栈,这个标志能保留帧指针,让报错信息更完整。
小技巧:如果你用 Clang,还可以加 -fsanitize-address-use-after-scope,能检测栈上变量的 use-after-scope 问题。这个 GCC 不支持。
集成 UBSan
UBSan 我一般和 ASan 一起开。它俩不冲突,可以同时启用。
# 设置 UBSan 编译标志
set(UBSAN_FLAGS "-fsanitize=undefined -fno-sanitize-recover=all")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${UBSAN_FLAGS}")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${UBSAN_FLAGS}")
# 链接标志
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} ${UBSAN_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} ${UBSAN_FLAGS}")
-fno-sanitize-recover=all 这个参数很关键。默认情况下,UBSan 检测到未定义行为只会打印警告,程序继续跑。加上这个,它会直接 abort,让你不得不修。
我曾经在一个项目里没加这个参数,结果 UBSan 报了 200 多个警告,但程序一直没崩,团队就拖着不修。后来加上 abort,两天内全修完了。你想想看,这就是人性。
集成 TSan
TSan 单独用,不能和 ASan 混用。它需要所有依赖库都带上 TSan 编译,否则会有误报。
# 设置 TSan 编译标志
set(TSAN_FLAGS "-fsanitize=thread -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${TSAN_FLAGS}")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${TSAN_FLAGS}")
# 链接标志
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} ${TSAN_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} ${TSAN_FLAGS}")
警告:如果你链接了第三方静态库,而这些库没有用 TSan 编译,TSan 会报大量误报。解决办法是:要么全部重新编译,要么用 TSAN_OPTIONS="ignore_noninstrumented_modules=1" 忽略非检测模块。
封装成 CMake 函数
每次手动加标志太累了。我习惯封装成一个函数,按需调用。
function(target_enable_sanitizers target_name)
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo")
message(WARNING "Sanitizers should only be used in Debug/RelWithDebInfo builds")
return()
endif()
# 收集所有 Sanitizer 标志
set(SANITIZER_FLAGS "")
# ASan
if(SANITIZE_ADDRESS)
list(APPEND SANITIZER_FLAGS "-fsanitize=address" "-fno-omit-frame-pointer")
endif()
# UBSan
if(SANITIZE_UNDEFINED)
list(APPEND SANITIZER_FLAGS "-fsanitize=undefined" "-fno-sanitize-recover=all")
endif()
# TSan
if(SANITIZE_THREAD)
list(APPEND SANITIZER_FLAGS "-fsanitize=thread" "-fno-omit-frame-pointer")
endif()
# 应用到目标
target_compile_options(${target_name} PRIVATE ${SANITIZER_FLAGS})
target_link_options(${target_name} PRIVATE ${SANITIZER_FLAGS})
endfunction()
使用方式:
# 在 CMakeLists.txt 中
option(SANITIZE_ADDRESS "Enable AddressSanitizer" OFF)
option(SANITIZE_UNDEFINED "Enable UndefinedBehaviorSanitizer" OFF)
option(SANITIZE_THREAD "Enable ThreadSanitizer" OFF)
add_executable(my_app main.cpp)
target_enable_sanitizers(my_app)
然后编译时通过 -D 选项控制:
cmake -B build -DSANITIZE_ADDRESS=ON -DSANITIZE_UNDEFINED=ON
cmake --build build
Sanitizer 知识体系
下面这张图总结了 Sanitizer 的核心逻辑和集成流程:
避坑指南
最后分享几个我踩过的坑:
- ASan 和 TSan 冲突:我曾经同时开了 ASan 和 TSan,结果程序启动就崩,报错信息完全看不懂。后来查文档才知道它们共享一些内部数据结构,不能共存。
- UBSan 的 false positive:有些第三方库的代码确实有未定义行为,但你不方便改。可以用
-fsanitize-blacklist=path/to/file排除特定文件。 - TSan 的性能开销:TSan 会让程序慢 5-10 倍。我建议只在 CI 的测试阶段启用,不要在开发机上一整天开着。
- macOS 上的 ASan:macOS 的默认链接器不支持 ASan,需要加
-fsanitize=address -shared-libsan,或者用 LLVM 的 lld 链接器。
总结一下:Sanitizer 是 C++ 项目质量保障的利器。ASan 抓内存问题,UBSan 抓未定义行为,TSan 抓并发问题。把它们集成到 CMake 里,用条件编译控制开关,就能在开发阶段提前发现大量隐蔽 bug。
我个人习惯在项目的 CI 流程里,每天跑一次带 ASan+UBSan 的测试套件。效果很明显,线上崩溃率降了至少 60%。你也不妨试试看。