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

C/C++ Linux 开发教程(GCC + CMake) / 代码质量(Sanitizer/Valgrind/Clang-Tidy)

代码质量(Sanitizer/Valgrind/Clang-Tidy)

C/C++ 代码中的内存错误、数据竞争和未定义行为是最常见的 Bug 来源。本文介绍如何使用 Sanitizer、Valgrind、Clang-Tidy 等工具系统性地检测和预防这些问题。

AddressSanitizer (ASan)

基本使用

# 编译时启用 ASan
gcc -fsanitize=address -fno-omit-frame-pointer -g -O1 leaky.c -o leaky

# 运行程序,ASan 会自动报告内存错误
./leaky

检测的错误类型

错误类型说明示例
Heap-buffer-overflow堆缓冲区溢出malloc(10); p[10] = 1;
Stack-buffer-overflow栈缓冲区溢出int a[10]; a[10] = 1;
Use-after-free释放后使用free(p); *p = 1;
Use-after-return返回后使用栈变量函数返回后引用局部变量
Use-after-scope离开作用域后使用{int x; ptr=&x;} *ptr=1;
Double-free重复释放free(p); free(p);
Memory-leak内存泄漏malloc(100); // never freed

ASan 示例代码

// asan_demo.c
#include <stdlib.h>
#include <stdio.h>

// 1. 堆缓冲区溢出
void heap_overflow() {
    int *arr = (int *)malloc(5 * sizeof(int));
    arr[5] = 42;  // 越界!索引 5 超出了 [0..4] 的范围
    free(arr);
}

// 2. Use-after-free
void use_after_free() {
    int *p = (int *)malloc(sizeof(int));
    *p = 10;
    free(p);
    printf("%d\n", *p);  // 已释放!
}

// 3. 栈缓冲区溢出
void stack_overflow() {
    char buf[10];
    sprintf(buf, "This string is way too long for the buffer");  // 溢出!
}

// 4. 内存泄漏
void memory_leak() {
    int *p = (int *)malloc(100 * sizeof(int));
    p[0] = 1;
    // 没有 free(p)
}

// 5. Double-free
void double_free() {
    int *p = (int *)malloc(sizeof(int));
    free(p);
    free(p);  // 重复释放!
}

int main() {
    heap_overflow();
    use_after_free();
    stack_overflow();
    memory_leak();
    double_free();
    return 0;
}
# 编译并运行
gcc -fsanitize=address -fno-omit-frame-pointer -g -O0 asan_demo.c -o asan_demo
./asan_demo

# 输出示例:
# =================================================================
# ==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
# ...
# SUMMARY: AddressSanitizer: heap-buffer-overflow asan_demo.c:7 in heap_overflow

ASan 环境变量

# 设置检测选项
export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1"

# 常用选项:
# detect_leaks=1        — 启用内存泄漏检测(Linux 默认启用)
# halt_on_error=0       — 检测到错误后继续运行
# print_stats=1         — 打印统计信息
# detect_stack_use_after_return=1  — 检测栈使用
# allocator_may_return_null=1      — malloc 失败返回 NULL
# log_path=asan.log     — 将报告写入文件

# 运行时抑制已知问题
LSAN_OPTIONS="suppressions=lsan_suppressions.txt" ./asan_demo
# lsan_suppressions.txt — 泄漏抑制文件
leak:third_party_library_init
leak:libsome_legacy_code.so

ASan 与 CMake 集成

option(ENABLE_ASAN "Enable AddressSanitizer" OFF)

if(ENABLE_ASAN)
    add_compile_options(
        -fsanitize=address
        -fno-omit-frame-pointer
        -fno-optimize-sibling-calls
    )
    add_link_options(-fsanitize=address)
endif()
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
cmake --build build

ThreadSanitizer (TSan)

检测数据竞争

// tsan_demo.c
#include <pthread.h>
#include <stdio.h>

