0x01 Xcode调试一个LLVM Pass

前言

之前写过一篇《关于LLVM,这些东西你必须知道!》,先已收录在《iOS成长之路》。
其中讲解了编译器的编译过程,以及通过LLVM可以做哪些有意思的事情,今天继续来跟着官方的文档继续了解Pass。

获取代码:

1
2
3
4
git clone http://llvm.org/git/llvm.git
git clone http://llvm.org/git/clang.git llvm/tools/clang
git clone http://llvm.org/git/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone http://llvm.org/git/compiler-rt.git llvm/projects/compiler-rt

编译成Xcode项目:

1
2
3
4
5
mkdir build
cd build
cmake -G Xcode CMAKE_BUILD_TYPE="Debug" ../llvm

open LLVM.xcodeproj

然后运行clang,准备工作做好后,继续Pass的学习。

什么是Pass?

LLVM Pass是LLVM系统中非常重要和有趣的一个模块,Pass处理编译过程中代码的转换以及优化工作。

所有的Pass都是Pass的子类,不同的Pass实现不同的作用可以继承ModulePass,CallGraphSCCPass,FunctionPass,LoopPass,RegionPass,BasicBlockPass等类。

接下来开始介绍如何编写一个Pass,编译,加载以及运行的过程。

编写hello world

先编写一个“Hello” Pass,只是简单的输出程序中每个非外部方法的方法名,不会修改程序本身的功能。官方Demo的源代码存在lib/Transforms/Hello文件夹。

配置编译环境

首先来看看环境的配置,如果你想编译一个新的Pass,你可以在lib/Transforms/下新建一个文件夹,这里我自己创建的是PassDemo,然后配置编译脚本去编译你的源代码。 在新建的目录下创建文件CMakeLists.txt:

1
2
3
4
5
6
add_llvm_loadable_module(LLVMPassDemo
Hello.cpp

PLUGIN_TOOL
opt
)

然后增加下面一行代码到lib/Transforms/CMakeLists.txt

1
add_subdirectory(PassDemo)

编译脚本指定了编译源文件Hello.cpp去编译生成动态库$(LEVEL)/lib/LLVMPassDemo.dylib。该文件可以被opt通过-load参数动态加载。如果不是Mac平台生成的后缀会不一样,比如Linux下是so

配置好编译脚本后,打开Xcode项目,按command + B编译,然后在项目里面就可以看到刚刚增加的Target,打开Hello.cpp文件就可以编写代码了。

image

基础代码

首先编辑Hello.cpp文件,导入头文件:

1
2
3
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

因为需要编一个Pass然后操作Function, 还可能输出一些东西。

1
using namespace llvm;

由于导入文件里面的方法是属于llvm命名空间的,所以先指定使用的命名空间。

1
namespace {

编写一个匿名的命名空间,C++中的匿名命名空间和C语言里面的static关键词一样,定义在匿名空间中的变量仅仅对当前文件可见。

1
struct Hello : public FunctionPass{

定义Hello类继承FunctionPass,后面会讲到不同用处的Pass会继承不同的父类,现在只需要知道,继承自FunctionPass可以操作程序中的方法。

1
2
static char ID;
Hello() : FunctionPass(ID){}

定义可供LLVM标示的Pass ID。

1
2
3
4
5
6
7
        bool runOnFunction(Function &F) override{
errs() << "Hello: ";
errs().write_escaped(F.getName()) << "\n";
return false;
}
};
}

这里定义了一个runOnFunction,重载继承自父类的抽象虚函数。在这个函数里面可以做针对于函数的特殊处理,这里只是打印出每个方法的名字。

1
char Hello::ID = 0;

初始化Pass ID,LLVM使用ID的地址去识别一个Pass,所以初始化的值不重要。

1
static RegisterPass<Hello> X("hello", "Hello World Pass", false/* Only looks at CFG */, false/* Analysis Pass */);

最后,注册我们的class Hello, 指定命令行参数为hello,名字说明Hello World Pass

整个Hello.cpp文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
struct Hello : public FunctionPass{
static char ID;

Hello() : FunctionPass(ID){}

bool runOnFunction(Function &F) override{
errs() << "Hello: ";
errs().write_escaped(F.getName()) << "\n";
return false;
}
};
}

