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

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

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

一、Pass简介和构成

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

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

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

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

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

1
2
3
virtual StringRef getPassName() const;
virtual bool doInitialization(Module &) { return false; }
virtual bool doFinalization(Module &) { return false; }

主要用的也就两个, FunctionPassModulePass

1
2
virtual bool FunctionPass::runOnFunction(Function &F) = 0;
virtual bool ModulePass::runOnModule(Module&M)=0;

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace llvm {
class ShowName : public FunctionPass {
public:
static char ID;

ShowName() : FunctionPass(ID) {}

bool runOnFunction(Function &F) override;

virtual StringRef getPassName() const override {
return "Show Name";
}
};

char ShowName::ID = 0;
}
bool ShowName::runOnFunction(Function &F) {}

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

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

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

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

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

1
2
3
4
5
6
struct RegisterStandardPasses {
RegisterStandardPasses(PassManagerBuilder::ExtensionPointTy Ty,
PassManagerBuilder::ExtensionFn Fn) {
PassManagerBuilder::addGlobalExtension(Ty, std::move(Fn));
}
};

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

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

1
2
3
4
5
6
7
8
9
10
11
12
enum ExtensionPointTy {
EP_EarlyAsPossible,
EP_ModuleOptimizerEarly,
EP_LoopOptimizerEnd,
EP_ScalarOptimizerLate,
EP_OptimizerLast,
EP_VectorizerStart,
EP_EnabledOnOptLevel0,
EP_Peephole,
EP_LateLoopOptimizations,
EP_CGSCCOptimizerLate,
};

我一般只用3个, EP_EarlyAsPossibleEP_OptimizerLastEP_EnabledOnOptLevel0

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

EP_EnabledOnOptLevel0 在-O0 情况下触发;

EP_OptimizerLast 在非-O0情况下触发;

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

1
2
3
typedef
std::function<void(const PassManagerBuilder &Builder, legacy::PassManagerBase &PM)>
ExtensionFn;

表示使用的函数签名是

void(const PassManagerBuilder &Builder,legacy::PassManagerBase &PM)

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

例如:

1
2
3
4
5
6
7
static void registerPllvmEarly(const PassManagerBuilder &, legacy::PassManagerBase &PM) {
PM.add(new ShowName());
}

static RegisterStandardPasses
RegisterMyPass(PassManagerBuilder::EP_EarlyAsPossible,
registerPllvmEarly);

三、使用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 打印这个指针。

1、在EntryBlock 前插入我们的printfBlock

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

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

2、创建char 数组。

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

1
2
3
// type: i8[size+1]
ArrayType *arrayType = ArrayType::get(IntegerType::getInt8Ty(F.getContext()), function_string.size() + 1);
AllocaInst *alloc = IRB.CreateAlloca(arrayType);

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 。 代码里这么写:

1
Function *func_memset = Intrinsic::getDeclaration(F.getParent(), Intrinsic::memset,{IntegerType::getInt8PtrTy(F.getContext()), IntegerType::getInt64Ty(F.getContext())});

第二个参数,是表示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 帮助我们构造它,例如这个例子:

1
2
Value *Zero = ConstantInt::get(Type::getInt8Ty(F.getContext()), 0); 
Value *str = IRB.CreateGEP(alloc, {Zero, Zero});

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

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

这样就成功拿到了指向数组第一个元素的指针,是一个int8* 。 然后调用函数,完美!

1
IRB.CreateCall(func_memset, {str, Zero, Size, Bool});

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

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

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < function_string.size(); i++) {
// i64 index
Value *Index = ConstantInt::get(Type::getInt64Ty(F.getContext()), i);
// i8 char
Value *CDATA = ConstantInt::get(Type::getInt8Ty(F.getContext()), function_string[i]);
// get ptr bufferr[i]
Value *GEP = IRB.CreateGEP(alloc, {Zero, Index});
// store data
IRB.CreateStore(CDATA, GEP);
}

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

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

1
2
Function *func_printf = F.getParent()->getFunction("printf");
IRB.CreateCall(func_printf, {str});

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

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

1
IRB.CreateBr(entryBlock);

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

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

1、创建BasicBlock、找到printf 函数

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

Value *str=IRB.CreateGlobalStringPtr(function_string);

非常方便,IRB一句话搞定

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

1
2
3
4
// create printf(funcName)
IRB.CreateCall(func_printf, {str});
// create branch to entryBlock
IRB.CreateBr(entryBlock);

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

1、创建BasicBlock、找到printf 函数

2、创建一个全局GV

GlobalVariable *GV = IRB.CreateGlobalString(function_string);

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

GV->setConstant(false);

4、GEP、调用、跳转

全部代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Created by LeadroyaL on 2018/10/16.
//

