0%

插桩覆盖率信息动态获取

为什么afl插桩可以动态获取覆盖率,而gcov需要终止程序来收集?

AFL(American Fuzzy Lop)使用的是插桩 + 共享内存的覆盖率收集机制。插桩后的代码会把覆盖信息写到一个共享内存区域。

gcov这类工具,通常使用静态插桩 + 文件写出的方式收集覆盖率。只有在程序正常终止时,插桩逻辑才会将内存中的统计信息写到 .gcda 等文件中。

GCC、Clang、LLVM 三者的区别

名称 类型 说明
GCC 编译器套件 GNU 编译器合集,包括 gcc, g++, gfortran
Clang 编译器前端 LLVM 项目中的 C/C++/Objective-C 编译器
LLVM 编译基础设施框架 提供中间语言、优化、后端、JIT 等一整套架构

LLVM把gcc解耦了,前后端分离,模块化

Source Code

Frontend (Parser)
GCC or Clang ← GCC 与 Clang 竞争的是这里

LLVM IR ← Clang 会转成中间代码

Optimizer (opt)

Backend (llc)
Generate Assembly

Linker (lld)

💡 AFL 根据你系统中安装的编译器选择不同的插桩模式和深度。你用得越高级(比如 afl-clang-fast),对 Clang/LLVM 的依赖就越强。

插桩方式 编译器依赖 插桩原理 是否推荐
afl-gcc 只要 gcc 就行 静态插桩,效果一般 ❌(老旧)
afl-clang clang 即可 静态插桩 ❌(少用了)
afl-clang-fast clang + llvm LLVM IR 级插桩(支持 PCGUARD) ✅ 推荐使用

afl-clang-fast` 使用了 PCGUARD 插桩 ,但还封装了 共享内存逻辑 + 路径 hash + 热度计数;纯 PCGUARD 插桩更干净、更灵活,更适合你这种 Peach 外部控制执行流程的情况; 两者在“插桩方式”看起来类似,但在运行时处理覆盖率的方式不同(特别是 bitmap 的更新频率和逻辑);

项目 AFL 默认插桩(afl-gcc) afl-clang-fast(基于 PCGUARD) 纯 PCGUARD 插桩
插桩方式 修改源码或汇编 添加 LLVM IR pass,激活 trace-pc-guard 使用 clang 内置的 -fsanitize-coverage=trace-pc-guard
插桩代码 自己加 inline asm 让编译器插入回调,链接 AFL runtime 只插入调用 __sanitizer_cov_trace_pc_guard(),不链接 AFL runtime
侵入性 中等:依赖特殊宏定义 高:需要额外 runtime 支持 低:编译器自带
性能影响 高(加 hash + map 计数) 低(只标记一次命中)
实时性能力 ✅ 用共享内存读 bitmap ✅ 可用共享内存 ✅ 你可以自己定义共享内存 & 写 hook
结果粒度 分支级别 + hit 频率 分支级别 + hit 桶 分支级别(单次)

clang插桩编译

1
clang -fsanitize-coverage=trace-pc-guard -g -o target_cov target.c coverage_runtime.c
  • Clang trace-pc-guard 插桩只是调用 __sanitizer_cov_trace_pc_guard(),但是你需要提供这个函数的定义来告诉程序 “hit 了这个分支之后要干什么”

  • 记录在哪?怎么算?怎么展示? 这些逻辑都得你来写(AFL、SanCov、LibFuzzer 都是自带的 runtime 实现)。

当面对一个使用 Makefile 的项目,想用自己写的 PCGUARD 插桩 runtime(如 coverage_runtime.c 时:

1
2
3
clang -O0 -g -fno-omit-frame-pointer \ -fsanitize-coverage=trace-pc-guard \ -c coverage_runtime.c -o coverage_runtime.o

make CC=clang CFLAGS="-O0 -g -fno-omit-frame-pointer -fsanitize-coverage=trace-pc-guard" LDFLAGS="coverage_runtime.o -lrt"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define MAX_EDGES 65536
uint8_t* bitmap = NULL;

void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
if (!*guard) return;
uint32_t idx = *guard;
*guard = 0;
if (bitmap)
bitmap[idx % MAX_EDGES] = 1; // 标记为覆盖
        printf("[PCGUARD] Hit idx: %u\n", idx % MAX_EDGES); // 打印命中的索引
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {
static uint32_t N;
if (start == stop || *start) return;

int fd = open("/tmp/peach_cov_map", O_CREAT | O_RDWR, 0600);
ftruncate(fd, MAX_EDGES);
bitmap = mmap(0, MAX_EDGES, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);

for (uint32_t* x = start; x < stop; x++)
*x = ++N; // 每个guard初始化一个索引
}

guard就是edge的编号,hit(命中)了就记录在共享内存

特性/工具 LLVM/Clang 插桩 GCC/gcov 插桩
插桩工具 -fsanitize-coverage=... -fprofile-arcs -ftest-coverage
覆盖粒度 基本块、边(edge)、调用、返回等 行(line)、分支(branch)、函数(function)
用途 模糊测试、路径去重、反馈引导优化等 单元测试、代码覆盖分析
代表工具 AFL, LibFuzzer, etc. gcov, lcov, gcovr
报告能力 不生成测试报告(靠 fuzz 工具处理 bitmap) 自动生成 .gcov 报告文件

共享内存需要注意的问题

1) mmap映射文件到内存时必须先预分配大小,ftruncate(fd, MAX_EDGES); 要不然可能报错Aborted (core dumped)

2) 插桩runtime中定义的共享内存的地址、大小与读取覆盖率信息的程序中的必须保证完全一致,要不然可能读写的不是一个东西,导致输出全为0或其他错误。

3) 读取覆盖率信息的程序也可以创建共享内存文件, 可以避免执行先后顺序的问题。如果只读,有可能在SUT还没启动时就去访问共享内存初始化什么的出错。

demo

1
clang -fsanitize-coverage=trace-pc-guard -g -o target_cov target.c coverage_runtime.c
clang control.c -fPIC -shared -o libruntime.so
mcs Program.cs -out:peach.exe
------------- Thank you for reading -------------

Title - Artist
0:00