char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass", false/* Only looks at CFG */, false/* Analysis Pass */);

选中Target LLVMPassDemocommand + B编译,编译完后会生成文件build/Debug/lib/LLVMPassDemo.dylib

使用opt加载

上面在代码中通过RegisterPass注册了Pass,所以可以通过opt -load去加载动态库并指定参数hello

准备源文件:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int add(int x, int y) {
return x + y;
}

int main(){
printf("%d",add(3,4));
return 0;
}

编译源文件生成bitcode:

1
path/to/build/Debug/bin/clang -emit-llvm -c test.mm -o test.bc

然后编译启动参数, 选择opttarget, 点击Edit Scheme

image

填入启动参数:

image

command + R运行。

输出如下:

1
2
Hello: _Z3addii
Hello: main

当然还可以断点调试啦~

image

通过-help可以看到注册的pass参数:

1
opt -load lib/LLVMPassDemo.dylib -help
1
2
3
4
......
Optimizations available:
-hello - Hello World Pass
......

PassManager还提供--time-passes参数输出你的Pass和其它Pass执行时间的对比。

1
opt -load lib/LLVMPassDemo.dylib -hello -time-passes -disable-output test.bc
1
2
3
4
5
6
7
8
9
10
11
12
13
Hello: _Z3addii
Hello: main
===-------------------------------------------------------------------------===
... Pass execution timing report ...
===-------------------------------------------------------------------------===
Total Execution Time: 0.0003 seconds (0.0003 wall clock)

---User Time--- --System Time-- --User+System-- ---Wall Time--- --- Name ---
0.0002 ( 75.3%) 0.0000 ( 27.9%) 0.0002 ( 62.1%) 0.0002 ( 62.7%) Module Verifier
0.0001 ( 24.7%) 0.0001 ( 72.1%) 0.0001 ( 37.9%) 0.0001 ( 37.3%) Hello World Pass
0.0002 (100.0%) 0.0001 (100.0%) 0.0003 (100.0%) 0.0003 (100.0%) Total

===-------------------------------------------------------------------------===

tips:
为了在执行opt时能自动检测LLVMPassDemo模块有没有被修改,如果修改了,重新编译LLVMPassDemo模块,我们把LLVMPassDemo模块加到opt的依赖里面。

编辑tools/opt/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
add_llvm_tool(opt
AnalysisWrappers.cpp
BreakpointPrinter.cpp
GraphPrinters.cpp
NewPMDriver.cpp
PassPrinters.cpp
PrintSCC.cpp
opt.cpp

DEPENDS
intrinsics_gen
LLVMPassDemo
)

更多Pass类

当你写一个新的Pass的时候,你需要根据需要去选择需要继承的父类,根据不同的作用有 ImmutablePassModulePassCallGraphSCCPassFunctionPassLoopPassRegionPassBasicBlockPassMachineFunctionPass。具体可以参考官方文档

调用其它Pass

如果在编写的Pass中需要使用到其它Pass提供的函数功能,需要在:

1
virtual void getAnalysisUsage(AnalysisUsage &AU) const;

说明,比如像要获取程序中存在循环的信息,可以在该函数里面申请需要依赖的Pass。

以上面的Pass为例,导入头文件#include "llvm/Analysis/LoopInfo.h",在getAnalysisUsage中调用:

1
2
AU.addRequired<LoopInfoWrapperPass>();
AU.setPreservesAll();

然后就可以通过它提供接口获取存在循环的个数。

1
2
3
4
5
6
LoopInfo &LI = getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
int loopCounter = 0;
for (LoopInfo::iterator i = LI.begin(), e = LI.end(); i != e; ++i) {
loopCounter++;
}
errs() << "loop num:" << loopCounter << "\n";

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int add(int x, int y) {
for (int i = 0; i < 10; i++) {
printf("%d\n",i);
}
return x + y;
}

int main(){
printf("%d",add(3,4));
return 0;
}

输出如下:

1
2
3
4
5
Hello: _Z3addii
loop num:1
Hello: main
loop num:0
Program ended with exit code: 0
AloneMonkey wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!