LLVM Pass编写
一、LLVM
LLVM的架构图如下:
它由以下三部分组成:
前端
前端负责解析源码,验证源码的正确性,然后生成IR代码,交付给优化器。LLVM支持两种前端:clang和基于GNU编译器集合解析器的前端
优化器(亦称为中端)
优化器负责删除IR代码中的冗余代码和死代码、简化控制流图等。之后将结果传送给后端。
后端
根据目标架构生成高效的汇编代码
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根据作用可以分为两类:
优化Pass(Optimization Passes)
优化 Pass 用于改善程序的性能、减小程序的大小等。这些 Pass 可以用于改变程序的控制流、数据流以及内存访问模式,以达到改善程序性能的目的。(混淆Pass是一种特殊的优化Pass)
一些常见的优化 Pass 包括:
- 常量折叠(Constant Folding):将常量表达式求值为一个常量。
- 内联(Inlining):将函数调用处替换为函数体的拷贝。
- 死代码消除(Dead Code Elimination):删除不会被执行到的代码。
- 循环优化(Loop Optimization):对循环结构进行优化,如循环展开、循环变量消除等。
分析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 |
|
然后在上级目录(Transforms)下的CMakeLists.txt中,添加新创建的目录:
1 |
|
然后是pass代码的编写:
1 |
|
最后重新执行一遍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 |
|
官方给出的 <project dir>/CMakeLists.txt
的内容并不准确,在执行的过程中会报错:
根据报错内容可知,我们还需要添加cmake_minimum_required(VERSION 3.22)
到第一行,添加project(ProjectName)
到cmake_minimum_required
之后,同时还需要设置LLVMConfig.cmake
的所在路径,具体修改如下图所示:
对应代码如下:
1 |
|
接下来是我们的pass代码编写,这里额外写了一个函数名hash替换:
对该项目的根目录下执行如下指令进行编译:
1 |
|
最后进行测试,两个pass按不同的顺序先后执行:
从这里可以看出,先执行的pass会影响后执行的pass。
五、通过clang执行pass
在第4小节所展示的pass都是通过opt工具执行的,但是我们也可以通过clang进行pass的执行。
脱离llvm源码执行pass
在原有的脱离源码的pass项目(见4.2小节)中,在*pass.cpp
中添加如下代码(导入llvm的库,路径为build/include目录下):
1 |
|
执行如下指令:
1 |
|
当我调换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生效。
lib/Transforms/CMakeLists.txt
修改,添加之前创建的子目录MyPass(在之前已经完成)IPO/CMakeLists.txt
修改,添加MyPass子目录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)