开始复现CAFL工具,工具原文和笔记在链接。这一周着手写了一个对于compare条件之前,提取操作数并转发的简单(其实我觉得写起来查资料超级麻烦的)pass
关于llvm-pass基础知识有用网站
经过吐血搜索,最后找到集中比较有用的。不得不说,llvm的文档实在是太不友好了,查一个什么类直接告诉你reference,一点解释都没有,直接告诉你源码位置。如下图
我就想说,不想写文档就不要写,还搞个自动生成的文档。。
不过还是有一些部分是有用的,比如说这些
- 学习llvm ir的c++api最好的开始的地方:https://llvm.org/docs/ProgrammersManual.html
- 学习llvm基础知识:https://llvm.org/docs/LangRef.html
- 特别详细的入门llvm ir c++ api的例子:https://blog.csdn.net/Zhanglin_Wu/article/details/125289502
- 国外的编译器优化教程,会从零基础讲llvm pass如何编写
此外就是stackoverflow了,很多问题的解决办法都是在这上面找的。一定要google搜索相关问题。
此外还需要感谢肖浩宇学长,帮我解决了很多pass实现上的问题
instrument value pass
这一部分关于我如何编写一个简单的,获取cmp比较的两个操作数的内容,并转发的pass。首先,我选择的是function pass。关于为什么选择了这个pass,是因为我懒得改模板了。(其实是我觉得Function Pass也能完成任务,不知道和Module Pass的区别主要在哪里呜呜
写一个pass,需要一些prelogue,基本就是声明一个类,然后重载runOnFunction函数
1 | struct MyPlacementPass : public FunctionPass |
在runOnFunction
内部,我们编写的所有内容都可以作用到每一个函数中,而我所想要做的,就是遍历每一个basic block、遍历每一个instruction、寻找到cmp、提取操作数、插装计算并转发。
遍历每一个Basic Block
通过runOnFunction,对于每一个函数做如下操作。首先我打印了一句话,接着我找到一个函数,这个是我暂时用来转发变量的函数(实际复现中可能也需要这样的函数,因为用IR写并且转发似乎没有必要)
1 | bool runOnFunction(Function &F) override |
接着就是遍历每一个basic block并获取每一个指令了
1 | LLVMContext &context = F.getParent()->getContext(); |
在llvm中,有一些很有用的API,比如Function,BasicBlock的iterator。这些迭代器能够帮助我们遍历整个程序,并对他们做修改。上图中我们首先建立了一个Function::iterator I
(实际上是BasicBlock)接着在程序上下文中找到了一个函数,和上面的show_var是一样的。接着建立BasicBlock::iterator I
(其实就是Instruction*)。
寻找到cmp
如下,直接用instruction->getOpcode()就可以了,如果Opcode是Instruction::ICmp
,就可以直接拿出来,作为比较的指令。
1 | IRBuilder<> Builder(I->getContext()); |
提取比较类型
比方说比较用的是”>”, “<”, “==“这样的,我们该怎么提取呢?直接用getPredicate即可。
1 | int cmp_operand = cmp->getPredicate(); |
Opcode类型也可以从网站中找到。因此我们的代码框架可以是
1 | switch (cmp_operand){ |
插桩转发
这一部分是我经过的坑最多的地方!首先是我们要明白,变量的值都是动态产生的,因此我们必须把代码嵌入到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 | case ICmpInst::ICMP_SGE:{ |
到此,一个简单的针对大于符号的pass就写好了。在此基础上可以写基于别的符号的pass。
编译和测试
编译
用到的所有编译指令如下
1 | generate a pass |
流程一般是这样
- 用clang编译pass和源文件,生成pass.so和bc文件
- 用opt加载pass,从而对bc进行插桩修改
- 从bc获取.o文件
- 将.o文件链接成最终可执行文件
测试
用下面的源文件进行测试
1 |
|
最终成功在ir上面插桩,得到如下文件(下面是部分截图)
1 | %13 = load i32, i32* %2, align 4 |
执行的结果如下,成功~(最后花了快两天时间呜呜呜)
碰到的问题和解决措施
在执行过程中碰到了很多问题。这也是第一次自己写一个pass
seg fault
这是最常见的错误了,类似下图
一般原因是:函数参数不匹配/插入的语句不在basic block中(这些是有原因会告诉你的,比方上面这张图,就是broken module found)
但是有一些报错,根本不会告诉你原因!
后来才知道,这种情况大部分因为没有检查API返回内容是否是NULL,从而使用了空指针
比方说下面修改后的内容,加上了if/else来检查。如果不检查就有可能出现空指针引用的问题。
1 | I->getModule()->getOrInsertGlobal("for_equal",Type::getInt32Ty(context)); |
指令不在basic block中
主要是出现下列错误
error: Instruction referencing instruction not embedded in a basic block!
网上根本搜不到怎么做。我当时只是想获取某个指令的计算结果,并传给send_mem函数,就出现了这种问题。
出现的原因:使用了以下方式进行指令的插入
1 | inst = CallInst::Create(sendFunc,args); |
使用上面的方法,确实能在IR中插入语句,但是basic block里面不会记载!因此,我们不能使用这一条指令对应的结果!
后来我才知道正确的插入方法应该是用IRbuilder
,先Builder.SetInsertPoint()之后再编写语句,事实上,这样编写也比直接创建指令来得简单很多。
后期计划
- 在IR中使用debugInfo,实现定位变量
- 整合我的pass和AFLGo
其实进展已经比想象中的快了,希望能够顺利的做完毕设!