llvm学习(八):Pass编写简单案例

讲了那么多基础知识,现在我们实际操作一下,讲一下Pass的注册和简单用途,通俗易懂,看不懂只能说明你脑子不够用。

本文的全部代码都可以在文章最后找到。

一、Pass简介和构成

Pass 是用来处理IR 的,llvm 自身就包含很多Pass,是一种流水线的处理方式,按照一定顺序,将IR 进行一层一层的修改、优化,也是进行混淆的最佳位置。

网上盗个图,随便找一张,中间那堆就是Pass了,输入是 IR,输出也是 IR。

Pass 分为下面几种,最常用的是前两个,(后面几个我不大知道是干嘛的)

  • ModulePass
  • FunctionPass
  • BasicBlockPass
  • CallGraphSCCPass
  • LoopPass
  • RegionPass

自带3个方法,如果有需要可以重写它们

主要用的也就两个, FunctionPass  ModulePass 

顾名思义,实际加载时也是按照 initial–runOnXXX—finalize 去执行的。

二、Pass的定义注册方式(只是一种方式,不全)

先看一个简单的定义的例子

因为  FunctionPass 构造时候需要一个ID,就是任意的char,随便传就行。

getPassName  可选,为了美观这里重写了虚方法;

runOnFunction  必选,毕竟这里是核心的代码;

doInitialization / doFinalization ,可选,如果需要一些配合和内存回收。

注册方式比较花哨,网上看了一堆,我用的是 llvm::RegisterStandardPasses 这个结构体,只要在我们的library 里创建static的结构体,就可以实现动态加载,非常方便。关键词好像是:“llvm pass auto-register”, “PassManager”, “PassManagerBuilder”,如果觉得讲的不清楚可以搜搜看别人的。

在加载我们的 so 的过程中,就会初始化这个静态变量,这时就会执行构造方法将我们的pass 加入到全局的池子里。

第一个参数是 PassManagerBuilder::ExtensionPointTy ,是枚举值,表示加载的时机,这个枚举值非常多,在不同的优化级别的场景下,很多其实是不可达的。

我一般只用3个, EP_EarlyAsPossible , EP_OptimizerLast , EP_EnabledOnOptLevel0 。

EP_EarlyAsPossible 只能加载 FunctionPass ,加载  ModulePass  会爆炸;

EP_EnabledOnOptLevel0 在 -O0 情况下触发;

EP_OptimizerLast 在非 -O0情况下触发;

第二个参数,是函数的指针,

表示使用的函数签名是

这时候提供了注册时的上下文,一般用 legacy::PassManagerBase &PM  这个变量就够了,第一个参数不知道用来干嘛,第二个可以用来添加Pass

例如:

三、使用IRBuilder

这是一个非常非常好用的东西,对于连续插入语句时非常友好,包括了绝大多数Instruction  快速插入也提供了一些方便的对于全局变量操作API

主要有这两种构造方式

IRBuilder<> IRB(BasicBlock*); 在某个 BasicBlock 末尾连续插入语句

IRBuilder<> IRB(Instruction*); 在某个 Instruction 前方连续插入语句

例如创建跳转IRB.CreateBr ;创建加法, IRB.CreateAdd ;创建栈变量, IRB.CreateAlloca ;对于开发效率有巨大的提升。

四、写一个简单的FunctionPass

把ShowName 讲一下,这部分我们动手写3个东西,在函数调用的开头,使用printf打印栈上的函数名、rodata 的函数名、rwdata 的函数名,也是我第一个练手的Pass,比较有教育意义。

先弄一个在栈上的。

思路是创建BasicBlock,创建char数组,将其memset,再挨个赋值进去,再使用printf 打印这个指针。

1EntryBlock 前插入我们的printfBlock

BasicBlock *printfBlock = BasicBlock::Create(F.getContext(), "printfStackBlock", &F, entryBlock);

BasicBlock  只有这一个构造方法,意思是在 EntryBlock前插入一个空的 BasicBlock,注意,因为它是空的、没有后继,所以这时候CFG 不完整,会编译失败的。要记得在 BasicBlock 最后一句加上跳转之类的 TerminatorInst 

2、创建char 数组。

在IR 里是没有char的概念的,我们使用 int8 这种类型

3、使用llvm 内置的memset 方法,对其进行初始化

