2312llvm,06即时编译
即时
编译器
LLVM即时(即时)
编译器是基于函数
的动态翻译引擎
.
术语来自即时
制造,即工厂按需
制造或购买物资,而不放入仓库
.在编译
过程中,该比喻也很合适,因为即时
编译器不会存储
二进制程序到磁盘
,而是在需要时才开始编译
.
即时
策略的优势
,在于知道运行程序
的精确的机器和微架构
.这让即时
系统可对特定
处理器微调
代码.
而且,有的编译器只有在运行时
才知道其输入
,因而只能实现即时
系统.
如,GPU
驱动即时编译着色语言
,互联网浏览器处理js
等.
了解LLVM
即时引擎基础
LLVM
即时编译器是基于函数
的,因为它一次可编译单个函数
.这定义了编译器的工作粒度
,对即时
系统来说这是个重要
决定.
通过按需
编译函数
,编译器只会处理当前程序调用中实际用到
的函数.如,你的程序有多个函数
,但启动时设置
了错误的命令行参数
,基于函数的即时
系统只会编译
那个打印帮助消息
的函数,而不是该程序
.
即时
引擎,在运行时
编译且执行LLVMIR
函数.在编译阶段,即时
引擎会用LLVM
生成目标相关
的二进制指令
组成的二进制数据块
.
它返回编译后的可执行的函数指针
.
一篇有趣的博客文章对比了即时
编译的开源方法.
LLVM
作为静态
编译器比即时
系统更加有名,因为在即时
编译过程中,每趟
消耗的时间
是很重要的,这都算程序执行
的成本
.
和GCC
相似,LLVM
基础架构更注重支持慢而强
的优化,而不是对构建有竞争力的即时
系统很重要的快而弱
的优化.
即时
系统不值得浪费大量时间
去优化仅执行几次
的程序片段
.
介绍执行引擎
LLVM
即时系统有个支持执行LLVM
模块的执行引擎
.在<llvm_source>/include/llvm/ExecutionEngine/ExecutionEngine.h
中定义ExecutionEngine
类.
设计它通过即时
系统或解释器
执行.一般,一个执行引擎
负责管理执行用户程序
,分析要运行的程序片段
,采取合理动作来执行
它.
要即时
编译,必须要有执行
管理器来协调
编译策略,(一次一个片段
)运行
用户程序.就LLVM
的ExecutionEngine
类而言,它把执行部分
抛回给用户.
可运行编译管线
,产生内存
中的代码
,但由你决定是否执行
此代码.
除了有待执行LLVM
模块,引擎
支持下面几个场景:
1,懒(lazy)
编译:调用函数
时,引擎
才编译它.关闭懒编译
后,一旦请求函数指针
,引擎就编译
它们.
2,编译外部全局变量
:包括解析当前LLVM
模块的外部实体符号并分配内存
.
3,与运行时动态加载共享对象(DSO)
一样,通过dlsym
查找和解析外部符号
.
LLVM
实现了两个执行引擎:llvm::JIT
类和llvm::MCJIT
类.ExecutionEngine::EngineBuilder()
方法根据IR
模块参数,实例化一个ExecutionEngine
对象.
接着,ExecutionEngine::create()
方法创建一个即时
或MCJIT
实例,两者实现截然不同.
注意,解释器
实现了一个非传统
策略来执行硬件平台(主机平台)
不原生地支持的用户代码
.如,LLVMIR
是x86
平台上的用户代码
,因为x86
处理器不能直接执行LLVMIR
.
不同于即时
编译器,解释器
任务是读取
每条指令,解码
并执行它们
,在软件
中,模仿了物理
处理器.
尽管解释器
省去了启动
编译器,翻译用户代码
时间,它们往往慢得多
.
管理内存
一般,即时
引擎在运行时由ExecutionManager
类,写二进制
数据块进内存.随后,就可跳转到分配
的内存区域
(即调用ExecutionManager
返回给你的函数指针
),来执行这些指令
了.
这里,对很多普通任务
,如分配内存,释放内存,为加载库提供空间,和内存权限管理
,管理内存
很重要.
即时
和MCJIT
类都实现了自定义管理内存类
,从RTDyldMemoryManager
基类继承
而来.
ExecutionEngine
用户也可提供自定义的RTDyldMemoryManager
子类,来指定在哪放置
不同的即时
组件.
可在<llvm_source>/include/llvm/ExecutionEngine/RTDyldMemoryManager.h
文件中找该接口
.
如,RTDyldMemoryManager
类声明了如下方法:
1,allocateCodeSection()
和allocateDataSection()
:这些方法分配
内存,来保存
给定大小和对齐的可执行代码和数据
.
管理内存
用户可通过内部节
标识追踪
已分配的节
.
2,getSymbolAddress()
:返回当前链接的库
中可获得的符号
的地址.注意这不是用来取即时
编译生成的符号
.
调用时,必须提供一个std::string
实例以保存符号
的名字.
3,finalizeMemory()
:应该在加载完对象
时调用,然后终于可设置内存权限
了.如,不能在调用
它前,运行生成代码
.
它直接定向到MCJIT
用户而不是即时
用户.
尽管用户可提供自定义管理内存
实现,JITMemoryManager
和SectionMemoryManager
分别是即时
和MCJIT
的默认子类
.
llvm::即时
基础结构
即时
类和它的框架
代表旧引擎,它是用LLVM
生成代码的不同部分
实现的.LLVM3.5
之后,就要移除它.尽管该引擎
大部分是目标无关
的,每个目标
必须为它的具体指令
实现二进制指令输出
.
把数据块写到内存
即时
类通过MachineCodeEmitter
类的JITCodeEmitter
子类,输出二进制指令
.MachineCodeEmitter
类用来输出机器代码
,它和新的机器代码(MachineCode,MC)
框架没有关联,尽管旧,它依然存在并支持即时
类的功能.
局限
是只支持多个
目标,且对支持目标,不支持所有目标特性
.
MachineCodeEmitter
类的方法,方便下列任务
:
1,为当前要发射的函数
分配空间
2,写二进制数据块
到内存缓冲
(emitByte(),emitWordLE(),emitWordBE(),emitAlignment()
,等)
3,追踪当前缓冲地址
(就是下一条发射指令
位置的指针)
4,添加与此缓冲
内的指令地址
相关联的重定向
由参与发射代码
过程JITCodeEmitter
类写字节到内存
.由JITCodeEmitter
的子类
,实现相关的即时
功能和管理.
JITCodeEmitter
是相当简单的,只是写字节
到缓冲,而JITEmitter
有下列改进:
1,特化的管理内存器,JITMemoryManager
.
2,(JITResolver)
解决者实例,跟踪和解决
未编译函数的调用点
.对懒编译函数
至关重要.
使用JITMemoryManager
JITMemoryManager
类(见<llvm_source>/include/llvm/ExecutionEngine/JITMemoryManager.h
)实现了低级
内存处理,并为前面提及的类提供缓冲
.
除了来自RTDyldMemoryManager
的方法,它提供具体如allocateGlobal()
等方法来协助即时
类,为单个全局变量
分配内存;
而startFunctionBody()
,按读/写
可执行标记
分配内存时,创建调用即时
来发射指令.
内部,JITMemoryManager
类使用JITSlabAllocatorslab
分配器(<llvm_source>/lib/ExecutionEngine/即时/JITMemoryManager.cpp
)和MemoryBlock
单元(<llvm_source>/include/llvm/Support/Memory.h
).
发射目标代码
每个目标
都实现一个叫<Target>CodeEmitter
(见<llvm_source>/lib/Target/<Target>CodeEmitter.cpp
)的机器函数趟
,它按数据块
编码指令,并用JITCodeEmitter
写到内存.
如,MipsCodeEmitter
遍历所有函数基本块
,对每个(MI)
机器指令,调用emitInstruction()
:
(...)
MCE.startFunction(MF);
for (MachineFunction::iterator MBB = MF.begin(), E = MF.end(); MBB != E; ++MBB) {
MCE.StartMachineBasicBlock(MBB);
for (MachineBasicBlock::instr_iterator I = MBB->instr_begin(), E = MBB->instr_end(); I != E;)
emitInstruction(*I++, *MBB);
}
(...)
MIPS32
是固定4字节
长度的ISA
,因此emitInstruction()
实现很简单
.
void MipsCodeEmitter::emitInstruction(MachineBasicBlock::instr_iterator MI, MachineBasicBlock &MBB) {
...
MCE.processDebugLoc(MI->getDebugLoc(), true);
emitWord(getBinaryCodeForInstr(*MI));
++NumEmitted; //跟踪`mi`发射的#
...
}
emitWord()
方法是包装JITCodeEmitter
,getBinaryCodeForInstr()
是TableGen
通过解读.td
文件中的指令编码
描述,为每个目标
生成的.
<Target>CodeEmitter
类还必须实现自定义
方法以编码操作数和其它目标相关的实体
.
如,在MIPS
中,内存操作数必须使用getMemEncoding()
方法来正确编码(见<llvm_source>/lib/Target/Mips/MipsInstrInfo.td
):
def mem : Operand<iPTR> {
(...)
let MIOperandInfo = (ops ptr_rc, simm16);
let EncoderMethod = "getMemEncoding";
(...)
}
因此,MipsCodeEmitter
必须实现MipsCodeEmitter::getMemEncoding()
方法以符合该TableGen
描述.代码输出器和即时
框架关系:
MipsCodeEmitter
MachineCodeEmitter
两个代码发射器
ARMCodeEmitter
JITCodeEmitter
JITEmitter
RTDyldMemoryManager
JITMemoryManager
...
//内存管理器.
目标信息
为了支持即时
编译,每个目标
还必须提供一个TargetJITInfo
的子类(见include/llvm/Target/TargetJITInfo.h
),如MipsJITInfo
或X86JITInfo
.
TargetJITInfo
类为通用即时
功能提供了接口,需要每个目标
实现它们.下面,看这些函数的一些示例:
1,为了让执行引擎重编译
函数,或许因为修改了它,每个目标
要实现TargetJITInfo::replaceMachineCodeForFunction()
方法,用跳转指令
或调用
新版本函数修补
原先函数的位置
.
对自修改代码
,这是必需的.
2,类似动态链接器
,TargetJITInfo::relocate()
方法修补
当前发射函数
中的每个符号
引用至正确内存地址
.
3,TargetJITInfo::emitFunctionStub()
方法发射一个桩
:在给定地址
调用另一个
函数的函数
.每个目标
还要用发射的桩的字节大小和对齐
提供自定义的TargetJITInfo::StubLayout
信息.
JITEmitter
用此桩信息
在发射它前,为新桩
分配空间.
虽然TargetJITInfo
方法的目的
不是发射
如生成函数体
等普通指令,但是它们仍要为生成桩
发射特定指令
,并调用新的内存位置
.
然而,当创建即时
框架后,没有可依赖的接口
来更易发射
在MachineBasicBlock
之外的单独指令
.这是今天MCInsts
为MCJIT
做的事情.
没有MCInsts
,旧的即时
框架强制目标手工
编码指令
.
为了表示<Target>JITInfo
实现,如何需要手工
发射指令,看看MipsJITInfo::emitFunctionStub()
的代码(见<llvm_source>/lib/Target/Mips/MipsJITInfo.cpp
),它用以下代码生成4条
指令:
...
// lui $t9, %hi(EmittedAddr)
// addiu $t9, $t9, %lo(EmittedAddr)
// jalr $t8, $t9
// nop
if (IsLittleEndian) {
JCE.emitWordLE(0xf << 26 | 25 << 16 | Hi);
JCE.emitWordLE(9 << 26 | 25 << 21 | 25 << 16 | Lo);
JCE.emitWordLE(25 << 21 | 24 << 11 | 9);
JCE.emitWordLE(0);
...
学习如何使用即时
类
即时
是一个在<llvm_source>/lib/ExecutionEngine/JIT/JIT.h
中声明的ExecutionEngine
子类.即时
类是借助即时
基础结构的编译函数入口
.
ExecutionEngine::create()
方法用默认JITMemoryManager
,调用即时::createJIT()
.接着,即时
构造器执行下面的任务:
1,创建JITEmitter
实例
2,初化
目标信息对象
3,为生成代码
添加趟
4,添加最后
运行的<Target>CodeEmitter
趟
引擎
保存了一个趟管理器
对象,每当请求即时
编译函数时,来调用
生成代码及即时
发射趟
.
目的是取Sum
函数,且用即时
系统用运行时参数
,计算两个
不同加数.如下步骤:
1,首先,创建一个叫sum-jit.cpp
的新文件.要包含即时
执行引擎的资源
:
#include "llvm/ExecutionEngine/JIT.h"
2,包含涉及读写LLVM位码
,环境接口
等其它的头文件
,并导入LLVMnamespace
:
#include "llvm/ADT/OwningPtr.h"
#include "llvm/Bitcode/ReaderWriter.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/system_error.h"
#include "llvm/Support/TargetSelect.h"
using namespace llvm;
3,InitializeNativeTarget()
方法安装主机目标
,确保可链接即时
将用到的目标库
.和往常一样,每个线程需要一个LLVMContext
环境对象和一个MemoryBuffer
对象,以从磁盘读取位码
文件,如下:
int main() {
InitializeNativeTarget();
LLVMContext Context;
std::string ErrorMessage;
OwningPtr<MemoryBuffer> Buffer;
4,用getFile()
方法从磁盘
读文件,如下:
if (MemoryBuffer::getFile("./sum.bc", Buffer)) {
errs() << "sum.bc not found\n";
return -1;
}
5,ParseBitcodeFile
函数从MemoryBuffer
读取数据,并生成相应LLVMModule
类:
Module *M = ParseBitcodeFile(Buffer.get(), Context, &ErrorMessage);
if (!M) {
errs() << ErrorMessage << "\n";
return -1;
}
6,调用EngineBuilder
工厂创建一个ExecutionEngine
实例,再调用其create
方法,如下:
OwningPtr<ExecutionEngine> EE(EngineBuilder(M).create());
默认创建即时
执行引擎,且是即时
的设置点;它间接调用创建JITEmitter
,JITManager
的即时
构造器,并初化
所有生成代码
和目标相关
的发射趟
.
此刻,尽管引擎是LLVMModule
相关,但还没有编译
函数.
为了编译
函数,还需要调用
取本地即时
编译的函数指针
的getPointerToFunction()
.如果未曾即时
编译过该函数
,就即时
编译它并返回函数指针
.流程:
getPointerToFunction()
JIT::runJITOnFunctionUnlocked()
MipsCodeEmitter::emitInstruction
JIT pending
JIT::jitTheFunction()
MipsCodeEmitter::runOnFunction
functions
PassManager::run()
CodeGen Passes
7,通过getFunction()
方法,取表示sum
函数的函数IR
对象:
Function *SumFn = M->getFunction("sum");
这里,触发了即时
编译:
int (*Sum)(int, int) = (int (*)(int, int)) EE->getPointerToFunction(SumFn);
你要适当
转换类型到匹配该函数
的函数指针类型
.Sum
函数的LLVM
定义原型是i32@sum(i32%a,i32%b)
,因此用int(*)(int,int)
C原型.
另一个选项是考虑调用getPointerToFunctionOrStub()
而不是getPointerToFunction()
的懒编译
.如果目标函数
还没有编译且开启懒编译
,则生成一个桩函数
,并返回它的指针
.
桩是一个包含,稍后修改它就可跳转/调用
实际函数的占位符
的简单函数
.
8,接着,根据Sum
所指向的即时
编译了的函数
,调用原始Sum
函数,如下:
int res = Sum(4, 5);
outs() << "Sum result: " << res << "\n";
用懒编译
时,Sum
调用桩函数(a)
,它(a
)会用一个编译回调
函数来即时
编译实际函数
.然后修改桩
以重定向
到实际函数
,并执行它.
除非改变
了原始的Module
中的Sum
函数,不会再次编译
该函数.
9,再次调用Sum
来计算
下个结果,如下:
res = Sum(res, 6);
outs() << "Sum result: " << res << "\n";
懒编译中,因为在第一次调用Sum
时已编译了原始函数
,第二次
调用会直接执行
原生函数.
10,成功用即时
编译的Sum
函数计算
了两次加法.现在,释放执行引擎
分配的保存函数代码的内存
,调用llvm_shutdown()
函数并返回:
EE->freeMachineCodeForFunction(SumFn);
llvm_shutdown();
return 0;
}
要编译并链接sum-jit.cpp
,可如下:
$ clang++ sum-jit.cpp -g -O3 -rdynamic -fno-rtti $(llvm-config --cppflags --ldflags --libs jit native irreader) -o sum-jit
或,用前面的Makefile
,添加-rdynamic
选项,并更改llvm-config
调用以使用前面命令行
指定的库.
尽管该示例
不用外部函数
,-rdynamic
选项是重要的,它保证运行时解析外部函数
.
运行该示例并查看输出:
$ ./sum-jit
Sum result: 9
Sum result: 15
通用值
在前例中,为了用C风格
函数去调用
该函数,把返回的函数指针
转换为恰当的原型
.然而,当处理多个
函数且带众多的签名和参数类型
时,需要更灵活
方法去执行它们.
执行引擎
提供了另一个调用即时
编译的函数
的方法.不需要提前调用getPointerToFunction()
,runFunction()
方法编译并用GenericValue
向量决定函数参数
来运行函数.
在<llvm_source>/include/llvm/ExecutionEngine/GenericValue.h
中定义GenericValuestruct
,它可保存任意常见类型
.
修改前例,用runFunction()
而不是getPointerToFunction()
加转换类型.
首先,创建sum-jit-gv.cpp
文件,并保存该新版本,在开头
添加GenericValue
头文件:
#include "llvm/ExecutionEngine/GenericValue.h"
从sum-jit.cpp
复制其余的内容,关注修改部分.在初化SumFn
函数指针后,创建GenericValue
向量类型的FnArgs
,并用APInt
接口(<llvm_source>/include/llvm/ADT/APInt.h
)填充整数值
.根据sum(i32%a,i32%b)
函数原型,用两个32
位长度的整数填充
:
(...)
Function *SumFn = m->getFunction("sum");
std::vector<GenericValue> FnArgs(2);
FnArgs[0].IntVal = APInt(32, 4);
FnArgs[1].IntVal = APInt(32, 5);
用函数变量
和参数向量
调用runFunction()
.这样,会即时
编译并执行
函数.相应结果也是i32
位可访问的GenericValue
.
GenericValue Res = EE->runFunction(SumFn, FnArgs);
outs() << "Sum result: " << Res.IntVal << "\n";
重复相同的过程,执行第二个加法:
FnArgs[0].IntVal = Res.IntVal;
FnArgs[1].IntVal = APInt(32, 6);
Res = EE->runFunction(SumFn, FnArgs);
outs() << "Sum result: " << Res.IntVal << "\n";
(...)
llvm::MCJIT
框架
MCJIT
类是LLVM
新的即时
实现.MC
提供了统一
的指令表达
方式,且为汇编器,反汇编器,汇编打印器
和MCJIT
所共享的一个框架
.
MC
库的第一个优势
在,目标只需要指定一次指令的编码
,因为所有子系统
都会得到此信息
.因此,编写LLVM
后端时,如果实现了目标的发射目标代码
功能,也就实现了即时
功能.
将在LLVM3.5
之后删除llvm::即时
,并完全替换为llvm::MCJIT
框架.那么,为何学习旧即时
呢?虽然它们是不同实现
,但是ExecutionEngine
类是通用
的,大部分概念
是两者共有
的.
MCJIT
引擎
和旧即时
引擎相同,调用ExecutionEngine::create()
创建MCJIT
引擎.它调用执行MCJIT
构造器的MCJIT::createJIT()
.
在<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.h
文件中声明MCJIT
类.在<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.cpp
文件中实现createJIT()
方法和MCJIT
构造器.
MCJIT
构造器创建一个SectionMemoryManager
实例,添加LLVM
模块到它内部的OwningModuleContainer
,模块容器
,并初化目标信息
.
模块的状态
MCJIT
类为创建引擎
时,为初始LLVM
模块实例,插入的表示模块编译阶段
的指定状态
.如下:
1,Added
:模块集,还未编译
但已添加到执行引擎
了.允许模块
为其它模块
暴露函数定义
,直到必需
时,才编译它们.
2,Loaded
:模块,已即时
编译,但未准备好执行
.还没有重定向
,还未授权内存页
.愿意
在内存中重映射
已即时
编译函数的用户
,用loaded
状态模块,也许可避免重编译
.
3,Finalized
:模块包含已准备好执行的函数
.在此状态下,因为已重定向
,不能重映射函数
了.
即时
和MCJIT
的一个主要区别
就在于模块状态
.在MCJIT
中,在请求符号
地址(函数和全局变量
)前,整个模块
必须就绪(finalized)
.
MCJIT::finalizeObject()
函数,把已添加模块
转换为已加载
模块,接着转为终止化
.
首先,它调用generateCodeForModule()
生成已加载
模块.
接着,通过finalizeLoadedModules()
方法,终止化
所有模块.
不像旧即时
,MCJIT::getPointerToFunction()
函数要求在调用之前模块对象
已就绪.因此,必须在使用前调用MCJIT::finalizeObject()
.
LLVM3.4
添加的新方法取消了该限制
,当使用MCJIT
时,getFunctionAddress()
淘汰了getPointerToFunction()
方法.
在请求符号
地址前,该新方法
不调用finalizeObject()
,就加载
并终止化模块.
注意,在旧即时
中,执行引擎
单独即时
编译及执行
各个函数.在MCJIT
中,在执行函数
前必须即时
编译整个模块(所有函数)
.
因为编译粒度
变大了,不能再说它是基于函数
的,而是基于模块
的翻译引擎
.
理解MCJIT
如何编译模块
在模块对象加载
阶段生成
代码,由在<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.cpp
文件中的MCJIT::generateCodeForModule()
方法触发.执行下面的任务:
1,创建一个ObjectBuffer
实例来保存
模块对象.如果已加载(编译)
模块对象,就用ObjectCache
接口取,避免重编译.
2,假设之前没有
缓存(cache)
,MCJIT::emitObject()
就发射MC
代码.结果是一个ObjectBufferStream
对象(支持流的ObjectBuffer
子类).
3,RuntimeDyld
动态链接器加载生成的ObjectBuffer
对象,并通过RuntimeDyld::loadObject()
创建符号表(symboltable)
.并返回ObjectImage
对象.
4,按已加载
标记模块
.
对象缓冲,缓存,图像
ObjectBuffer
类,(<llvm_source>/include/llvm/ExecutionEngine/ObjectBuffer.h)
实现了MemoryBuffer
类(<llvm_source>/include/llvm/Support/MemoryBuffer.h)
的包装.
MCObjectStreamer
子类用MemoryBuffer
类发射指令和数据
到内存.此外,ObjectCache
类直接引用可从中取ObjectBuffer
的MemoryBuffer
实例.
ObjectBufferStream
类是一个带额外的让读写内存缓冲
容易的标准C++
流符号(如,>>
和<<
)的ObjectBuffer
子类.
ObjectImage
对象(<llvm_source>/include/llvm/ExecutionEngine/ObjectImage.h)
来保持
加载的模块,且可直接
访问ObjectBuffer
和ObjectFile
的引用.
由目标相关
的如ELF,COFF
,和MachO
等目标文件
类型特化ObjectFile
对象.ObjectFile
对象可从MemoryBuffer
对象,直接取符号,重定向,和节
.
动态链接
按ObjectImage
实例表示MCJIT
加载的模块对象
.如前,可通过目标无关
的ObjectFile
接口,透明
访问内存缓冲
.因此,可处理符号,节,和重定向
.
为了生成ObjectImage
对象,MCJIT
有RuntimeDyld
类提供的动态链接特性
.该类提供了访问这些特性的公共接口
,而每个对象文件类型
特化的RuntimeDyldImpl
对象提供实际实现
.
因此,从ObjectBuffer
生成ObjectImage
对象的RuntimeDyld::loadObject()
方法,首先创建目标相关
的RuntimeDyldImpl
对象,然后调用RuntimeDyldImpl::loadObject()
.
过程中,还创建了可通过ObjectImage
对象取它的ObjectFile
对象.
在模块就绪
过程中,用运行时RuntimeDyld
动态链接器来解决重定向
,并为模块对象
注册异常处理帧
.
回想起getFunctionAddress()
和getPointerToFunction()
执行引擎方法,要求引擎
知道符号(函数)地址
.
为此,MCJIT
还通过RuntimeDyld::getSymbolLoadAddress()
方法,用RuntimeDyld
取任意符号地址
.
管理内存器
LinkingMemoryManager
类,是另一个RTDyldMemoryManager
的子类,是MCJIT
引擎所用的实际管理内存器
.它聚集了一个SectionMemoryManager
实例,并向它发送代理请求
.
每当RuntimeDyld
动态链接器,通过LinkingMemoryManager::getSymbolAddress()
请求符号地址
时,它有两个选择:如果在已编译模块
中可取得符号
,就从MCJIT
取地址;
否则,从由SectionMemoryManager
实例加载并映射
的外部库
请求地址.
参考<llvm_source>/lib/ExecutionEngine/MCJIT/MCJIT.cpp
中的LinkingMemoryManager::getSymbolAddress()
了解详情.
SectionMemoryManager
实例是个简单管理器
.作为一个RTDyldMemoryManager
的子类,SectionMemoryManager
继承了它所有的查询库
方法,但是通过直接处理低级MemoryBlock
单元(<llvm_source>/include/llvm/Support/Memory.h)
,来分配代码和数据节
.
发射MC
代码
MCJIT
调用MCJIT::emitObject()
发射MC
代码.如下:
1,创建一个PassManager
对象.
2,添加一个目标布局趟
,调用addPassesToEmitMC()
以添加所有生成代码趟
并发射MC
代码.
3,用PassManager::run()
方法,运行所有的趟
.在一个ObjectBufferStream
对象中存储
结果代码.
4,添加已编译的对象
到ObjectCache
实例并返回
它.
MCJIT
的发射代码
比旧即时
更一致.不是给即时
提供自定义的输出器和目标信息
,MCJIT
透明访问现有MC
基础设施的所有信息
.
终止化对象
最终,在MCJIT::finalizeLoadedModules()
里让模块对象就绪
:已解决重定向
,把已加载
模块移到已就绪
模块组,并调用LinkingMemoryManager::finalizeMemory()
以改变内存页权限
.
对象就绪
后,就可执行MCJIT
编译的函数
了.
使用MCJIT
引擎
下面的sum-MCJIT.cpp
源文件包含了用MCJIT
而不是旧即时
框架,即时
编译Sum
函数所必需的代码
.
为了表明它与前面的即时
示例相似
,保留了旧代码
,并用UseMCJIT
布尔变量来决定
使用旧即时
还是MCJIT
.
1,首先,如下包含MCJIT
头文件:
#include "llvm/ExecutionEngine/MCJIT.h"
2,包含其它必需的头文件,并导入llvm
名字空间:
#include "llvm/ADT/OwningPtr.h"
#include "llvm/Bitcode/ReaderWriter.h"
#include "llvm/ExecutionEngine/JIT.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/system_error.h"
#include "llvm/Support/FileSystem.h"
using namespace llvm;
3,设置UseMCJIT
为true
,以测试MCJIT
.设置为false
就用旧即时
运行该示例,如下:
bool UseMCJIT = true;
int main() {
InitializeNativeTarget();
4,MCJIT
需要初化汇编解析器和打印器
:
if (UseMCJIT) {
InitializeNativeTargetAsmPrinter();
InitializeNativeTargetAsmParser();
}
LLVMContext Context;
std::string ErrorMessage;
OwningPtr<MemoryBuffer> Buffer;
if (MemoryBuffer::getFile("./sum.bc", Buffer)) {
errs() << "sum.bc not found\n";
return -1;
}
Module *M = ParseBitcodeFile(Buffer.get(), Context, &ErrorMessage);
if (!M) {
errs() << ErrorMessage << "\n";
return -1;
}
5,创建执行引擎,调用SetUseMCJIT(true)
方法,让引擎使用MCJIT
,如下:
OwningPtr<ExecutionEngine> EE;
if (UseMCJIT)
EE.reset(EngineBuilder(M).setUseMCJIT(true).create());
else
EE.reset(EngineBuilder(M).create());
6,旧即时
需要稍后取函数指针
并析构分配的内存
的Function
引用:
Function* SumFn = NULL;
if (!UseMCJIT)
SumFn = cast<Function>(M->getFunction("sum"));
7,如前,MCJIT
淘汰了getPointerToFunction()
,在MCJIT
中只能用getFunctionAddress()
.因此,对各个即时
类型,要用正确
方法:
int (*Sum)(int, int) = NULL;
if (UseMCJIT)
Sum = (int (*)(int, int)) EE->getFunctionAddress(std::string("sum"));
else
Sum = (int (*)(int, int)) EE->getPointerToFunction(SumFn);
int res = Sum(4, 5);
outs() << "Sum result: " << res << "\n";
res = Sum(res, 6);
outs() << "Sum result: " << res << "\n";
8,因为MCJIT
一次编译整个模块
,释放Sum
函数的机器代码
,仅在旧即时
中才有意义:
if (!UseMCJIT)
EE->freeMachineCodeForFunction(SumFn);
llvm_shutdown();
return 0;
}
9,用下面命令编译和链接sum-MCJIT.cpp
:
$ clang++ sum-MCJIT.cpp -g -O3 -rdynamic -fno-rtti $(llvm-config --cppflags --ldflags --libs 即时 MCJIT native irreader) -o sum-MCJIT
或,修改前面的Makefile
.运行该示例,并检查输出:
$ ./sum-MCJIT
Sum result: 9
Sum result: 15
用LLVM即时
编译工具
LLVM
提供了一些即时
引擎的工具.如lli
和llvm-rtdyld
.
使用lli
工具
用LLVM
执行引擎,(lli)
解释工具实现了一个LLVM位码
解释器和即时
编译器.考虑如下sum-main.c
源文件:
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
int main() {
printf("sum: %d\n", sum(2, 3) + sum(3, 4));
return 0;
}
只要有main
函数,lli
工具可运行位码
文件.用clang
生成sum-main.bc
位码文件:
$ clang -emit-llvm -c sum-main.c -o sum-main.bc
现在,通过lli
用旧即时
编译引擎运行位码
:
$ lli sum-main.bc
sum: 12
或,用MCJIT
引擎:
$ lli -use-MCJIT sum-main.bc
sum: 12
也有应用更慢的解释器
的标志:
$ lli -force-interpreter sum-main.bc
sum: 12
使用llvm-rtdyld
工具
llvm-rtdyld
工具()是个非常简单
的测试MCJIT
对象加载和链接
框架的工具.它可从磁盘
读取二进制目标文件
,并通过命令行
,执行指定函数
.
它不即时
编译和执行,但允许测试和运行
目标文件.
考虑下面三个C源码
文件:main.c,add.c
,和sub.c
:
main.c
int add(int a, int b);
int sub(int a, int b);
int main() {
return sub(add(3, 4), 2);
}
add.c
int add(int a, int b) {
return a+b;
}
sub.c
int sub(int a, int b) {
return a-b;
}
按目标文件
编译它们:
$ clang -c main.c -o main.o
$ clang -c add.c -o add.o
$ clang -c sub.c -o sub.o
用llvm-rtdyld
工具,以-entry
和-execute
选项执行main
函数:
$ llvm-rtdyld -execute -entry=_main main.o add.o sub.o; echo $ loaded '_main' at: 0x104d98000
5
或用-printline
,为编译了调试信息
的函数打印行信息
.如下:
$ clang -g -c add.c -o add.o
$ llvm-rtdyld -printline add.o
Function: _add, Size = 20
Line info @ 0: add.c, line: 2
Line info @ 10: add.c, line: 3
Line info @ 20: add.c, line: 3
可见,llvm-rtdyld
工具,从MCJIT
框架抽象的对象
.llvm-rtdyld
工具读取一系列二进制目标文件
到ObjectBuffer
对象,并通过RuntimeDyld::loadObject()
生成ObjectImage
实例.
加载完所有目标文件
后,由RuntimeDyld::resolveRelocations()
解决重定向
.
接着,通过getSymbolAddress()
取入口(entrypoint)
,并调用函数
.
llvm-rtdyld
工具用了自定义
的管理内存的TrivialMemoryManager
.这是个简单的RTDyldMemoryManager
子类.
可帮助你理解MCJIT
框架的基本概念
.
其它资源
<llvm_source>/examples/HowToUseJIT
和<llvm_source>/examples/ParallelJIT
包含了学习即时
基础的简单源码示例
.