插桩统计覆盖率的相关部分主要在afl-llvm-pass-so.c
中的bool AFLCoverage::runOnModule(Module &M)
部分
插桩率设置
在函数开始会有相关变量的初始化工作,但是主要值得关注的是关于inst_ratio_str
的设置。inst_ratio_str
表示 AFL 应该插桩的基本块的比例。例如,如果 AFL_INST_RATIO
设置为 10
,那么 AFL 大约会选择 10% 的基本块进行插桩。如果 AFL_INST_RATIO
未设置,或者解析失败,或者值超出了允许的范围,就会使用默认值 100,即插桩所有的基本块。
/* Decide instrumentation ratio */
char* inst_ratio_str = getenv("AFL_INST_RATIO");
unsigned int inst_ratio = 100;
if (inst_ratio_str) {
if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || !inst_ratio ||
inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 1 and 100)");
}
关键全局变量
之后定义了两个关键的全局变量。
AFLMapPtr
是一个指针类型的全局变量,指向 AFL 共享内存区域的开始。这个内存区域存储了 AFL 追踪数据(trace_bits
),用于在程序运行时记录哪些路径(或更具体地说,哪些基本块转移)已经被执行过。当 AFL 插桩的代码执行时,它会更新这个内存区域的相应部分。
AFLPrevLoc
是一个 32 位整数类型的全局变量,存储了上一个执行的基本块的 ID。注意,这个变量是线程局部的,这意味着每个线程都有自己的一份副本。这样做是为了避免在多线程环境下出现数据竞态。当 AFL 插桩的代码执行时,它会用当前基本块的 ID 更新这个变量。
/* Get globals for the SHM region and the previous location. Note that
__afl_prev_loc is thread-local. */
GlobalVariable *AFLMapPtr =new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");
GlobalVariable *AFLPrevLoc = new GlobalVariable(M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",0, GlobalVariable::GeneralDynamicTLSModel, 0, false);
进行插桩处理
重要的解释都在下面代码相关行段,主要解释一些补充内容。
IRB的作用和用法
IRB
主要被用来在LLVM的BasicBlock中插入新的IR指令。例如,IRB.CreateLoad
、IRB.CreateZExt
、IRB.CreateGEP
、IRB.CreateAdd
和IRB.CreateStore
都是通过IRB
创建新的Load、ZExt(零扩展)、GEP(GetElementPointer,获取元素指针)、Add和Store指令。创建
IRB
的时候,将一个Instruction指针传递给IRB
的构造函数,这个Instruction指定了新创建的IR指令将被插入的位置。在这个例子中,新创建的IR指令被插入到每个BasicBlock的第一个插入点。
为什么要创建随机位置cur_loc
在AFL的插桩策略中,为每个插桩点生成一个随机位置
cur_loc
的主要原因是为了创建一个独特的、在全局范围内唯一的标识符。AFL使用的覆盖追踪策略主要依赖于这个cur_loc
。在每次执行插桩的基本块时,AFL将当前的cur_loc
与上一个cur_loc
进行异或操作,并将结果用作索引,将共享内存中该索引位置的计数器增加1。通过这种方式,AFL可以通过观察共享内存区的变化来知道哪些插桩的基本块被执行了,从而了解代码的覆盖情况。
Value *MapPtrIdx = IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));
解释
首先进行了一个 XOR 位运算(通过
IRB.CreateXor(PrevLocCasted, CurLoc)
实现),将上一个位置的值与当前位置的值进行异或操作。
然后使用CreateGEP
函数根据异或操作的结果生成一个新的指针。GEP 代表 GetElementPointer,它是 LLVM IR 中的一种指令,用于计算复杂数据结构(如数组、结构体)中元素的地址。在 AFL 中,这个指令被用来在共享内存区中定位到一个特定的位置,这个位置对应的是当前和上一个插桩代码块的异或结果。这样就能对每一对连续执行的插桩代码块进行唯一标识,实现精确的路径覆盖统计。
LoadInst
是什么数据类型
LoadInst
是一种指令类,继承自Instruction
类。它代表了一个从内存加载数据的操作,与在 C 或者 C++ 中的读取变量值的操作相对应。
/* Instrument all the things! */
#定义一个计数器,用来统计插桩的BasicBlock的数量。
int inst_blocks = 0;
#这两个嵌套的for循环用来遍历Module M中的所有Function F和每个Function中的所有BasicBlock BB。
for (auto &F : M)
for (auto &BB : F) {
#找到BB的第一个可插入点IP,然后创建一个IRBuilder对象IRB,用来在这个位置插入新的IR指令。
BasicBlock::iterator IP = BB.getFirstInsertionPt();
IRBuilder<> IRB(&(*IP));
#通过生成0-100的随机数,来和inst_ratio进行比较决定该基本块是否要进行插桩
if (AFL_R(100) >= inst_ratio) continue;
/* Make up cur_loc */
unsigned int cur_loc = AFL_R(MAP_SIZE);
#这行代码的主要目的是在 LLVM IR 中创建一个表示当前位置(或基本块ID)的常量对象,以便后续的插桩代码可以使用。
ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);
/* Load prev_loc */
#创建一个新的 LLVM IR 的 Load 指令,加载之前存储的上一个位置的值。`AFLPrevLoc` 是一个全局变量,存储了上一个插桩块的位置(即代码路径)。`CreateLoad` 函数会生成一个加载指令,从指定的内存位置加载一个值。
LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
#给加载指令添加元数据,禁用某些编译器优化。"nosanitize" 是元数据的种类,它告诉编译器不要对这个加载指令应用任何可以改变其行为的优化。这是必要的,因为 AFL 需要能准确地跟踪代码的执行路径。
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
#创建一个新的 LLVM IR 的 ZeroExtend 指令。ZeroExtend (ZExt) 是一种类型转换指令,它将一个小的整数类型扩展为一个更大的整数类型,高位用0填充。在这里,`PrevLoc`(上一位置值)被扩展为32位整数,用于后续的 XOR 操作。ZeroExtend 不会改变数的值,但会改变它的位宽,使得它可以和其他32位整数进行操作。
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());
/* Load SHM pointer */
#创建一个新的 LLVM IR 的 Load 指令,用于加载 AFL 全局变量 `__afl_area_ptr` 的值,也就是 AFL 共享内存区域的起始地址。这个内存区域用于存储每个插桩代码块的执行计数。
LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
#加载指令添加元数据,禁用某些编译器优化。
MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *MapPtrIdx =
IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));
/* Update bitmap */
#从上一步计算得到的地址 `MapPtrIdx` 中加载当前路径的执行计数。
LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
#创建一个 LLVM 的加法指令,将加载出来的计数值加 1,之后再存回内存当中。
Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
IRB.CreateStore(Incr, MapPtrIdx)
->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
/* Set prev_loc to cur_loc >> 1 */
#创建了一个存储操作,将 `cur_loc >> 1` 的值(右移一位的当前路径位置)存储到全局变量 `__afl_prev_loc` 中。也就是说,这里将当前路径的位置保存下来,以备下一次使用。`cur_loc >> 1` 的操作是为了限制位置的范围在预定的大小以内。
StoreInst *Store =
IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
#记录已经插桩的基本块的数量。每插桩一个基本块,该计数器就增加一,用于最后报告插桩的统计结果
inst_blocks++;
}
/* Say something nice. */
#如果没有设置quiet模式,则输出插桩的信息。
if (!be_quiet) {
if (!inst_blocks) WARNF("No instrumentation targets found.");
else OKF("Instrumented %u locations (%s mode, ratio %u%%).",
inst_blocks, getenv("AFL_HARDEN") ? "hardened" :
((getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) ?
"ASAN/MSAN" : "non-hardened"), inst_ratio);
}
return true;
}
注册 AFL 插桩的 Pass
registerAFLPass
:创建一个新的 AFLCoverage
对象,并将其添加到传递的 Pass 管理器(PM
)中。
RegisterAFLPass
:这行代码创建了一个 RegisterStandardPasses
对象,它的构造函数接受两个参数,一个是枚举值 EP_ModuleOptimizerEarly
,表示 Pass 的注册阶段;另一个是函数 registerAFLPass
,这是一个回调函数,当到达相应的注册阶段时,Pass 管理器会调用这个函数,执行具体的注册工作。
RegisterAFLPass0
:这表示即使在优化级别为 0 的情况下,也会执行这个 Pass。
static void registerAFLPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new AFLCoverage());
}
static RegisterStandardPasses RegisterAFLPass(
PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);
static RegisterStandardPasses RegisterAFLPass0(
PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);