最近做的工作要使用LLVM IR来进行编程,借这篇文章来整理一下最近学到的一些东西。
LLVM是一个非常有名的编译器基础设施。访问LLVM官方网站获得更多信息。
LLVM IR简介
LLVM有自己的一套中间表示IR(Intermedia Representation)。IR在编译器中承担着一个承前启后的角色。编译器前端对源程序进行语法和语义分析,生成IR。编译器后端则将IR汇编成对应的机器指令。此外,编译器中大部分的优化都是在IR上完成的。
LLVM的IR功能强大,其形式类似于RISC机器的指令。LLVM可以使用解释来执行IR,也可以利用JIT将IR翻译成对应的机器指令。
LLVM IR相关概念
使用LLVM IR编程要涉及到Module, Function, BasicBlock, Instruction, ExecutionEngine等概念。下面对这些概念进行一个简单的说明。
Module
可以将LLVM中的Module类比为C程序中的源文件。一个C源文件中包含函数和全局变量定义、外部函数和外部函数声明,一个Module中包含的内容也基本上如此,只不过C源文件中是源码来表示,Module中是用IR来表示。
Function
Function是LLVM JIT操作的基本单位。Function被Module所包含。LLVM的Function包含函数名、函数的返回值和参数类型。Function内部则包含BasicBlock。
BasicBlock
BasicBlock与编译技术中常见的基本块(basic block)的概念是一致的。BasicBlock必须以跳转指令结尾。
Instruction
Instruction就是LLVM IR的最基本单位。Instruction被包含在BasicBlock中。
ExecutionEngine
ExecutionEngine是用来运行IR的。运行IR有两种方式:解释运行和JIT生成机器码运行。相应的ExecutionEngine就有两种:Interpreter和JIT。ExecutionEngine的类型可以在创建ExecutionEngine时指定。
LLVM IR编程基本流程
- 创建一个Module
- 在Module中添加Function
- 在Function中添加BasicBlock
- 在BasicBlock中添加指令
- 创建一个ExecutionEngine
- 使用ExecutionEngine来运行IR
LLVM IR编程示例与说明
注意: LLVM处于快速发展之中,其API变化非常大,不同版本之间的API可能不兼容。本文的示例都基于LLVM 2.9。
创建Module
Module创建时需要一个context,通常使用global context。在例子中,Module的name被设置为test
。
1 | // Module Construction |
在Module中添加Function
在Module中添加Function的方法比较多,这里介绍一种比较简洁的方法。下面的代码生成了一个函数void foo(void)
。
1 | Constant* c = module->getOrInsertFunction("foo", |
到目前为止,还没有添加BasicBlock,函数foo仅仅是一个函数原型。第6行设置foo遵循C函数调用的规则。LLVM中的函数支持多种调用规则,通常使用C的调用规则即可。更多调用规则可以参考llvm::CallingConv::ID
。
在Function中添加BasicBlock
创建BasicBlock可以使用BasicBlock类的静态函数Create。
1 | BasicBlock* block = BasicBlock::Create(context, "entry", foo); |
第三个参数foo
表示将block
插入到Function foo
中。
在BasicBlock中添加指令
下面介绍一个在BasicBlock中添加指令的简洁方法。这个方法使用了一个工厂类IRBuilder
的实例builder
。
首先,初始化builder
。
1 | IRBuilder<> builder(block); |
这里将block
作为参数表示接下来的指令将被插入到block
中。
接下来的一段代码开始向block
中插入代码。含义包含在注释中。
1 | //Create three constant integer x, y, z. |
至此,我们通过LLVM的IR生成一个Module test
,这个Module中包含一个Function foo
,而foo
中包含一个BasicBlock entry
。
展示已经生成的IR
我们可以使用Module的dump方法先展示目前的成果。
1 | module->dump(); |
输出结果
1 | ; ModuleID = 'test' |
创建ExecutionEngine
接下来就要使用ExecutionEngine来生成代码了。
创建一个JIT类型的ExecutionEngine,为了便于观察IR生成的机器码,设置为不优化。
1 | InitializeNativeTarget(); |
生成机器指令
JIT生成机器指令以Function为单位。
1 | void * fooAddr = ee->getPointerToFunction(foo); |
如果用gdb跟踪函数执行,待输出fooAddr后,用x/i
命令,即可查看foo
对应的机器指令。
例如,我的X86_64机器上输出为:
1 | 0x7ffff7f6d010: movabs $0x7fffffffe2b0,%rax |
运行机器指令
使用类型转换将fooAddr转换成一个函数fooFunc,然后调用。
1 | //Run the function |
我们使用value的值来检验foo
构造的正确性。运行之后的输出
1 | Before calling foo: value = 10 |
经过验算,foo
的功能是正确的。
直接生成并运行机器指令
ExecutionEngine还提供一个接口runFunction
直接JIT并运行机器指令。具体做法可以参考LLVM::ExecutionEngine::runFunction
的文档。
代码
本文中的全部代码可以在这里查看。