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-expression | sizeof 表达式错误 |
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
⚠️ 注意点
- Sanitizer 不能混用:ASan 和 TSan 不能同时使用
- 性能影响:ASan 约 2x 慢,TSan 约 5-15x 慢,MSan 约 3x 慢
- 内存开销:ASan 需要约 3x 内存,TSan 约 5-10x
- MSan 限制:必须用 Clang 编译,所有依赖也需要 MSan 编译
- Valgrind 不适合 TSan 替代:Valgrind helgrind 检测能力弱于 TSan
- 覆盖率≠测试质量:高覆盖率不意味着测试有效
💡 提示
- 开发时默认启用 ASan:
-fsanitize=address 应成为开发环境默认选项 - UBSan 安全启用:
-fsanitize=undefined 开销小,可安全用于生产环境 - Clang-Tidy 在 CI 中强制:将关键规则设为
WarningsAsErrors - Valgrind 适合无源码场景:不需要重新编译即可检测
- 抑制已知问题:使用 suppressions 文件管理已知的第三方库问题
- 定期运行完整检查:每周在 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
扩展阅读