本篇文章介绍如何实现一个简单的Pass, 并在此基础上进行改动。
Pass简介
LLVM Pass 框架是LLVM系统的重要组成部分,它的主要作用是对IR进行分析、转换、优化。编译器开发者将IR分解为不同的处理对象,并将其处理过程实现为单独的Pass类型。在编译器初始化的时候,pass被实例化,并被添加到pass管理器中,pass管理器以流水线的方式将各个独立的pass衔接起来,然后以预定义的顺序遍历每个pass,根据pass实例返回值启动、停止、重复运行不同的pass。因此LLVM Pass的模块主要包括:pass、pass管理器、pass注册以及相关模块。
所有的LLVM Pass都是Pass类的子类,它里面含有很多虚函数供子类继承和重写。我们在编写pass的时候,要根据需求来继承Module Pass、CallGraphSCCPass、FunctionPass、LoopPass、RegionPass、BasicBlockPass等。LLVM Pass框架的一个主要特性就是它根据传递遇到的约束来调度传递以高效的方式运行。
模板代码讲解
我们写的Pass的模板来自 llvm-pass-skeleton ,可以直接使用以下命令clone到合适的位置
$ git clone https://github.com/sampsyo/llvm-pass-skeleton.git
这个模板十分简单,仅仅实现了一些打印的功能,并没有对IR代码进行修改,下面对模板中的代码进行详细讲解:
首先引入头文件:
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
在头文件中的函数存在于llvm的命名空间中,所以我们必须使用 llvm 的命名空间
using namespace llvm;
接下来开始一个新的匿名命名空间,为了引入静态全局作用域,就像C语言的“static”关键字,它使在匿名命名空间中声明的内容仅对当前文件可见:
namespace{
声明我们的Pass,继承自FunctionPass,这是一个对函数进行操作的类,它对初学者来说比较友好:
struct SkeletonPass : public FunctionPass {
我们需要一个标识符,这允许LLVM避免使用C++运行时信息:
static char ID;
SkeletonPass() : FunctionPass(ID) {}
声明一个runOnFunction
方法,覆盖了从FunctionPass继承的抽象虚方法,在这个方法中,我们仅仅打印出一些基本信息:
virtual bool runOnFunction(Function &F) {
errs() << "I saw a function called " << F.getName() << "!\n";
return false;
}
errs()
是 LLVM 提供的 C++ 输出流,我们可以使用它来打印到控制台。函数返回
false
表明它没有修改函数F
. 稍后,当我们真正转换程序时,我们需要 returntrue
。
初始化PassID,LLVM使用ID的地址来标识ID,因此ID的初始化值并不重要
char SkeletonPass::ID = 0;
注册我们的pass
static void registerSkeletonPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new SkeletonPass());
}
static RegisterStandardPasses
RegisterMyPass(PassManagerBuilder::EP_EarlyAsPossible,
registerSkeletonPass);
总的代码如下:
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
using namespace llvm;
namespace {
struct SkeletonPass : public FunctionPass {
static char ID;
SkeletonPass() : FunctionPass(ID) {}
virtual bool runOnFunction(Function &F) {
errs() << "I saw a function called " << F.getName() << "!\n";
return false;
}
};
}
char SkeletonPass::ID = 0;
// Automatically enable the pass.
// http://adriansampson.net/blog/clangpass.html
static void registerSkeletonPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new SkeletonPass());
}
static RegisterStandardPasses
RegisterMyPass(PassManagerBuilder::EP_EarlyAsPossible,
registerSkeletonPass);
编译、运行
构建我们的Pass
$ git clone https://github.com/sampsyo/llvm-pass-skeleton.git
$ cd llvm-pass-skeleton
$ mkdir build
$ cd build
$ cmake ..
$ make
告诉Cmake你的LLVM安装到哪了:
LLVM_DIR = ~/桌面/tools/llvm-12.0.1/llvm/llvm-build/lib/cmake/llvm cmake ..
这会生成一个共享库,位置在build/skeleton/libSkeletonPass.so
创建一个C语言文件
$ vim a.c
int main(){
return 42;
}
调用clang
编译执行 C 程序并使用一些flag来指向刚刚编译的共享库:
bupt@ubuntu-virtual-machine:~/桌面/tools/test_pass/llvm-pass-skeleton$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so a.c
输出:
I saw a function called main!
正常运行!
最终文件树如下:
bupt@ubuntu-virtual-machine:~/桌面/tools/test_pass/llvm-pass-skeleton$ tree -L 2
.
├── a.c
├── a.out
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── Makefile
│ └── skeleton
├── CMakeLists.txt
├── LICENSE
├── README.md
└── skeleton
├── CMakeLists.txt
└── Skeleton.cpp
查看IR的组件结构
IR中的组件结构:
一个模块(Module)代表一个源文件(粗略地)或一个翻译单元。Module是其他LLVM IR的对象的顶级容器,每个Module都直接包含了一系列全局变量、函数、该Module依赖的库(或其他Module)以及有关目标特性的各种数据。
Modules包含Function命名的可执行代码块。(在 C++ 中,函数和方法都对应于 LLVM 函数。)
除了声明其名称和参数外,Function 主要是BasicBlock(BB)的容器。基本块(BB)可以理解为一段连续的指令。
指令是单个代码操作。例如,指令可能是整数加法、浮点除法或存储到内存。
LLVM 中的大多数东西(包括 Function、BasicBlock 和 Instruction)都是从名为Value的基类继承而来。Value是可以在计算中使用的任何数据——例如,数字或某些代码的地址。全局变量和常量(又名literals或immediates)也是Value。
可以在刚才的Skeleton.cpp文件里修改代码,从而查看每一条IR指令
$ vim skeleton/Skeleton.cpp
输出整个函数的IR,遍历函数中的所有basic block,同时遍历basic block中的instruction,将其输出:
errs() << "Function body:\n" << F << "\n";
for (auto& B : F) {
errs() << "Basic block:\n" << B << "\n";
for (auto& I : B) {
errs() << "Instruction: " << I << "\n";
}
}
编译运行:
$ cd ~/桌面/tools/test_pass/llvm-pass-skeleton/build
$ make
$ cd ..
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so a.c
输出结果如下:
可以看到,其中有1个basic block,3个Instruction
修改功能
a.c 代码如下:
int main(int argc, char **argv){
return argc + 5;
}
// argc是参数个数,argv是参数列表。
// 注意,程序本身会有一个参数
我们想保持代码中的+
不变,但实际实现的是x
(乘法)的功能
需要用到IR builder,这是LLVM的一个库
#include "llvm/IR/IRBuilder.h"
先把原函数的输出删去,然后在循环里如下改动:
for (auto& I : B) {
// 获取二元操作符+,检查是否获取到了
if(auto* op = dyn_cast<BinaryOperator>(&I)){
// 指定 op 出现的位置进行代码改写
IRBuilder<> builder(op);
// 从操作数中获取lhs和rhs
Value *lhs = op->getOperand(0);
Value *rhs = op->getOperand(1);
// 用乘法来运算lhs和rhs,保存运算结果
Value *mul = builder.CreateMul(lhs,rhs);
//因为我们要用新的操作符来替换旧的操作符
//所以我们要找到所有使用旧的操作符的地方,并换成新的操作符
for(auto& U :op->uses()){
User* user = U.getUser();
user->setOperand(U.getOperandNo(),mul);
}
// return true来告诉剩余的pipline我们已经改过这个函数了
return true;
}
}
说明:
- dyn_cast
(&I) 是检查强制转换的操作,它检查操作数是否属于指定的类型,如果是则返回指向他的指针,否则返回空,在我们这个例子中,它用来检验I
是不是二元操作符。 - IRBuilder帮助我们创建可能想要的任何类型的指令。
- 将新指令拼接到代码中,我们必须找到它原来的所有位置并将新指令作为参数进行交换。
完整的代码如下:
Skeleton.cpp:
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/IR/InstrTypes.h"
#include "llvm/IR/IRBuilder.h"
using namespace llvm;
namespace {
struct SkeletonPass : public FunctionPass {
static char ID;
SkeletonPass() : FunctionPass(ID) {}
virtual bool runOnFunction(Function &F) {
errs() << "I saw a function called " << F.getName() << "!\n";
for (auto& B : F) {
for (auto& I : B) {
if(auto* op = dyn_cast<BinaryOperator>(&I)){
IRBuilder<> builder(op);
Value *lhs = op->getOperand(0);
Value *rhs = op->getOperand(1);
Value *mul = builder.CreateMul(lhs,rhs);
for(auto& U :op->uses()){
User* user = U.getUser();
user->setOperand(U.getOperandNo(),mul);
}
return true;
}
}
}
return false;
}
};
}
char SkeletonPass::ID = 0;
// Automatically enable the pass.
// http://adriansampson.net/blog/clangpass.html
static void registerSkeletonPass(const PassManagerBuilder &,
编译运行:
$ cd ..
$ cd build
$ make
$ cd ..
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so a.c
看不到什么输出
可以这样检验效果
首先编译
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so a.c
$ clang a.c
然后运行,并传入参数:
结果说明:
echo $?
是询问返回码,即return的值,源文件没有变,输出:argc+5
可以看到当不给a.out传入参数的时候,默认参数个数是1,输出5,即1*5
当给a.out传入3个参数的时候,参数个数是4,输出20,即4*5
说明实现了用x
(乘法)替换+
的功能:)
参考
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1149440709@qq.com