为什么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
当面对一个使用 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"
检查插桩了是否真的用上了自定义的runtime: 用nm命令查看:
1 nm fftp | grep trace_pc_guard
输出大概如下:
1 2 3 4 5 0000000000424fc0 T __sanitizer_cov_trace_pc_guard 00000000004250b0 T __sanitizer_cov_trace_pc_guard_init 0000000000ed4c28 b __sanitizer_cov_trace_pc_guard_init.N 00000000004183a0 T __sanitizer_dump_trace_pc_guard_coverage 0000000000425b10 t sancov.module_ctor_trace_pc_guard
1 2 3 4 0000000000417dd0 W __sanitizer_cov_trace_pc_guard 0000000000417e20 W __sanitizer_cov_trace_pc_guard_init 00000000004180d0 T __sanitizer_dump_trace_pc_guard_coverage 00000000004252f0 t sancov.module_ctor_trace_pc_guard
确保 coverage_runtime.o 在所有目标文件之前 coverage_runtime.c 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就是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 2 3 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