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,编译直接报错。

小提示:ASan开启后程序运行会变慢2-5倍,内存占用也会增加。所以只建议在Debug或RelWithDebInfo配置下开启,Release版本就别用了。

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'。你看,问题定位得清清楚楚。

注意:ASan和UBSan可以同时开启,但要注意链接顺序。我建议先单独测试ASan,再叠加UBSan,这样出问题好排查。

静态分析集成

静态分析是在编译前检查代码,不需要运行程序。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里,这样每个人提交代码前都会自动格式化。

核心思路:Sanitizer、静态分析、代码格式化这三者构成了代码质量的「三道防线」。Sanitizer抓运行时问题,静态分析抓编译前问题,格式化保证代码风格统一。缺一个都不够稳。

下面这张图展示了这三道防线的关系:

代码质量三道防线 第一道防线 代码格式化 clang-format 统一风格,减少review噪音 第二道防线 静态分析 clang-tidy / cppcheck 编译前发现潜在bug 第三道防线 运行时Sanitizer ASan / UBSan 运行时精准定位bug 集成到CMake构建流程 开发阶段 → 开启ASan+UBSan(Debug模式) 提交前 → 运行clang-tidy + clang-format CI/CD → 全部开启,任何警告视为错误

最后说一句,这些工具不是装上了就完事。我见过有人开了ASan,但测试用例覆盖率不到10%,那基本等于没开。工具只是辅助,关键还是要有「写高质量代码」的意识。嗯,今天就聊到这里,希望你的项目从此告别那些莫名其妙的内存崩溃。

公众号:蓝海资料掘金营,微信deep3321