#ifndef _PLLVM_PROJECT_SHOWNAME_H
#define _PLLVM_PROJECT_SHOWNAME_H

#include "llvm/Pass.h"

namespace llvm {
Pass *createShowName();
void initializeShowNamePass(PassRegistry &Registry);
}


#endif //_PLLVM_PROJECT_SHOWNAME_H
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//
// Created by LeadroyaL on 2018/10/16.
//

#include <string>

#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/IntrinsicInst.h"

#include "ShowName.h"

using namespace std;
using namespace llvm;

/*
* 简单的用来练手的,在每个函数被调用时,先打印函数名,分别在栈、data、rodata 存放函数名,测试用的。
* */
namespace llvm {
class ShowName : public FunctionPass {
public:
static char ID;

ShowName() : FunctionPass(ID) {}

bool runOnFunction(Function &F) override;

virtual StringRef getPassName() const override {
return "Show Name";
}
};

Pass *createShowName() {
return new ShowName();
}

char ShowName::ID = 0;
}

void declarePrintf(Function &F);

void insertRodata(Function &F);

void insertData(Function &F);

void insertStack(Function &F);

bool ShowName::runOnFunction(Function &F) {
errs() << "enter ShowName " << F.getName() << "n";
declarePrintf(F);
insertRodata(F);
insertData(F);
insertStack(F);
return true;
}

void declarePrintf(Function &F) {
Function *func_printf = F.getParent()->getFunction("printf");
if (!func_printf) {
FunctionType *FT = FunctionType::get(Type::getInt8PtrTy(F.getContext()), true);
Function::Create(FT, Function::ExternalLinkage, "printf", F.getParent());
}
}

void insertRodata(Function &F) {
// get entry block
BasicBlock *entryBlock = &F.getEntryBlock();
// insert printfBlock before entry block
BasicBlock *printfBlock = BasicBlock::Create(F.getContext(), "printfRodataBlock", &F, entryBlock);
// create function_string
std::string function_string = "Function ";
function_string += F.getName();
function_string += " is invoked! @rodatan";
// insert at end of printfBlock
IRBuilder<> IRB(printfBlock);
// find printf
Function *func_printf = F.getParent()->getFunction("printf");
if (!func_printf)
assert(false && "printf not found");
// create string ptr
Value *str = IRB.CreateGlobalStringPtr(function_string);
// create printf(funcName)
IRB.CreateCall(func_printf, {str});
// create branch to entryBlock
IRB.CreateBr(entryBlock);
}

void insertData(Function &F) {
// get entry block
BasicBlock *entryBlock = &F.getEntryBlock();
// insert printfBlock before entry block
BasicBlock *printfBlock = BasicBlock::Create(F.getContext(), "printfDataBlock", &F, entryBlock);
// create function_string
std::string function_string = "Function ";
function_string += F.getName();
function_string += " is invoked! @datan";
// insert at end of printfBlock
IRBuilder<> IRB(printfBlock);
// find printf
Function *func_printf = F.getParent()->getFunction("printf");
if (!func_printf)
assert(false && "printf not found");
// create string ptr
GlobalVariable *GV = IRB.CreateGlobalString(function_string);
// set writable
GV->setConstant(false);
// i32 0
Constant *Zero = ConstantInt::get(Type::getInt32Ty(F.getContext()), 0);
// for index
Constant *Indices[] = {Zero, Zero};
// get ptr buffer[0]
Value *str = ConstantExpr::getInBoundsGetElementPtr(GV->getValueType(), GV, Indices);
// create printf(funcName)
IRB.CreateCall(func_printf, {str});
// create branch to entryBlock
IRB.CreateBr(entryBlock);
}

