name: 覆盖率分析 type: technique description: > 覆盖率分析测量在模糊测试期间执行的代码。 用于评估测试工具的有效性或识别模糊测试障碍。
覆盖率分析
覆盖率分析对于理解在模糊测试期间代码的哪些部分被执行至关重要。它有助于识别模糊测试障碍,如魔数检查,并跟踪测试工具改进随时间的效果。
概述
模糊测试期间的代码覆盖率有两个关键目的:
- 评估测试工具的有效性:了解应用程序的哪些部分实际上被测试工具执行
- 跟踪模糊测试进展:监控当更新测试工具、模糊器或系统下测试(SUT)时覆盖率的变化
覆盖率是模糊器能力和性能的代理。虽然覆盖率在绝对意义上不是衡量模糊器性能的理想指标,但它可靠地指示您的测试工具在给定设置中是否有效工作。
关键概念
| 概念 | 描述 |
|---|---|
| 覆盖率仪器 | 跟踪执行代码路径的编译器标志 |
| 语料库覆盖率 | 通过运行模糊测试语料库中的所有测试用例实现的覆盖率 |
| 魔数检查 | 难以发现的条件检查,阻碍模糊器进展 |
| 覆盖率引导的模糊测试 | 优先发现新代码路径的输入模糊测试策略 |
| 覆盖率报告 | 执行与未执行代码的可视化或文本表示 |
何时应用
应用此技术时:
- 开始新的模糊测试活动以建立基准
- 模糊器似乎达到平台期而没有找到新路径
- 在测试工具修改后验证改进
- 当在不同模糊器之间迁移时
- 识别需要字典条目或种子输入的区域
- 调试为什么某些代码路径未达到
跳过此技术时:
- 模糊测试活动正在积极发现崩溃
- 覆盖率基础设施尚未设置
- 处理极大的代码库,其中完整覆盖率报告不实用
- 模糊器的内部覆盖率指标足以满足您的需求
快速参考
| 任务 | 命令/模式 |
|---|---|
| LLVM 覆盖率仪器(C/C++) | -fprofile-instr-generate -fcoverage-mapping |
| GCC 覆盖率仪器 | -ftest-coverage -fprofile-arcs |
| cargo-fuzz 覆盖率(Rust) | cargo +nightly fuzz coverage <target> |
| 生成 LLVM 配置文件数据 | llvm-profdata merge -sparse file.profraw -o file.profdata |
| LLVM 覆盖率报告 | llvm-cov report ./binary -instr-profile=file.profdata |
| LLVM HTML 报告 | llvm-cov show ./binary -instr-profile=file.profdata -format=html -output-dir html/ |
| gcovr HTML 报告 | gcovr --html-details -o coverage.html |
理想覆盖率工作流
以下工作流表示将覆盖率分析集成到模糊测试活动中的最佳实践:
[模糊测试活动]
|
v
[生成语料库]
|
v
[覆盖率分析]
|
+---> 覆盖率增加? --> 继续模糊测试,使用更大的语料库
|
+---> 覆盖率减少? --> 修复测试工具或调查 SUT 变化
|
+---> 达到平台期? --> 添加字典条目或种子输入
关键原则:使用每个模糊测试活动后生成的语料库来计算覆盖率,而不是实时模糊器统计数据。这种方法提供了可重复、可比较的测量,跨越不同的模糊测试工具。
分步指南
步骤 1:使用覆盖率仪器构建
根据工具链选择仪器方法:
LLVM/Clang(C/C++):
clang++ -fprofile-instr-generate -fcoverage-mapping \
-O2 -DNO_MAIN \
main.cc harness.cc execute-rt.cc -o fuzz_exec
GCC(C/C++):
g++ -ftest-coverage -fprofile-arcs \
-O2 -DNO_MAIN \
main.cc harness.cc execute-rt.cc -o fuzz_exec_gcov
Rust:
rustup toolchain install nightly --component llvm-tools-preview
cargo +nightly fuzz coverage fuzz_target_1
步骤 2:创建执行运行时(仅 C/C++)
对于 C/C++ 项目,创建一个执行语料库的运行时:
// execute-rt.cc
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <stdint.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
void load_file_and_test(const char *filename) {
FILE *file = fopen(filename, "rb");
if (file == NULL) {
printf("Failed to open file: %s
", filename);
return;
}
fseek(file, 0, SEEK_END);
long filesize = ftell(file);
rewind(file);
uint8_t *buffer = (uint8_t*) malloc(filesize);
if (buffer == NULL) {
printf("Failed to allocate memory for file: %s
", filename);
fclose(file);
return;
}
long read_size = (long) fread(buffer, 1, filesize, file);
if (read_size != filesize) {
printf("Failed to read file: %s
", filename);
free(buffer);
fclose(file);
return;
}
LLVMFuzzerTestOneInput(buffer, filesize);
free(buffer);
fclose(file);
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s <directory>
", argv[0]);
return 1;
}
DIR *dir = opendir(argv[1]);
if (dir == NULL) {
printf("Failed to open directory: %s
", argv[1]);
return 1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG) {
char filepath[1024];
snprintf(filepath, sizeof(filepath), "%s/%s", argv[1], entry->d_name);
load_file_and_test(filepath);
}
}
closedir(dir);
return 0;
}
步骤 3:在语料库上执行
LLVM(C/C++):
LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec corpus/
GCC(C/C++):
./fuzz_exec_gcov corpus/
Rust:
覆盖率数据在运行 cargo fuzz coverage 时自动生成。
步骤 4:处理覆盖率数据
LLVM:
# 合并原始配置文件数据
llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata
# 生成文本报告
llvm-cov report ./fuzz_exec \
-instr-profile=fuzz.profdata \
-ignore-filename-regex='harness.cc|execute-rt.cc'
# 生成 HTML 报告
llvm-cov show ./fuzz_exec \
-instr-profile=fuzz.profdata \
-ignore-filename-regex='harness.cc|execute-rt.cc' \
-format=html -output-dir fuzz_html/
GCC with gcovr:
# 安装 gcovr(通过 pip 获取最新版本)
python3 -m venv venv
source venv/bin/activate
pip3 install gcovr
# 生成报告
gcovr --gcov-executable "llvm-cov gcov" \
--exclude harness.cc --exclude execute-rt.cc \
--root . --html-details -o coverage.html
Rust:
# 安装所需工具
cargo install cargo-binutils rustfilt
# 创建 HTML 生成脚本
cat <<'EOF' > ./generate_html
#!/bin/sh
if [ $# -lt 1 ]; then
echo "Error: Name of fuzz target is required."
echo "Usage: $0 fuzz_target [sources...]"
exit 1
fi
FUZZ_TARGET="$1"
shift
SRC_FILTER="$@"
TARGET=$(rustc -vV | sed -n 's|host: ||p')
cargo +nightly cov -- show -Xdemangler=rustfilt \
"target/$TARGET/coverage/$TARGET/release/$FUZZ_TARGET" \
-instr-profile="fuzz/coverage/$FUZZ_TARGET/coverage.profdata" \
-show-line-counts-or-regions -show-instantiations \
-format=html -o fuzz_html/ $SRC_FILTER
EOF
chmod +x ./generate_html
# 生成 HTML 报告
./generate_html fuzz_target_1 src/lib.rs
步骤 5:分析结果
审查覆盖率报告以识别:
- 未覆盖的代码块:可能需要更好的种子输入或字典条目的区域
- 魔数检查:具有硬编码值的条件语句,阻碍进展
- 死代码:可能无法通过测试工具访问的函数
- 覆盖率变化:与基线比较以跟踪改进或回归
常见模式
模式:识别魔数值
问题:模糊器无法发现由魔数检查保护的路径。
覆盖率显示:
// 覆盖率显示此块从未执行
if (buf == 0x7F454C46) { // ELF 魔数
// 开始解析 buf
}
解决方案:将魔数值添加到字典文件:
# magic.dict
"\x7F\x45\x4C\x46"
模式:处理崩溃输入
问题:当语料库包含崩溃输入时,覆盖率生成失败。
之前:
./fuzz_exec corpus/ # 在坏输入上崩溃,无覆盖率生成
之后:
// 在执行前 fork 以隔离崩溃
int main(int argc, char **argv) {
// ... 目录打开代码 ...
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG) {
pid_t pid = fork();
if (pid == 0) {
// 子进程 - 崩溃不会影响父进程
char filepath[1024];
snprintf(filepath, sizeof(filepath), "%s/%s", argv[1], entry->d_name);
load_file_and_test(filepath);
exit(0);
} else {
// 父进程等待子进程
waitpid(pid, NULL, 0);
}
}
}
}
模式:CMake 集成
用例:将覆盖率构建添加到 CMake 项目。
project(FuzzingProject)
cmake_minimum_required(VERSION 3.0)
# 主二进制文件
add_executable(program main.cc)
# 模糊测试二进制文件
add_executable(fuzz main.cc harness.cc)
target_compile_definitions(fuzz PRIVATE NO_MAIN=1)
target_compile_options(fuzz PRIVATE -g -O2 -fsanitize=fuzzer)
target_link_libraries(fuzz -fsanitize=fuzzer)
# 覆盖率执行二进制文件
add_executable(fuzz_exec main.cc harness.cc execute-rt.cc)
target_compile_definitions(fuzz_exec PRIVATE NO_MAIN)
target_compile_options(fuzz_exec PRIVATE -O2 -fprofile-instr-generate -fcoverage-mapping)
target_link_libraries(fuzz_exec -fprofile-instr-generate)
构建:
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .
cmake --build . --target fuzz_exec
高级用法
提示与技巧
| 提示 | 为什么有帮助 |
|---|---|
使用 LLVM 18+ 的 -show-directory-coverage |
按目录结构组织大报告,而不是扁平文件列表 |
| 导出到 lcov 格式以获得更好的 HTML | llvm-cov export -format=lcov + genhtml 提供更清晰的每文件报告 |
| 比较不同活动的覆盖率 | 存储带时间戳的 .profdata 文件以跟踪随时间进展 |
| 从报告中过滤测试工具代码 | 使用 -ignore-filename-regex 专注于 SUT 覆盖率 |
| 在 CI/CD 中自动化覆盖率 | 在计划模糊测试运行后自动生成覆盖率报告 |
| 对 Clang 14+ 使用 gcovr 5.1+ | 较旧的 gcovr 版本与最近的 LLVM 有兼容性问题 |
增量覆盖率更新
GCC 的 gcov 仪器在多次运行中增量更新 .gcda 文件。这对于在添加测试用例时跟踪覆盖率很有用:
# 第一次运行
./fuzz_exec_gcov corpus_batch_1/
gcovr --html coverage_v1.html
# 第二次运行(添加到现有覆盖率)
./fuzz_exec_gcov corpus_batch_2/
gcovr --html coverage_v2.html
# 重新开始
gcovr --delete # 删除 .gcda 文件
./fuzz_exec_gcov corpus/
处理大型代码库
对于有数百个源文件的项目:
-
按前缀过滤:仅为相关目录生成报告
llvm-cov show ./fuzz_exec -instr-profile=fuzz.profdata /path/to/src/ -
使用目录覆盖率:按目录分组以减少混乱(LLVM 18+)
llvm-cov show -show-directory-coverage -format=html -output-dir html/ -
生成 JSON 用于程序化分析:
llvm-cov export -format=lcov > coverage.json
差异覆盖率
比较两个模糊测试活动之间的覆盖率:
# 活动 1
LLVM_PROFILE_FILE=campaign1.profraw ./fuzz_exec corpus1/
llvm-profdata merge -sparse campaign1.profraw -o campaign1.profdata
# 活动 2
LLVM_PROFILE_FILE=campaign2.profraw ./fuzz_exec corpus2/
llvm-profdata merge -sparse campaign2.profraw -o campaign2.profdata
# 比较
llvm-cov show ./fuzz_exec \
-instr-profile=campaign2.profdata \
-instr-profile=campaign1.profdata \
-show-line-counts-or-regions
反模式
| 反模式 | 问题 | 正确方法 |
|---|---|---|
| 使用模糊器报告的覆盖率进行比较 | 不同的模糊器计算覆盖率不同,跨工具比较无意义 | 使用专用覆盖率工具(llvm-cov、gcovr)进行可重复测量 |
| 使用优化生成覆盖率 | -O3 优化可以消除代码,使覆盖率误导 |
使用 -O2 或 -O0 进行覆盖率构建 |
| 不过滤测试工具代码 | 测试工具覆盖率夸大数字并掩盖 SUT 覆盖率 | 使用 -ignore-filename-regex 或 --exclude 过滤测试工具文件 |
| 混合 LLVM 和 GCC 仪器 | 不兼容格式导致解析失败 | 对覆盖率构建坚持一个工具链 |
| 忽略崩溃输入 | 崩溃阻止覆盖率生成,隐藏真实覆盖率数据 | 首先修复崩溃,或使用进程 forking 隔离它们 |
| 不跟踪随时间覆盖率 | 一次性覆盖率检查错过回归和改进 | 存储带时间戳的覆盖率数据并跟踪趋势 |
工具特定指导
libFuzzer
libFuzzer 默认使用 LLVM 的 SanitizerCoverage 引导模糊测试,但需要单独的仪器生成报告。
构建覆盖率:
clang++ -fprofile-instr-generate -fcoverage-mapping \
-O2 -DNO_MAIN \
main.cc harness.cc execute-rt.cc -o fuzz_exec
执行语料库并生成报告:
LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec corpus/
llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata
llvm-cov show ./fuzz_exec -instr-profile=fuzz.profdata -format=html -output-dir html/
集成提示:
- 不要对覆盖率构建使用
-fsanitize=fuzzer(与配置文件仪器冲突) - 重用相同的测试工具函数(
LLVMFuzzerTestOneInput),但使用不同的主函数 - 使用
-ignore-filename-regex标志从覆盖率报告中排除测试工具代码 - 对于模板繁重的 C++ 代码,考虑使用 llvm-cov 的
-show-instantiation标志
AFL++
AFL++ 提供自己的覆盖率反馈机制,但对于详细报告使用标准 LLVM/GCC 工具。
使用 LLVM 构建覆盖率:
clang++ -fprofile-instr-generate -fcoverage-mapping \
-O2 main.cc harness.cc execute-rt.cc -o fuzz_exec
使用 GCC 构建覆盖率:
AFL_USE_ASAN=0 afl-gcc -ftest-coverage -fprofile-arcs \
main.cc harness.cc execute-rt.cc -o fuzz_exec_gcov
执行并生成报告:
# LLVM 方法
LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec afl_output/queue/
llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata
llvm-cov report ./fuzz_exec -instr-profile=fuzz.profdata
# GCC 方法
./fuzz_exec_gcov afl_output/queue/
gcovr --html-details -o coverage.html
集成提示:
- 不要对覆盖率构建使用 AFL++ 的仪器(
afl-clang-fast) - 改用带覆盖率标志的标准编译器
- AFL++ 的
queue/目录包含语料库 - AFL++ 的内置覆盖率统计对实时监控有用,但不适合详细分析
cargo-fuzz(Rust)
cargo-fuzz 使用 LLVM 工具提供内置覆盖率生成。
安装先决条件:
rustup toolchain install nightly --component llvm-tools-preview
cargo install cargo-binutils rustfilt
生成覆盖率数据:
cargo +nightly fuzz coverage fuzz_target_1
创建 HTML 报告脚本:
cat <<'EOF' > ./generate_html
#!/bin/sh
FUZZ_TARGET="$1"
shift
SRC_FILTER="$@"
TARGET=$(rustc -vV | sed -n 's|host: ||p')
cargo +nightly cov -- show -Xdemangler=rustfilt \
"target/$TARGET/coverage/$TARGET/release/$FUZZ_TARGET" \
-instr-profile="fuzz/coverage/$FUZZ_TARGET/coverage.profdata" \
-show-line-counts-or-regions -show-instantiations \
-format=html -o fuzz_html/ $SRC_FILTER
EOF
chmod +x ./generate_html
生成报告:
./generate_html fuzz_target_1 src/lib.rs
集成提示:
- 始终对覆盖率使用 nightly 工具链
-Xdemangler=rustfilt标志使函数名可读- 按源文件过滤(例如,
src/lib.rs)以专注于 crate 代码 - 使用
-show-line-counts-or-regions和-show-instantiations获得更好的 Rust 特定输出 - 语料库位于
fuzz/corpus/<target>/
honggfuzz
honggfuzz 与标准 LLVM/GCC 覆盖率仪器兼容。
构建覆盖率:
# 使用标准编译器,不是 honggfuzz 编译器
clang -fprofile-instr-generate -fcoverage-mapping \
-O2 harness.c execute-rt.c -o fuzz_exec
执行语料库:
LLVM_PROFILE_FILE=fuzz.profraw ./fuzz_exec honggfuzz_workspace/
集成提示:
- 不要对覆盖率构建使用
hfuzz-clang - honggfuzz 语料库通常在 workspace 目录中
- 使用与 libFuzzer 相同的 LLVM 工作流
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
error: no profile data available |
配置文件未生成或路径错误 | 验证 LLVM_PROFILE_FILE 已设置且 .profraw 文件存在 |
Failed to load coverage |
二进制文件与配置文件数据不匹配 | 使用相同标志重建二进制文件 |
| 覆盖率报告显示 0% | 使用了错误的二进制文件生成报告 | 使用仪器化二进制文件,而不是模糊测试二进制文件 |
no_working_dir_found 错误(gcovr) |
.gcda 文件在意外位置 |
添加 --gcov-ignore-errors=no_working_dir_found 标志 |
| 崩溃阻止覆盖率生成 | 语料库包含崩溃输入 | 过滤崩溃或使用 forking 方法隔离故障 |
| 覆盖率在测试工具更改后下降 | 测试工具现在跳过某些代码路径 | 审查测试工具逻辑;可能需要支持更多输入格式 |
| HTML 报告是扁平文件列表 | 使用较旧的 LLVM 版本 | 升级到 LLVM 18+ 并使用 -show-directory-coverage |
incompatible instrumentation |
混合 LLVM 和 GCC 覆盖率 | 使用相同工具链重建所有内容 |
相关技能
使用此技术的工具
| 技能 | 如何应用 |
|---|---|
| libfuzzer | 使用 SanitizerCoverage 反馈;覆盖率分析评估测试工具有效性 |
| aflpp | 使用边缘覆盖率反馈;详细分析需要单独仪器 |
| cargo-fuzz | 内置 cargo fuzz coverage 命令用于 Rust 项目 |
| honggfuzz | 使用边缘覆盖率;使用标准 LLVM/GCC 工具分析 |
相关技术
| 技能 | 关系 |
|---|---|
| fuzz-harness-writing | 覆盖率揭示测试工具达到的代码路径;指导测试工具改进 |
| fuzzing-dictionaries | 覆盖率识别需要字典条目的魔数检查 |
| corpus-management | 覆盖率分析通过识别冗余测试用例帮助管理语料库 |
| sanitizers | 覆盖率帮助验证 sanitizer 仪器化代码实际执行 |
资源
关键外部资源
LLVM Source-Based Code Coverage LLVM 配置文件仪器的全面指南,包括分支覆盖率、区域覆盖率等高级功能,以及与现有构建系统的集成。涵盖编译器标志、运行时行为和配置文件数据格式。
llvm-cov Command Guide
llvm-cov 命令的详细 CLI 参考,包括 show、report 和 export。记录所有过滤选项、输出格式和与 llvm-profdata 的集成。
gcovr Documentation gcovr 工具的完整指南,用于从 gcov 数据生成覆盖率报告。涵盖 HTML 主题、过滤选项、多目录项目和 CI/CD 集成模式。
SanitizerCoverage Documentation LLVM 的 SanitizerCoverage 仪器的低级文档。解释内联 8 位计数器、PC 表以及模糊器如何使用覆盖率反馈引导。
On the Evaluation of Fuzzer Performance 研究论文检查覆盖率作为模糊测试性能指标的局限性。主张在简单代码覆盖率百分比之外使用更细微的评估方法。
视频资源
不适用 - 覆盖率分析主要是一个工具和工作流程主题,最好通过文档和动手实践学习。