名称: 覆盖分析 类型: 技术 描述: > 覆盖分析测量在模糊测试期间执行的代码。 当评估harness有效性或识别fuzzing阻塞点时使用。
覆盖分析
覆盖分析对于理解在模糊测试期间哪些代码部分被执行至关重要。它帮助识别fuzzing阻塞点,如魔法值检查,并跟踪harness改进随时间的效果。
概述
模糊测试期间的代码覆盖率有两个关键目的:
- 评估harness有效性:理解您的应用程序的哪些部分实际上由您的模糊测试harness执行
- 跟踪fuzzing进度:监视当更新harness、fuzzers或系统在测试(SUT)时覆盖率如何变化
覆盖率是模糊测试能力和性能的代理。虽然覆盖率在绝对意义上不适用于测量模糊测试性能,但它可靠地指示您的harness在给定设置中是否有效工作。
关键概念
| 概念 | 描述 |
|---|---|
| 覆盖工具化 | 跟踪哪些代码路径被执行的编译器标志 |
| 语料库覆盖率 | 运行模糊测试语料库中所有测试用例实现的覆盖率 |
| 魔法值检查 | 难以发现的条件检查,阻塞模糊测试进度 |
| 覆盖引导模糊测试 | 优先发现新代码路径的输入的模糊测试策略 |
| 覆盖率报告 | 执行与未执行代码的可视或文本表示 |
何时应用
在以下情况下应用此技术:
- 开始新的模糊测试活动以建立基线
- 模糊测试器似乎停滞,未找到新路径
- 在harness修改后验证改进
- 在不同模糊测试器之间迁移时
- 识别需要字典条目或种子输入的区域
- 调试为什么某些代码路径未到达
在以下情况下跳过此技术:
- 模糊测试活动正在积极发现崩溃
- 覆盖基础设施尚未设置
- 处理极大的代码库,其中完整覆盖率报告不实用
- 模糊测试器内部覆盖指标足够满足您的需求
快速参考
| 任务 | 命令/模式 |
|---|---|
| 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
[覆盖分析]
|
+---> 覆盖率增加? --> 继续模糊测试,使用更大语料库
|
+---> 覆盖率减少? --> 修复harness或调查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/
使用gcovr的GCC:
# 安装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:分析结果
审查覆盖率报告以识别:
- 未覆盖代码块:可能需要更好种子输入或字典条目的区域
- 魔法值检查:带有硬编码值的条件语句,阻塞进度
- 死代码:可能无法通过您的harness访问的函数
- 覆盖变化:与基线比较以跟踪改进或回归
常见模式
模式:识别魔法值
问题:模糊测试器无法发现由魔法值检查保护的路径。
覆盖显示:
// 覆盖显示此块从未执行
if (buf == 0x7F454C46) { // ELF魔法数字
// 开始解析buf
}
解决方案:添加魔法值到字典文件:
# magic.dict
"\x7F\x45\x4C\x46"
模式:处理崩溃输入
问题:当语料库包含崩溃输入时,覆盖生成失败。
之前:
./fuzz_exec corpus/ # 在坏输入上崩溃,未生成覆盖
之后:
// 在执行前分叉以隔离崩溃
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文件以跟踪随时间进度 |
| 从报告中过滤harness代码 | 使用-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 |
| 不过滤harness代码 | Harness覆盖膨胀数字并掩盖SUT覆盖 | 使用-ignore-filename-regex或--exclude过滤harness文件 |
| 混合LLVM和GCC工具化 | 不兼容格式导致解析失败 | 对覆盖构建坚持使用一个工具链 |
| 忽略崩溃输入 | 崩溃阻止覆盖生成,隐藏真实覆盖数据 | 首先修复崩溃,或使用进程分叉隔离它们 |
| 不随时间跟踪覆盖 | 一次性覆盖检查错过回归和改进 | 存储带时间戳的覆盖数据并跟踪趋势 |
工具特定指导
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(它与配置文件工具化冲突) - 重用相同harness函数(
LLVMFuzzerTestOneInput),但使用不同主函数 - 使用
-ignore-filename-regex标志从覆盖率报告中排除harness代码 - 考虑对模板重的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
集成提示:
- 总是对覆盖使用夜间工具链
- 标志
-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标志 |
| 崩溃阻止覆盖生成 | 语料库包含崩溃输入 | 过滤崩溃或使用分叉方法隔离失败 |
| 在harness变化后覆盖率下降 | Harness现在跳过某些代码路径 | 审查harness逻辑;可能需要支持更多输入格式 |
| HTML报告是平面文件列表 | 使用旧LLVM版本 | 升级到LLVM 18+并使用-show-directory-coverage |
incompatible instrumentation |
混合LLVM和GCC覆盖 | 使用相同工具链重建一切 |
相关技能
使用此技术的工具
| 技能 | 如何应用 |
|---|---|
| libfuzzer | 使用SanitizerCoverage进行反馈;覆盖分析评估harness有效性 |
| aflpp | 使用边缘覆盖进行反馈;详细分析需要单独工具化 |
| cargo-fuzz | 内置cargo fuzz coverage命令用于Rust项目 |
| honggfuzz | 使用边缘覆盖;使用标准LLVM/GCC工具分析 |
相关技术
| 技能 | 关系 |
|---|---|
| fuzz-harness-writing | 覆盖揭示harness到达哪些代码路径;指导harness改进 |
| fuzzing-dictionaries | 覆盖识别需要字典条目的魔法值检查 |
| corpus-management | 覆盖分析通过识别冗余测试用例帮助管理语料库 |
| sanitizers | 覆盖帮助验证sanitizer工具化代码是否实际执行 |
资源
关键外部资源
LLVM基于源代码的代码覆盖 LLVM配置文件工具化的综合指南,包括分支覆盖、区域覆盖和与现有构建系统集成等高级功能。涵盖编译器标志、运行时行为和配置文件数据格式。
llvm-cov命令指南
llvm-cov命令的详细CLI参考,包括show、report和export。文档所有过滤选项、输出格式和与llvm-profdata的集成。
gcovr文档 从gcov数据生成覆盖报告的gcovr工具的完整指南。涵盖HTML主题、过滤选项、多目录项目和CI/CD集成模式。
SanitizerCoverage文档 LLVM的SanitizerCoverage工具化的低级文档。解释内联8位计数器、PC表以及模糊测试器如何使用覆盖反馈进行引导。
关于模糊测试器性能评估 研究论文,检查覆盖作为模糊测试性能指标的局限性。主张超越简单代码覆盖百分比的更细致评估方法。
视频资源
不适用 - 覆盖分析主要是工具和工作流主题,最好通过文档和动手实践学习。