void insertStack(Function &F) {
// get entry block
BasicBlock *entryBlock = &F.getEntryBlock();
// insert printfBlock before entry block
BasicBlock *printfBlock = BasicBlock::Create(F.getContext(), "printfStackBlock", &F, entryBlock);
// create function_string
std::string function_string = "Function ";
function_string += F.getName();
function_string += " is invoked! @stackn";
// insert at end of printfBlock
IRBuilder<> IRB(printfBlock);
// find printf
Function *func_printf = F.getParent()->getFunction("printf");
if (!func_printf)
assert(false && "printf not found");
// i8 0
Value *Zero = ConstantInt::get(Type::getInt8Ty(F.getContext()), 0);
// i64 size
Value *Size = ConstantInt::get(Type::getInt64Ty(F.getContext()), function_string.size() + 1);
// i1 bool
Value *Bool = ConstantInt::get(Type::getInt1Ty(F.getContext()), 0);
// type: i8[size]
ArrayType *arrayType = ArrayType::get(IntegerType::getInt8Ty(F.getContext()), function_string.size() + 1);
// find llvm.memset.p0i8.i64
Function *func_memset = Intrinsic::getDeclaration(F.getParent(), Intrinsic::memset, {IntegerType::getInt8PtrTy(F.getContext()), IntegerType::getInt64Ty(F.getContext())});
// new i8[size]
AllocaInst *alloc = IRB.CreateAlloca(arrayType);
// align 16
alloc->setAlignment(16);
// get ptr buffer[0]
Value *str = IRB.CreateGEP(alloc, {Zero, Zero});
IRB.CreateCall(func_memset, {str, Zero, Size, Bool});

for (int i = 0; i < function_string.size(); i++) {
// i64 index
Value *Index = ConstantInt::get(Type::getInt64Ty(F.getContext()), i);
// i8 char
Value *CDATA = ConstantInt::get(Type::getInt8Ty(F.getContext()), function_string[i]);
// get ptr bufferr[i]
Value *GEP = IRB.CreateGEP(alloc, {Zero, Index});
// store data
IRB.CreateStore(CDATA, GEP);
}
// create printf(funcName)
IRB.CreateCall(func_printf, {str});
// create branch to entryBlock
IRB.CreateBr(entryBlock);
}
1
2
3
4
5
#include<stdio.h>
int main(){
printf("HelloWorld!n");
return 0;
}

运行效果:

1
2
3
4
5
6
7
8
➜  /tmp clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include -Xclang -load -Xclang libmypass.so -w test.c -o test.bin
enter ShowName main

➜ /tmp ./test.bin
Function main is invoked! @stack
Function main is invoked! @data
Function main is invoked! @rodata
HelloWorld!

五、写一个简单的ModulePass

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

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

1、看看有没有main

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

2、创建函数

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

1
2
FunctionType *FT = FunctionType::get(Type::getInt32Ty(M.getContext()),false);
F = Function::Create(FT,GlobalValue::LinkageTypes::ExternalLinkage,"main",&M);

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

3、加入BasicBlock

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

BasicBlock *EntryBB = BasicBlock::Create(M.getContext(), "EntryBlock", F);

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

1
2
3
4
5
6
7
8
9
IRBuilder<> IRB(EntryBB);
for (Function &FF:M) {
if(FF.getName() == "main")
continue;
if(FF.empty())
continue;
IRB.CreateCall(&FF);
}
IRB.CreateRet(ConstantInt::get(Type::getInt32Ty(M.getContext()), 0));

全部代码是:

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
43
44
45
46
47
48
49
50
51
52
53
54
//
// Created by LeadroyaL on 2018/11/25.
//

#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/IntrinsicInst.h"

#include "SimpleInvoker.h"

using namespace std;
using namespace llvm;

namespace llvm {
class SimpleInvoker : public ModulePass {
public:
static char ID;

SimpleInvoker() : ModulePass(ID) {}

bool runOnModule(Module &M) override;

virtual StringRef getPassName() const override {
return "Simple Invoker";
}
};

Pass *createSimpleInvoker() {
return new SimpleInvoker();
}

char SimpleInvoker::ID = 0;
}

bool SimpleInvoker::runOnModule(Module &M) {
Function *F = M.getFunction("main");
if (F) {
errs() << "Main Function Found! So return.n";
return true;
}
errs() << "Main Function Not Found! So create.n";
FunctionType *FT = FunctionType::get(Type::getInt32Ty(M.getContext()), false);
F = Function::Create(FT, GlobalValue::LinkageTypes::ExternalLinkage, "main", &M);
BasicBlock *EntryBB = BasicBlock::Create(M.getContext(), "EntryBlock", F);
IRBuilder<> IRB(EntryBB);
for (Function &FF:M) {
if(FF.getName() == "main")
continue;
if(FF.empty())
continue;
IRB.CreateCall(&FF);
}
IRB.CreateRet(ConstantInt::get(Type::getInt32Ty(M.getContext()), 0));
return true;
}
1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

void f1(){
printf("f1n");
}

int f2(){
printf("f2n");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
// Created by LeadroyaL on 2018/11/25.
//

#ifndef _PLLVM_PROJECT_SIMPLEINVOKER_H
#define _PLLVM_PROJECT_SIMPLEINVOKER_H

#include "llvm/Pass.h"

namespace llvm {
Pass *createSimpleInvoker();
void initializeSimpleInvokerPass(PassRegistry &Registry);
}

#endif //_PLLVM_PROJECT_SIMPLEINVOKER_H

运行效果是:

1
2
3
4
5
6
➜  /tmp clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include -Xclang -load -Xclang libmypass.so -w test.c -o test.bin
Main Function Not Found! So create.

➜ /tmp ./test.bin
f1
f2

六、总结

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