这里有个小知识,叫 intrinsic function ,是llvm 对常见的函数进行的IR 层的封装,例如 memcpy/memset/malloc  这种,非常常见的函数,比如这个链接(https://llvm.org/docs/LangRef.html#llvm-memcpy-intrinsic虽然这个链接在瞎扯淡)

IR ,表现出来是函数调用,将来翻译成汇编时候可能会有各种优化。

这里可能我讲的不清楚,可以搜索关键字“intrinsic function”

memset 为例,llvm在32bit 下和64bit 下,对应的有两种重载,名字分别是 llvm.memset.p0i8.i64/llvm.memset.p0i8.i32 。

区别在于后面的参数是32bit 指针还是64bit指针。根据llvm 的规定,一定要明确的将二者区分出来,才能使用intrinsic function,这里演示一下使用i64的案例。

官方说有两个重载: @llvm.memset.p0i8.i32@llvm.memset.p0i8.i64 

代码里这么写:

第二个参数,是表示memset 的一个枚举值;

第三个参数,是一个临时的Vector,表示额外的信息,这里是 int8* int64 

从表现上猜一下,逻辑是根据第二个参数,获取一个字符串叫“llvm.memset”,再根据第三个参数拼接“.p0i8”“.i64”,得到完整的函数名,再去发起这个调用。所以如果有重载,一定要提供文档里要求的那两个额外信息。

拿到这个函数后,观察下需要的参数,分别是 i8*,i8,i64,i1

后面三个都是常数,直接用 ConstantInt 拿值即可,第一个比较难搞,因为对 ArrayType  进行alloca生成的是栈上的一个索引,它不是一个数据,而是一个变量的引用。举个例子, Alloca(i32) 后,要用 LoadInst  去加载它,拿到真正int32 。而对于printf 的例子,是对数组操作的,需要使用GEP Get Element Pointer这个概念,这里使用IRB 帮助我们构造它,例如这个例子:

 

第一个参数是创建的int8数组

 

第二个参数是Vector,存放两个int32类型的Value,绝大部分情况下第一个是零,第二个是要取的下标。

这样就成功拿到了指向数组第一个元素的指针,是一个 int8* 

然后调用函数完美!

4、对栈上的字符串挨个char 进行赋值

这里写个循环,使用 GEP  访问每个位置的值,使用 StoreInst  存储。

代码里的注释已经很详细了。

5、好,准备完毕,寻找并调用printf

如果printf没有被导入的话,这里其实会产生nullptr 的。为了方便,我们在写c代码时,里面写一句printf的常见调用,就可以让llvm 帮我们处理好这件事。

6、最重要的,一定要给BasicBlock添加Terminator,这个经常会被新手遗忘。

第一部分完结,在栈上创建它,还是很简单的。

第二部分,打印rodata 上的字符串

1、创建BasicBlock、找到printf 函数

2、使用IRB 创建rodata 上的string并获得指向第一个元素的指针

非常方便,IRB一句话搞定

3、调用printf,添加Terminator 的跳转

第三部分,打印rwdata 上的字符串

1、创建BasicBlock、找到printf 函数

2、创建一个全局GV

3、因为默认的是Constant 的,所以要设置一下

4、GEP、调用、跳转

全部代码是:

运行效果:

五、写一个简单的ModulePass

功能:当 c 文件里没有 main 的时候,自动生成 main并且调用当前 module 里的所有函数。

思路是,先看看有没有 main,有的话就终止操作,没有的话就创造一个 main,之后创建BB、创建Inst、完成调用。

1、看看有没有main

M.getFunction("main");  的返回值是否为nullptr

2、创建函数

使用 Function 提供的静态构造方法,需要提供函数类型、连接类型、函数名、所在模块。

这样创造出来的是int main()

3、加入BasicBlock

默认创建出来Function  是没有 BasicBlock  ,编译会挂,所以要主动创建

4、使用IRBuilder 快速完成指令插入

全部代码是:

运行效果是:

六、总结

最开始写起来挺难受的,后面就觉得不难了,难点在于对 API 的不熟悉、对常见的 IR 约定不熟悉,本文讲的是最最最基础的一些操作,真正写混淆器时,遇到的困难比这个多多了,将来会分析前辈们的项目,讲讲实现方式。有机会的话,也会把一些不敏感的Pass 讲一下,但可能性不大。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code