LLVM Pass编写

一、LLVM

LLVM的架构图如下:

它由以下三部分组成:

  1. 前端

    前端负责解析源码,验证源码的正确性,然后生成IR代码,交付给优化器。LLVM支持两种前端:clang和基于GNU编译器集合解析器的前端

  2. 优化器(亦称为中端)

    优化器负责删除IR代码中的冗余代码和死代码、简化控制流图等。之后将结果传送给后端。

  3. 后端

    根据目标架构生成高效的汇编代码

LLVM独立于语言和平台。如果需要支持一种新的编程语言,那么只需要在LLVM中实现一个新的前端即可。如果需要支持一种新的硬件设备,那么只需要在LLVM中实现一个新的后端即可。无论是怎样的前端后端,代码进行编译优化等操作都会借助IR代码来进行。

LLVM项目中常见几个工具:

  • clang:LLVM 中前端的编译器,用于将源代码编译成IR代码。
  • llvm-as:LLVM的汇编器,将IR代码成机器码(二进制)。
  • llvm-dis:LLVM的反汇编器,即将机器码转成IR代码。
  • opt:LLVM的通用优化器和分析器,对IR代码进行优化和分析。
  • llc:LLVM 中后端的静态编译器,用于将IR转换成目标平台的汇编代码。
  • lli:LLVM的直接执行引擎,用于解释执行IR代码。

二、什么是IR

LLVM IR(Low Level Virtual Machine Intermediate Representation)是一个中间代码表示形式,是LLVM编译器框架的核心部分。这种中间表示形式有助于编译器在进行优化、代码生成和分析时更高效地操作代码。

详细可参考PowerPoint Presentation (llvm.org)

直接看个明显的例子更能够帮助理解(我个人感觉IR跟汇编代码类似,只不过汇编代码的指令跟目标架构有关):

三、什么是pass

LLVM Pass框架是LLVM系统的重要部分,它用于转换、优化,生成分析结果。Pass 代表了 LLVM 中的一种特定的优化或者分析操作,可以作用于 LLVM 的中间表示(Intermediate Representation,IR)上。

所有 LLVM pass 都是 Pass 类的子类,它们通过重写从 Pass 继承的虚拟方法来实现功能,例如从 ModulePass 、 CallGraphSCCPass 、 FunctionPass 、 LoopPass 或 RegionPass 类继承。

Pass根据作用可以分为两类:

  1. 优化Pass(Optimization Passes)

    优化 Pass 用于改善程序的性能、减小程序的大小等。这些 Pass 可以用于改变程序的控制流、数据流以及内存访问模式,以达到改善程序性能的目的。(混淆Pass是一种特殊的优化Pass)

    一些常见的优化 Pass 包括:

    • 常量折叠(Constant Folding):将常量表达式求值为一个常量。
    • 内联(Inlining):将函数调用处替换为函数体的拷贝。
    • 死代码消除(Dead Code Elimination):删除不会被执行到的代码。
    • 循环优化(Loop Optimization):对循环结构进行优化,如循环展开、循环变量消除等。
  2. 分析Pass(Analysis Passes)

    分析 Pass 用于收集程序的信息,而不会改变程序的行为。这些 Pass 通常用于了解程序的结构、性能特征或者用于其他优化 Pass 的依赖。

    一些常见的分析 Pass 包括:

    • 数据流分析(Data Flow Analysis):用于分析数据在程序中的流动。
    • 依赖分析(Dependency Analysis):分析程序中各个部分之间的依赖关系。
    • 内存分析(Memory Analysis):用于分析程序中的内存使用情况。

四、通过opt执行pass

4.1 依赖源码执行pass

详细可参考https://llvm.org/docs/WritingAnLLVMPass.html#introduction-what-is-a-pass。

其实在LLVM项目的llvm-project/llvm/lib/Transforms/目录下,有一个官方给出的学习案例(Hello文件)。我们可以根据这个案例,来写一写自己的pass。

假定我们在Transforms目录下创建一个MyPass文件夹,该文件夹同样含有MyPass.cpp、CMakeLists.txt。

在MyPass目录下的CMakeLists.txt中,我们需要加入以下内容(注释用于解释):

1
2
3
4
5
6
add_llvm_library( //表示向LLVM系统中添加一个库或者插件
LLVMMyPass MODULE //MODULE指定要创建的是模块,且模块名为LLVMMyPass
MyPass.cpp //表示这个模块是通过编译MyPass.cpp文件来创建的
PLUGIN_TOOL //表示将这个模块作为一个插件工具 (PLUGIN_TOOL) 被使用
opt //指定PLUGIN_TOOL为opt,即通过opt能使用这个模块的功能
)

然后在上级目录(Transforms)下的CMakeLists.txt中,添加新创建的目录:

1
add_subdirectory(MyPass)

然后是pass代码的编写:

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

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

MyPass() : FunctionPass(ID){}