int shared_counter = 0;  // 共享变量,无保护

void *increment(void *arg) {
    for (int i = 0; i < 100000; i++) {
        shared_counter++;  // 数据竞争!
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Counter: %d (expected: 200000)\n", shared_counter);
    return 0;
}
# 编译并运行(TSan 不能与 ASan 同时使用)
gcc -fsanitize=thread -g -O1 tsan_demo.c -lpthread -o tsan_demo
./tsan_demo

# 输出示例:
# ==================
# WARNING: ThreadSanitizer: data race (pid=12345)
#   Write of size 4 at 0x... by thread T1:
#     #0 increment tsan_demo.c:9
#   Previous write of size 4 at 0x... by thread T2:
#     #0 increment tsan_demo.c:9
# ==================

修复数据竞争

// tsan_fixed.c
#include <pthread.h>
#include <stdio.h>

int shared_counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *increment(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        shared_counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Counter: %d\n", shared_counter);
    pthread_mutex_destroy(&mutex);
    return 0;
}

TSan 选项

export TSAN_OPTIONS="history_size=7:second_deadlock_stack=1:print_suppressions=0"

# 常用选项:
# history_size=7        — 历史记录大小(增加可发现更多问题)
# second_deadlock_stack=1 — 死锁检测时打印两个栈
# suppressions=tsan_suppressions.txt — 抑制已知问题

UndefinedBehaviorSanitizer (UBSan)

检测未定义行为

// ubsan_demo.c
#include <stdio.h>
#include <limits.h>

// 1. 整数溢出
void integer_overflow() {
    int x = INT_MAX;
    int y = x + 1;  // 有符号整数溢出!
    printf("%d\n", y);
}

// 2. 空指针解引用
void null_deref() {
    int *p = NULL;
    *p = 42;  // 空指针解引用!
}

// 3. 除以零
void divide_by_zero() {
    int a = 1;
    int b = 0;
    int c = a / b;  // 除以零!
}

// 4. 数组越界(有限支持)
void array_oob() {
    int arr[5] = {1, 2, 3, 4, 5};
    int x = arr[10];  // 越界!
    printf("%d\n", x);
}

// 5. 对齐问题
void misaligned_access() {
    char buf[10];
    int *p = (int *)(buf + 1);  // 未对齐的指针
    *p = 42;
}

int main() {
    integer_overflow();
    // null_deref();     // 会导致程序终止
    // divide_by_zero();  // 会导致程序终止
    return 0;
}
# 编译并运行
gcc -fsanitize=undefined -g -O1 ubsan_demo.c -o ubsan_demo
./ubsan_demo

# 输出示例:
# ubsan_demo.c:8:15: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented

UBSan 选项

export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"

# 选项:
# print_stacktrace=1    — 打印完整栈信息
# halt_on_error=1       — 遇到未定义行为立即终止
# suppressions=ubsan_suppressions.txt

MemorySanitizer (MSan)

检测未初始化内存读取

// msan_demo.c
#include <stdio.h>

int main() {
    int x;  // 未初始化
    if (x > 0) {  // 读取未初始化的值!
        printf("positive\n");
    }
    return 0;
}
# ⚠️ MSan 只能用 Clang 编译
clang -fsanitize=memory -fno-omit-frame-pointer -g -O1 msan_demo.c -o msan_demo
./msan_demo

# 输出:
# ==12345==WARNING: MemorySanitizer: use-of-uninitialized-value
#     #0 main msan_demo.c:5

⚠️ MSan 不能与 ASan 或 TSan 同时使用。MSan 要求所有依赖库也使用 MSan 编译。

Sanitizer 组合使用

Sanitizer组合 ASan组合 TSan组合 UBSan
ASan
TSan
UBSan
MSan
# 同时启用 ASan + UBSan
gcc -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 demo.c -o demo

# 同时启用 TSan + UBSan
gcc -fsanitize=thread,undefined -g -O1 demo.c -lpthread -o demo

Valgrind

Valgrind 套件概览

工具用途说明
memcheck内存错误检测默认工具,最常用
callgrind函数调用分析性能分析
cachegrind缓存命中分析缓存优化
helgrind线程错误检测类似 TSan
drd线程错误检测另一个线程检测器
massif堆内存分析内存使用可视化

Valgrind Memcheck

# 基本使用
valgrind --leak-check=full --show-leak-kinds=all ./app

# 完整选项
valgrind \
    --tool=memcheck \
    --leak-check=full \
    --show-leak-kinds=all \
    --track-origins=yes \
    --verbose \
    --log-file=valgrind.log \
    ./app

# 查看输出
cat valgrind.log

Memcheck 输出解读

==12345== HEAP SUMMARY:
==12345==     in use at exit: 1,024 bytes in 2 blocks
==12345==   total heap usage: 10 allocs, 8 frees, 4,096 bytes allocated
==12345==
==12345== 512 bytes in 1 blocks are definitely lost in loss record 1 of 2
==12345==    at 0x...: malloc (vg_replace_malloc.c:...)
==12345==    by 0x...: main (demo.c:10)

泄漏类型说明:

类型含义严重程度
Definitely lost确定泄漏🔴 严重
Indirectly lost间接泄漏🔴 严重
Possibly lost可能泄漏🟡 注意
Still reachable仍可访问🟢 低

Valgrind Callgrind

# 运行 callgrind
valgrind --tool=callgrind ./app

# 生成了 callgrind.out.12345 文件

# 使用 KCachegrind 可视化
kcachegrind callgrind.out.12345

# 或命令行分析
callgrind_annotate callgrind.out.12345

Valgrind Cachegrind

# 运行 cachegrind
valgrind --tool=cachegrind ./app

# 分析输出
cg_annotate cachegrind.out.12345

# 可视化
kcachegrind cachegrind.out.12345

Valgrind Helgrind

# 检测线程错误
valgrind --tool=helgrind ./threaded_app

# 检测死锁和数据竞争
valgrind --tool=helgrind --history-level=full ./threaded_app

Clang-Tidy

基本使用

# 分析单个文件
clang-tidy main.c -- -I./include -Wall

# 使用编译数据库(推荐)
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
clang-tidy -p build src/main.c

# 批量检查
run-clang-tidy -p build src/

配置文件 .clang-tidy

# .clang-tidy
---
Checks: >
  -*,
  bugprone-*,
  cert-*,
  clang-analyzer-*,
  concurrency-*,
  cppcoreguidelines-*,
  google-*,
  misc-*,
  modernize-*,
  performance-*,
  readability-*,
  -modernize-use-trailing-return-type,
  -readability-magic-numbers

WarningsAsErrors: >
  bugprone-*,
  clang-analyzer-*,
  concurrency-*,
  cppcoreguidelines-owning-memory,
  cppcoreguidelines-prefer-member-initializer

HeaderFilterRegex: 'src/.*'

CheckOptions:
  - key: readability-identifier-naming.ClassCase
    value: CamelCase
  - key: readability-identifier-naming.FunctionCase
    value: camelBack
  - key: readability-identifier-naming.VariableCase
    value: camelBack
  - key: readability-identifier-naming.ConstantCase
    value: UPPER_CASE
  - key: readability-identifier-naming.MemberPrefix
    value: m_
  - key: misc-include-order
    value: 'llvm'
...

常用检查规则

规则说明
bugprone-use-after-move移动后使用
bugprone-dangling-handle悬垂引用
bugprone-sizeof-expressionsizeof 表达式错误
cert-dcl50-cpp不可移植的变参函数
performance-unnecessary-copy不必要的拷贝
modernize-use-auto建议使用 auto
modernize-use-nullptr建议使用 nullptr
readability-braces-around-statements建议添加花括号

Clang-Tidy 修复

# 自动修复
clang-tidy -p build --fix src/main.c

# 只修复特定检查
clang-tidy -p build --fix \
    -checks='-*,modernize-use-nullptr' \
    src/main.c

# 预览修复(不实际应用)
clang-tidy -p build --fix-notes src/main.c

Cppcheck 静态分析

# 基本使用
cppcheck --enable=all --std=c11 src/

# 使用编译数据库
cppcheck --project=build/compile_commands.json

# 输出到文件
cppcheck --enable=all --xml --xml-version=2 src/ 2> cppcheck.xml

# 与 CMake 集成
# CMakeLists.txt
find_program(CPPCHECK cppcheck)
set(CMAKE_C_CPPCHECK ${CPPCHECK} --enable=all --std=c11)

Cppcheck 常用选项

选项说明
--enable=all启用所有检查
--enable=warning只启用警告
--std=c11指定 C 标准
--suppress=memleak抑制特定警告
--inconclusive报告不确定的问题
--force强制检查所有配置

代码覆盖率

gcov + lcov

# 编译时启用覆盖率
gcc --coverage -g -O0 main.c utils.c -o app

# 运行程序生成 .gcda 文件
./app

# 使用 gcov 查看
gcov main.c utils.c

# 使用 lcov 生成 HTML 报告
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

# 浏览报告
xdg-open coverage_report/index.html

CMake 集成覆盖率

option(ENABLE_COVERAGE "Enable code coverage" OFF)

if(ENABLE_COVERAGE)
    add_compile_options(--coverage -fprofile-arcs -ftest-coverage)
    add_link_options(--coverage)
endif()
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
cmake --build build
cd build && ctest
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

CMake 集成 Sanitizer

完整 Sanitizer 配置

# cmake/Sanitizers.cmake
function(enable_sanitizers target)
    option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
    option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
    option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
    option(ENABLE_MSAN "Enable MemorySanitizer" OFF)
    
    if(ENABLE_ASAN)
        message(STATUS "Enabling AddressSanitizer for ${target}")
        target_compile_options(${target} PRIVATE
            -fsanitize=address
            -fno-omit-frame-pointer
            -fno-optimize-sibling-calls
        )
        target_link_options(${target} PRIVATE -fsanitize=address)
    endif()
    
    if(ENABLE_TSAN)
        message(STATUS "Enabling ThreadSanitizer for ${target}")
        target_compile_options(${target} PRIVATE
            -fsanitize=thread
            -fno-omit-frame-pointer
        )
        target_link_options(${target} PRIVATE -fsanitize=thread)
    endif()
    
    if(ENABLE_UBSAN)
        message(STATUS "Enabling UndefinedBehaviorSanitizer for ${target}")
        target_compile_options(${target} PRIVATE
            -fsanitize=undefined
            -fno-omit-frame-pointer
        )
        target_link_options(${target} PRIVATE -fsanitize=undefined)
    endif()
    
    if(ENABLE_MSAN)
        if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
            message(WARNING "MSan only works with Clang")
        else()
            message(STATUS "Enabling MemorySanitizer for ${target}")
            target_compile_options(${target} PRIVATE
                -fsanitize=memory
                -fno-omit-frame-pointer
                -fPIE
            )
            target_link_options(${target} PRIVATE -fsanitize=memory)
        endif()
    endif()
endfunction()
# 使用
include(cmake/Sanitizers.cmake)
add_executable(app src/main.c)
enable_sanitizers(app)

代码质量 CI 流水线

GitHub Actions 配置

# .github/workflows/quality.yml
name: Code Quality

on: [push, pull_request]

jobs:
  sanitizers:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        sanitizer: [asan, tsan, ubsan]
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: sudo apt-get install -y cmake build-essential
      - name: Build with ${{ matrix.sanitizer }}
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Debug \
            -DENABLE_${SANITIZER^^}=ON
          cmake --build build
        env:
          SANITIZER: ${{ matrix.sanitizer }}
      - name: Run tests
        run: cd build && ctest --output-on-failure
  
  static-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install tools
        run: |
          sudo apt-get install -y clang-tidy cppcheck
          pip install compiledb
      - name: Generate compile_commands.json
        run: |
          cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
      - name: Run clang-tidy
        run: run-clang-tidy -p build src/
      - name: Run cppcheck
        run: cppcheck --project=build/compile_commands.json --enable=all
  
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build with coverage
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
          cmake --build build
      - name: Run tests
        run: cd build && ctest
      - name: Generate coverage
        run: |
          cd build
          lcov --capture --directory . --output-file coverage.info
          lcov --remove coverage.info '/usr/*' '*/test/*' -o coverage.info
          genhtml coverage.info --output-directory coverage_report
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: build/coverage_report/

Valgrind CI 集成

  valgrind:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install valgrind
        run: sudo apt-get install -y valgrind
      - name: Build
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Debug
          cmake --build build
      - name: Run with valgrind
        run: |
          cd build
          ctest -T memcheck --output-on-failure
          # 或手动:
          # valgrind --leak-check=full --error-exitcode=1 ./test_app

⚠️ 注意点

  1. Sanitizer 不能混用:ASan 和 TSan 不能同时使用
  2. 性能影响:ASan 约 2x 慢,TSan 约 5-15x 慢,MSan 约 3x 慢
  3. 内存开销:ASan 需要约 3x 内存,TSan 约 5-10x
  4. MSan 限制:必须用 Clang 编译,所有依赖也需要 MSan 编译
  5. Valgrind 不适合 TSan 替代:Valgrind helgrind 检测能力弱于 TSan
  6. 覆盖率≠测试质量:高覆盖率不意味着测试有效

💡 提示

  1. 开发时默认启用 ASan-fsanitize=address 应成为开发环境默认选项
  2. UBSan 安全启用-fsanitize=undefined 开销小,可安全用于生产环境
  3. Clang-Tidy 在 CI 中强制:将关键规则设为 WarningsAsErrors
  4. Valgrind 适合无源码场景:不需要重新编译即可检测
  5. 抑制已知问题:使用 suppressions 文件管理已知的第三方库问题
  6. 定期运行完整检查:每周在 CI 中运行所有 sanitizer + 静态分析

工程场景

场景 1:排查内存泄漏

# 步骤 1: 用 ASan 快速检测
gcc -fsanitize=address -g leaky.c -o leaky
./leaky 2>&1 | grep "leak"

# 步骤 2: 如果 ASan 不够,用 Valgrind 精确定位
valgrind --leak-check=full --show-leak-kinds=all ./leaky

# 步骤 3: 用 Massif 分析内存使用模式
valgrind --tool=massif ./leaky
ms_print massif.out.12345

场景 2:排查数据竞争

# 步骤 1: TSan 检测
gcc -fsanitize=thread -g threaded.c -lpthread -o threaded
./threaded

# 步骤 2: Helgrind 交叉验证
valgrind --tool=helgrind ./threaded

# 步骤 3: 静态分析检查
clang-tidy -p build src/threaded.c --checks='concurrency-*'

场景 3:代码审查自动化

# 本地审查脚本
#!/bin/bash
set -e

echo "=== Static Analysis ==="
clang-tidy -p build $(git diff --name-only HEAD~1 -- '*.c' '*.cpp')

echo "=== Sanitizer Test ==="
cmake -B build-asan -DENABLE_ASAN=ON
cmake --build build-asan
cd build-asan && ctest --output-on-failure

echo "=== Coverage ==="
cmake -B build-cov -DENABLE_COVERAGE=ON
cmake --build build-cov
cd build-cov && ctest
lcov --capture --directory . -o coverage.info
genhtml coverage.info -o coverage_report

扩展阅读