1、介绍
- LLVM 的 SanitizerCoverage 是一种代码覆盖工具,设计用于支持基于 fuzzing 的测试和其他安全相关工具。SanitizerCoverage 在编译时插桩代码,以在运行时收集覆盖信息,从而帮助识别未覆盖的代码路径,提高测试的有效性和全面性。
- 前提:安装clang即可。
- 核心功能:
- 边缘覆盖:插入代码以记录每个基本块的入口和出口,生成边缘覆盖信息。这样可以捕捉到程序执行路径的变化。
- 基本块覆盖:记录每个基本块的执行情况,生成基本块级别的覆盖信息。
- 函数覆盖:记录每个函数的调用情况,以函数级别生成覆盖信息。
- 间接调用覆盖:追踪间接调用(例如通过函数指针)的执行路径。
- 官方文档:SanitizerCoverage — Clang 19.0.0git 文档 (llvm.org)
2、插桩并计算覆盖率
- 准备一个test.c文件,计算该程序运行时的覆盖率。
-
#include <stdio.h> int main(int argc, char *argv[]) { int a, b; char op; int result; scanf("%d%c%d", &a, &op, &b); switch (op) { case '+': result = a + b; break; case '-': result = a - b; break; case '*': result = a * b; break; case '/': result = a / b; break; default: return 1; } printf("%d\n", result); return 0; }
-
- 自定义一个插桩文件,放在同一目录下。
-
// trace-pc-guard-cb.c #include <stdint.h> #include <stdio.h> #include <sanitizer/coverage_interface.h> // 这个回调由编译器作为模块构造函数插入到每个DSO中。 // 'start' 和 'stop' 对应于整个二进制(可执行文件或DSO)中保护区域的起始和结束。 // 这个回调将至少被每个DSO调用一次,并且可能会用相同的参数被多次调用。 void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { static uint64_t N; // 保护的计数器。 if (start == stop || *start) return; // 只初始化一次。 printf("INIT: %p %p\n", start, stop); for (uint32_t *x = start; x < stop; x++) *x = ++N; // 保护应该从1开始。 } // 这个回调由编译器在控制流的每个边上插入(应用了一些优化)。 // 通常,编译器会生成这样的代码: // if(*guard) // __sanitizer_cov_trace_pc_guard(guard); // 但是对于大型函数,它会生成一个简单的调用: // __sanitizer_cov_trace_pc_guard(guard); void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; // 复制保护检查。 // 如果将 *guard 设置为 0,这段代码将不会再次被该边调用。 // 现在你可以获取PC并做任何你想做的事: // 将其存储在某处或立即符号化并打印。 // `*guard` 的值是你在 // __sanitizer_cov_trace_pc_guard_init 中设置的,因此你可以使它们连续 // 并使用它们来解除数组或位向量的引用。 void *PC = __builtin_return_address(0); char PcDescr[1024]; // 这个函数是sanitizer运行时的一部分。 // 要使用它,请链接AddressSanitizer或其他sanitizer。 __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); }
-
- 编译运行:
-
clang -g -fsanitize-coverage=trace-pc-guard test.c -c clang trace-pc-guard-cb.c test.o -fsanitize=address -o test.exe test.exe
- -fsanitize-coverage=trace-pc-guard:对边进行插桩。(默认:edge)
- -fsanitize-coverage=func,trace-pc-guard:对每个函数插桩。
- -fsanitize-coverage=bb,no-prune,trace-pc-guard:对基本块插桩。
-
- 运行结果:
- INIT: 001e32d8 1e32f8:表示保护区域的内存范围。
- guard: 001e32d8 1 PC x1d180d:guard地址指向当前保护值的内存位置;1为该保护点的唯一标识;PC表示当前程序计数器的地址。
- 【结果分析】
- 0x009832d8到0x009832f8之间的字节数:
- 0x009832f8 - 0x009832d8 = 0x20
- guard(插入的点)个数 = 0x20 / 4 = 8(每个guard变量占用4个字节)
- 所以SanitizerCoverage在源代码中一共插入了8个点。
- 分析运行结果,在没有输入数据前,执行了两个点(不同的)。输入数据后,又执行了两个点(不同的),所以一共执行了4个点。
- 覆盖率计算
- 假设总共有N个guard变量,而运行时触发了M个不同的guard变量,那么覆盖率可以计算为:
- 覆盖率 = (𝑀 / 𝑁) × 100% = 4 / 8 = 50%
- 插桩位置:
-
#include <stdio.h> // 1 函数入口 int main(int argc, char *argv[]) { int a, b; char op; int result; // 2 条件分支 scanf("%d%c%d", &a, &op, &b); switch (op) { // 3 case '+' case '+': result = a + b; break; // 4 case '-' case '-': result = a - b; break; // 5 case '*' case '*': result = a * b; break; // 6 case '/' case '/': result = a / b; break; default: // 7 return 1 函数出口 return 1; } printf("%d\n", result); // 8 return 0 函数出口 return 0; }
-