bool runOnFunction(Function &F) override{
errs() << "MyPass: ";
errs().write_escaped(F.getName()) << '\n';
return false;
}
};
}
char MyPass::ID = 1;
// Register for opt
static RegisterPass<MyPass> X("mypass", "Welcome To My Pass");

最后重新执行一遍llvm的编译流程,由于之前已经编译过,因此只会编译新增的pass。

通过opt工具执行我们新增的pass,结果如下图所示:

没错,我们写的pass执行了!

4.2 脱离源码执行pass

详细可参考https://releases.llvm.org/8.0.1/docs/CMake.html#developing-llvm-passes-out-of-source。

我们创建的pass项目的目录结构如下:

1
2
3
4
5
6
7
8
<project dir>/
|
CMakeLists.txt
<pass name>/
|
CMakeLists.txt
Pass.cpp
...

官方给出的 <project dir>/CMakeLists.txt的内容并不准确,在执行的过程中会报错:

根据报错内容可知,我们还需要添加cmake_minimum_required(VERSION 3.22)到第一行,添加project(ProjectName)cmake_minimum_required之后,同时还需要设置LLVMConfig.cmake的所在路径,具体修改如下图所示:

对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.22)
project(gal2xyPass)

set(LLVM_DIR /home/gal2xy/Desktop/llvm-project-llvmorg-14.0.6/build/lib/cmake/llvm)
find_package(LLVM REQUIRED CONFIG)

list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(AddLLVM)

add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})

add_subdirectory(gal2xy)

接下来是我们的pass代码编写,这里额外写了一个函数名hash替换:

对该项目的根目录下执行如下指令进行编译:

1
2
3
4
mkdir build
cd build
cmake ..
make

最后进行测试,两个pass按不同的顺序先后执行:

从这里可以看出,先执行的pass会影响后执行的pass。

五、通过clang执行pass

在第4小节所展示的pass都是通过opt工具执行的,但是我们也可以通过clang进行pass的执行。

脱离llvm源码执行pass

在原有的脱离源码的pass项目(见4.2小节)中,在*pass.cpp中添加如下代码(导入llvm的库,路径为build/include目录下):

1
2
3
4
5
6
7
8
9
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/IR/LegacyPassManager.h"
// Register for clang
static RegisterStandardPasses Y(
PassManagerBuilder::EP_EarlyAsPossible,
[](const PassManagerBuilder &Builder, legacy::PassManagerBase &PM) { //[](){}是lambda表达式
PM.add(new PassName());//PassName记得替换
}
);

执行如下指令:

1
clang -Xclang -load -Xclang filename.so -flegacy-pass-manager filename.cpp -o filename.ll

当我调换CMakeLists.txt中logPass.cpp和namePass.cpp的顺序后,结果如下:

可见这种方式下pass的执行顺序是根据pass的添加顺序决定的。

依赖llvm源码执行pass

这个方法跟OLLVM移植到LLVM很相似(嗯,其实就是一样的)。

以依赖源码编写pass的例子为基础(见4.1小节),我们需要在llvm/include/llvm/Transforms下创建文件夹(我这里名字为MyPass),然后在里面创建一个头文件(MyPass.h),跟着OLLVM中的Obfuscation中的pass照猫画虎。

然后需要修改MyPass.cpp的代码:

通过执行clang指令时,如果我们输入了-mypass,那么flag就是true,会执行我们写的输出,否则flag就是false。

然后是llvm/lib/Transforms/MyPass目录下的CMakeList.txt文件也需要更新:

最后是将我们写的pass注册到clang中,也就是使我们的pass生效。

  1. lib/Transforms/CMakeLists.txt修改,添加之前创建的子目录MyPass(在之前已经完成)

  2. IPO/CMakeLists.txt修改,添加MyPass子目录

  3. IPO/PassManagerBuilder.cpp修改

    导入MyPass.h库

    1
    #include "llvm/Transforms/MyPass/MyPass.h"

    添加如下代码,使得命令行中能有我们写的pass的选项,这样一来我们可以通过命令行参数来控制程序是否启用pass功能。

    MyPass函数的第一个参数表示使用该pass时应该输入的命令行参数,第二个参数表示该pass默认为不使用,第三个参数是描述pass的。

    最后在PassManagerBuilder中添加pass模块。

以上修改完毕后,对llvm进行编译并进行测试,结果如下:

没错,我们成功的将自己的pass移植到llvm中了!


参考:

https://llvm.org/docs/WritingAnLLVMPass.htm

https://releases.llvm.org/8.0.1/docs/CMake.html#developing-llvm-passes-out-of-source

从LLVM到OLLVM学习笔记 | Whitebird’s Home (whitebird0.github.io)

PowerPoint Presentation (llvm.org)

Passes.pdf (llvm.org)


LLVM Pass编写
http://example.com/2024/04/22/LLVM and OLLVM/LLVM-Pass编写/
作者
gla2xy
发布于
2024年4月22日
许可协议