23、Sanitizer与代码质量:AddressSanitizer配置、UndefinedBehaviorSanitizer、静态分析集成、代码格式化
代码质量这个话题,说实话,很多团队都是嘴上重视、手上糊弄。我见过太多项目,功能跑通了就万事大吉,结果上线后各种段错误、内存泄漏、未定义行为轮番轰炸。嗯,今天我们就来聊聊怎么用CMake把这些「隐形炸弹」一个个拆掉。
Sanitizer是编译器内置的运行时检查工具,说白了就是让程序在运行过程中自己给自己做体检。我个人的习惯是:开发阶段必开AddressSanitizer和UndefinedBehaviorSanitizer,就像出门前必带钥匙一样自然。
AddressSanitizer配置
AddressSanitizer(简称ASan)是Google开发的工具,专门抓内存相关的bug。比如缓冲区溢出、use-after-free、内存泄漏这些,它都能在运行时精准定位。
配置起来其实很简单,在CMake里加几行就行:
# 开启AddressSanitizer
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
这里有个细节要注意:-fno-omit-frame-pointer是为了让报错信息里能看到完整的调用栈。我曾经遇到过不加这个选项,结果报错只给个地址,根本不知道是哪行代码出的问题,排查起来简直要命。
更好的做法是用CMake的target_compile_options,只对特定目标开启:
add_executable(my_app main.cpp)
target_compile_options(my_app PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-fsanitize=address>
$<$<CXX_COMPILER_ID:Clang>:-fsanitize=address>
)
target_link_options(my_app PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-fsanitize=address>
$<$<CXX_COMPILER_ID:Clang>:-fsanitize=address>
)
为什么要区分编译器?因为MSVC的ASan支持方式和GCC/Clang不一样。我踩过这个坑,直接给MSVC传-fsanitize=address,编译直接报错。
UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer(UBSan)专门抓C/C++里的未定义行为。比如整数溢出、空指针解引用、越界访问数组(这个ASan也能抓,但UBSan更细)。
配置方式和ASan类似:
target_compile_options(my_app PRIVATE
-fsanitize=undefined
-fno-sanitize-recover=all # 遇到未定义行为直接终止程序
)
target_link_options(my_app PRIVATE
-fsanitize=undefined
)
-fno-sanitize-recover=all这个选项很关键。默认情况下UBSan遇到未定义行为只会打印警告然后继续运行,这会导致问题被掩盖。我建议直接让它崩溃,这样你才能第一时间发现并修复。
举个例子,看看UBSan能抓到什么:
int main() {
int x = 1;
int shift = x << 31; // 左移31位,对于32位int是未定义行为
return 0;
}
运行后UBSan会报:runtime error: left shift of 1 by 31 places cannot be represented in type 'int'。你看,问题定位得清清楚楚。
静态分析集成
静态分析是在编译前检查代码,不需要运行程序。CMake可以集成clang-tidy和cppcheck这些工具。
配置clang-tidy的方式:
# 在CMakeLists.txt中
set(CMAKE_CXX_CLANG_TIDY "clang-tidy;--checks=*,-modernize-use-trailing-return-type")
# 或者用更细粒度的配置文件
set(CMAKE_CXX_CLANG_TIDY "clang-tidy;--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy")
然后在项目根目录放一个.clang-tidy文件:
Checks: '
clang-analyzer-*,
bugprone-*,
performance-*,
readability-*,
-readability-identifier-length
'
WarningsAsErrors: '*'
HeaderFilterRegex: '.*'
我个人习惯把WarningsAsErrors设为'*',这样所有静态分析警告都会变成编译错误。听起来很严格,但想想看:如果代码里有潜在bug,你是希望编译时就发现,还是等到线上出事故?
cppcheck的集成方式类似:
find_program(CPPCHECK cppcheck)
if(CPPCHECK)
add_custom_target(cppcheck
COMMAND ${CPPCHECK}
--enable=all
--suppress=missingIncludeSystem
--suppress=unmatchedSuppression
--error-exitcode=1
${CMAKE_SOURCE_DIR}/src
COMMENT "Running cppcheck static analysis..."
)
endif()
这样你只需要运行make cppcheck就能做一次完整的静态分析。
代码格式化
代码格式化看似小事,但团队协作时格式不统一,代码review简直是一场灾难。我见过因为缩进问题吵了半小时的团队,真的没必要。
CMake里集成clang-format:
find_program(CLANG_FORMAT clang-format)
if(CLANG_FORMAT)
file(GLOB_RECURSE ALL_SOURCE_FILES
src/*.cpp src/*.hpp include/*.hpp
)
add_custom_target(format
COMMAND ${CLANG_FORMAT}
-i -style=file
${ALL_SOURCE_FILES}
COMMENT "Formatting all source files..."
)
endif()
然后在项目根目录放.clang-format配置文件:
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: None
PointerAlignment: Left
运行make format就能一键格式化所有代码。我建议把这个命令加到pre-commit hook里,这样每个人提交代码前都会自动格式化。
下面这张图展示了这三道防线的关系:
最后说一句,这些工具不是装上了就完事。我见过有人开了ASan,但测试用例覆盖率不到10%,那基本等于没开。工具只是辅助,关键还是要有「写高质量代码」的意识。嗯,今天就聊到这里,希望你的项目从此告别那些莫名其妙的内存崩溃。