所有的LLVM pass都是Pass类的子类,他们都继承并重写了Pass类中所定义的虚函数。根据需求,我们可以选择继承ModulePassCallGraphSCCPassFunctionPass 或者LoopPass或者RegionPass 等类。通过继承这些父类,我们可以告诉编译系统我们的自定义pass大概要做哪些事情,并且如何去与其他的pass进行组合。

接下来我们尝试写一个简单的pass,从编码,到编译,再到运行,最后讨论如何编写一个高级的pass。

环境设置

下面我们编写一个Hello World pass。我们的pass将实现一个非常简单的功能--打印函数名,这意味着它不会修改源程序,只是简单地打印源程序的相关信息。

首先,配置并构建LLVM项目,然后在LLVM源码文件夹下新建一个目录。遵照官方文档,我们在llvm源码树的lib/Transforms目录下新建一个example目录。

然后将下面的代码添加到lib/Transforms/CMakeLists.txt中

add_subdirectory(example)

这样CMake会将example.cpp文件链接到$(LEVEL)/lib/LLVMHello.so动态库文件中,在使用opt工具时只需要使用-load选项即可将该动态库链接进来。

编写代码

接下来新建example.cpp作为源码文件。

首先引入LLVM相关的头文件:

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

Pass头文件中有我们需要的父类,而我们将编写的是Function相关的pass,所以需要引入function.h。

然后引入llvm的命名空间:

using namespace llvm;

由于该pass只是个示例,所以我们接下来创建一个匿名的命名空间:

namespace {
...
}

匿名命名空间中的内容仅在当前文件中是可见的。

接下来,声明我们的自定义pass本体:

struct example : public FunctionPass {
...
}

我们将example声明为FunctionPass的子类。稍后我们会介绍一些其他的内置pass子类,但是现在只需要记住,FunctionPass可以对函数进行各种操作。

然后声明Pass标识符,会被LVM用作识别pass:

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

接下来继承并重写FunctionPass中的runOnFunction虚函数:

bool runOnFunction(Function& F) override {
errs() << "Hello, this is example!";
errs().write_escaped(F.getName()) << "\n";
return false;
}

然后,初始化pass ID。由于LLVM只是使用ID来定位一个pass,所以这个ID的初始值并不重要。

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

最后,将我们的example类作为一个新的pass注册,命令行参数是exp,名字是Hello World Pass

最后两个参数描述了它的行为:如果pass不修改CFG,那么第三个参数被设为true;如果该pass是一个分析pass,例如支配树pass,则第四个参数为true。

如果我们希望将pass注册为现有管道的一个步骤,LLVM也提供了一些扩展点。例如PassManagerBuilder::EP_EarlyAsPossible会在其他优化进行之前调用我们的pass,又或者PassManagerBuilder::EP_FullLinkTimeOptimizationLast会在链接优化之后调用我们的pass。

static llvm::RegisterStandardPasses Y(
llvm::PassManagerBuilder::EP_EarlyAsPossible,
[](const llvm::PassManagerBuilder &Builder,
llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });

最后,整个文件如下:

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


using namespace llvm;

namespace {
struct example : public FunctionPass {
static char ID;
example() : Function(ID) {

}

bool runOnFunction(Function& F) override {
errs() << "Hello, this is example!";
errs().write_escaped(F.getName()) << "\n";
return false;
}
}
}

char example::ID = 0;

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

最后,回到LLVM源码树的根目录,再次build项目,就可以在build文件夹下的lib文件夹中看到生成的LLVMExample.dylib共享库文件(macOS)。

在opt工具中使用

首先创建一个test.c文件:

#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}

然后生成llvm的bitcode:

clang -O3 -emit-llvm test.c -c -o test.bc

最后加载运行即可:

~/Projects/llvm-project/build/bin/opt -enable-new-pm=0 -load ~/Projects/llvm-project/build/lib/LLVMExample.dylib -example test.bc > /dev/null

输出如下:

Hello, this is example: main

目前最新的LLVM已经使用了新的pass管理器来管理优化管道(新的pass管理器使用了新的编写pass的方法),但代码生成管道仍使用旧的pass管理器(如本文所述)。所以要在opt中使用旧的pass管理器,需要传入-enable-new-pm=0参数。

参考文献

Writing an LLVM Pass

《LLVM Cookbook》 Mayur Pandey