llvm-pass1

开始复现CAFL工具,工具原文和笔记在链接。这一周着手写了一个对于compare条件之前,提取操作数并转发的简单(其实我觉得写起来查资料超级麻烦的)pass

关于llvm-pass基础知识有用网站

经过吐血搜索,最后找到集中比较有用的。不得不说,llvm的文档实在是太不友好了,查一个什么类直接告诉你reference,一点解释都没有,直接告诉你源码位置。如下图

image-20221113113525154

我就想说,不想写文档就不要写,还搞个自动生成的文档。。

不过还是有一些部分是有用的,比如说这些

此外就是stackoverflow了,很多问题的解决办法都是在这上面找的。一定要google搜索相关问题。

此外还需要感谢肖浩宇学长,帮我解决了很多pass实现上的问题

instrument value pass

这一部分关于我如何编写一个简单的,获取cmp比较的两个操作数的内容,并转发的pass。首先,我选择的是function pass。关于为什么选择了这个pass,是因为我懒得改模板了。(其实是我觉得Function Pass也能完成任务,不知道和Module Pass的区别主要在哪里呜呜

写一个pass,需要一些prelogue,基本就是声明一个类,然后重载runOnFunction函数

1
2
3
4
5
6
7
8
struct MyPlacementPass : public FunctionPass
{
static char ID;
MyPlacementPass() : FunctionPass(ID){}
FunctionCallee monitor;

bool runOnFunction(Function &F) override
{

runOnFunction内部,我们编写的所有内容都可以作用到每一个函数中,而我所想要做的,就是遍历每一个basic block、遍历每一个instruction、寻找到cmp、提取操作数、插装计算并转发

遍历每一个Basic Block

通过runOnFunction,对于每一个函数做如下操作。首先我打印了一句话,接着我找到一个函数,这个是我暂时用来转发变量的函数(实际复现中可能也需要这样的函数,因为用IR写并且转发似乎没有必要)

1
2
3
4
5
6
7
8
9
10
11
12
bool runOnFunction(Function &F) override
{
printf("------------- runOnFunction --------------\n");
DebugInfoFinder Finder;
Finder.processModule(*(F.getParent()));
// not instrument before show_var function
if (F.getName().startswith("show_var"))
{
errs()<<"find show var"<<"\n";
// monitor = dyn_cast<FunctionCallee>(F);
return false;
}

接着就是遍历每一个basic block并获取每一个指令了

1
2
3
4
5
6
7
8
9
10
11
12
LLVMContext &context = F.getParent()->getContext();

for (Function::iterator I = F.begin(), E = F.end(); I != E; ++I)
{
BasicBlock &BB = *I;
FunctionType *type = FunctionType::get(Type::getVoidTy(context),{Type::getInt32Ty(context)},false);
FunctionCallee sendFunc = BB.getModule()->getOrInsertFunction("send_mem",type);
Value *zero = ConstantInt::get(Type::getInt32Ty(context),0);
Value *one = ConstantInt::get(Type::getInt32Ty(context),1);
CallInst * inst;
for (BasicBlock::iterator I = BB.begin(), E = BB.end(); I != E; ++I)
{

在llvm中,有一些很有用的API,比如Function,BasicBlock的iterator。这些迭代器能够帮助我们遍历整个程序,并对他们做修改。上图中我们首先建立了一个Function::iterator I(实际上是BasicBlock)接着在程序上下文中找到了一个函数,和上面的show_var是一样的。接着建立BasicBlock::iterator I(其实就是Instruction*)。

寻找到cmp

如下,直接用instruction->getOpcode()就可以了,如果Opcode是Instruction::ICmp,就可以直接拿出来,作为比较的指令。

1
2
3
4
5
6
7
IRBuilder<> Builder(I->getContext());
std::vector<Value*> args;
if(I->getOpcode() == Instruction::ICmp){
// we do instrument here
LLVM_DEBUG(dbgs() << "found cmp\n");
ICmpInst *cmp = dyn_cast<ICmpInst>(I);
int cmp_operand = cmp->getPredicate();

提取比较类型

比方说比较用的是”>”, “<”, “==“这样的,我们该怎么提取呢?直接用getPredicate即可。

1
2
3
int cmp_operand = cmp->getPredicate(); 
long distance = 0; // distance of variables
switch (cmp_operand){

Opcode类型也可以从网站中找到。因此我们的代码框架可以是

1
2
3
4
5
6
7
8
9
10
switch (cmp_operand){
case CmpInst::ICMP_EQ:{
{...}
break;
}
case ICmpInst::ICMP_SGE:{
{...}
break;
}
...

插桩转发

这一部分是我经过的坑最多的地方!首先是我们要明白,变量的值都是动态产生的,因此我们必须把代码嵌入到LLVM ir中,不能用pass直接分析。那么我们要做的事情就有

  • 动态获取变量的值
  • 插入计算语句
  • 获取计算语句的结果
  • 将结果转发给已经写好的函数

我们一步一步来

详细流程

下面用大于等于为例说明。

首先我们建立一个Builder,Builder初始化的语句为

IRBuilder<> Builder(I->getContext());

这里的context是什么意思?在stackoverflow里面可以找到这样的描述

This is an important class for using LLVM in a threaded context. It (opaquely) owns and manages the core “global” data of LLVM’s core infrastructure, including the type and constant uniquing tables.

也就是我们可以近似的理解为llvm的一种engine吧。

在初始化Builder之后,我们首先要设置当前builder得到的ir插入位置。也就是第一句SetInsertPoint()

接下来我们创建两个cmp的operand的减法。由于是大于等于,我们直接让第二个参数减去第一个,如果小于0就说明已经满足了,否则就是不满足,并将差距表示出来。

Value* sub_result = Builder.CreateSub(cmp->getOperand(1),cmp->getOperand(0));

接下来建立一个cmp和select语句的结合。这两个语句可以简单产生一个max()而不用新建basic block。参考的网站主要是select表示max和min

接下来只需要调用在C中的函数就可以了。首先准备参数,下面的args的原型为

std::vector<Value*> args;

我们先把他清空,之后push进去相应的函数参数即可。那么怎么找到源文件中的转发函数呢?其实在上面遍历每一个basic block的时候就已经提到过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case ICmpInst::ICMP_SGE:{
// n1 >= n2
// if (n1 - n2 > 0), return 0
// else return n1 - n2
Builder.SetInsertPoint(cmp);
Value* sub_result = Builder.CreateSub(cmp->getOperand(1),cmp->getOperand(0)); // hot to esure it's signed?
Value* cmp_result = Builder.CreateICmp(ICmpInst::ICMP_SGT, sub_result,zero);
Value* maxnum = Builder.CreateSelect(cmp_result,sub_result,zero);

// give to send_num()

args.clear();
args.push_back(maxnum);
inst = CallInst::Create(sendFunc,args);
inst->insertBefore(cmp);
errs()<<"instrument for >=(SGE) ok."<<"\n";


break;
}

到此,一个简单的针对大于符号的pass就写好了。在此基础上可以写基于别的符号的pass。

编译和测试

编译

用到的所有编译指令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# generate a pass
clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared ./instrument_printVar.cpp -o inst_var.so `llvm-config --ldflags`

# generate bc file
clang++ -emit-llvm -c -g test_clang.cpp -o test_clang.bc
llvm-dis ./change_clang.bc -o=change_clang_dis.ll

# use a pass
opt -load ./output_func.so -function-info ./test_clang.bc -o /dev/null
opt -load ./inst_var.so -var ./test_clang.bc -o ./change_clang.bc

# get .o file
llc -filetype=obj ./change_clang.bc

# compile to binary
clang++ ./change_clang.o -o change_clang


# extern issue
# https://stackoverflow.com/questions/29903416/how-to-call-function-in-llvm


流程一般是这样

  1. 用clang编译pass和源文件,生成pass.so和bc文件
  2. 用opt加载pass,从而对bc进行插桩修改
  3. 从bc获取.o文件
  4. 将.o文件链接成最终可执行文件

测试

用下面的源文件进行测试

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
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<string.h>
#include<malloc.h>
using namespace std;


extern "C" void show_var(long num){
printf("value content is %ld\n", num);
}

extern "C" void send_mem(int num){
printf("Value to be send is %d. \n",num);
}

// static void show_var(long num){
// printf("value content is %ld\n", num);
// }


int main()
{
int a = 10;
int b = 0;
// not equal. we should send 1
if(a != 20){
b ++;
}
// false. we should send (20-10)=10
//
if(a >= 20){
b++;
}
// True. we should send 0
if(a <= 20){
b++;
}
// false. we should send |10-20| = 10, but now I send -10
if(a == 20){
b++;
}
return 0;
}

最终成功在ir上面插桩,得到如下文件(下面是部分截图)

1
2
3
4
5
6
7
%13 = load i32, i32* %2, align 4
%14 = sub i32 20, %13
%15 = icmp sgt i32 %14, 0
%16 = select i1 %15, i32 %14, i32 0
call void @send_mem(i32 %16)
%17 = icmp sge i32 %13, 20
br i1 %17, label %18, label %21

执行的结果如下,成功~(最后花了快两天时间呜呜呜)

image-20221113140228527

碰到的问题和解决措施

在执行过程中碰到了很多问题。这也是第一次自己写一个pass

seg fault

这是最常见的错误了,类似下图

image-20221113140409166

一般原因是:函数参数不匹配/插入的语句不在basic block中(这些是有原因会告诉你的,比方上面这张图,就是broken module found)

但是有一些报错,根本不会告诉你原因!

后来才知道,这种情况大部分因为没有检查API返回内容是否是NULL,从而使用了空指针

比方说下面修改后的内容,加上了if/else来检查。如果不检查就有可能出现空指针引用的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
I->getModule()->getOrInsertGlobal("for_equal",Type::getInt32Ty(context));
GlobalVariable* for_equal = I->getModule()->getNamedGlobal("for_equal");
if(for_equal){
for_equal->setAlignment(MaybeAlign(4));
for_equal->setLinkage(GlobalValue::CommonLinkage);
Constant* const_int_val = ConstantInt::get(Type::getInt32Ty(context),APInt(32,0));
for_equal->setInitializer(const_int_val);
Builder.SetInsertPoint(cmp);
LoadInst *load = Builder.CreateLoad(for_equal);
StoreInst *store = Builder.CreateStore(NewInst,for_equal);
}
else{
errs()<<"not valid"<<"\n";
}

指令不在basic block中

主要是出现下列错误

error: Instruction referencing instruction not embedded in a basic block!

网上根本搜不到怎么做。我当时只是想获取某个指令的计算结果,并传给send_mem函数,就出现了这种问题。

出现的原因:使用了以下方式进行指令的插入

1
2
inst = CallInst::Create(sendFunc,args);
inst->insertBefore(cmp); // basic block will not remember!

使用上面的方法,确实能在IR中插入语句,但是basic block里面不会记载!因此,我们不能使用这一条指令对应的结果!

后来我才知道正确的插入方法应该是用IRbuilder,先Builder.SetInsertPoint()之后再编写语句,事实上,这样编写也比直接创建指令来得简单很多。

后期计划

  1. 在IR中使用debugInfo,实现定位变量
  2. 整合我的pass和AFLGo

其实进展已经比想象中的快了,希望能够顺利的做完毕设!

文章目录
  1. 1. 关于llvm-pass基础知识有用网站
  2. 2. instrument value pass
    1. 2.1. 遍历每一个Basic Block
    2. 2.2. 寻找到cmp
      1. 2.2.1. 提取比较类型
    3. 2.3. 插桩转发
      1. 2.3.1. 详细流程
  3. 3. 编译和测试
    1. 3.1. 编译
    2. 3.2. 测试
  4. 4. 碰到的问题和解决措施
    1. 4.1. seg fault
    2. 4.2. 指令不在basic block中
  5. 5. 后期计划
|