MCJIT 设计与实现¶
简介¶
本文档描述了 MCJIT 执行引擎和 RuntimeDyld 组件的内部工作原理。它旨在作为实现的高级概述,展示代码生成和动态加载过程中对象的流程和交互。
引擎创建¶
在大多数情况下,EngineBuilder 对象用于创建 MCJIT 执行引擎的实例。 EngineBuilder 接受 llvm::Module 对象作为其构造函数的参数。然后,客户端可以设置各种选项,我们控制这些选项稍后传递给 MCJIT 引擎,包括选择 MCJIT 作为要创建的引擎类型。特别值得关注的是 EngineBuilder::setMCJITMemoryManager 函数。如果客户端此时未显式创建内存管理器,则在实例化 MCJIT 引擎时将创建默认内存管理器(特别是 SectionMemoryManager)。
设置选项后,客户端调用 EngineBuilder::create 以创建 MCJIT 引擎的实例。如果客户端不使用此函数的采用 TargetMachine 作为参数的形式,则将基于与用于创建 EngineBuilder 的模块关联的目标三元组创建新的 TargetMachine。

EngineBuilder::create 将调用静态 MCJIT::createJIT 函数,传入其指向模块、内存管理器和目标机器对象的指针,所有这些指针随后将由 MCJIT 对象拥有。
MCJIT 类有一个成员变量 Dyld,其中包含 RuntimeDyld 包装器类的实例。此成员将用于 MCJIT 和实际 RuntimeDyldImpl 对象之间的通信,该对象在加载对象时创建。

创建后,MCJIT 持有指向从 EngineBuilder 接收的 Module 对象的指针,但它不会立即为此模块生成代码。代码生成被延迟,直到显式调用 MCJIT::finalizeObject 方法或调用诸如 MCJIT::getPointerToFunction 之类的需要已生成代码的函数为止。
代码生成¶
当如上所述触发代码生成时,MCJIT 将首先尝试从其 ObjectCache 成员(如果已设置)检索对象镜像。如果无法检索到缓存的对象镜像,MCJIT 将调用其 emitObject 方法。 MCJIT::emitObject 使用本地 PassManager 实例并创建一个新的 ObjectBufferStream 实例,并将它们都传递给 TargetMachine::addPassesToEmitMC,然后在使用它创建的模块上调用 PassManager::run。

PassManager::run 调用导致 MC 代码生成机制将完整的可重定位二进制对象镜像(以 ELF 或 MachO 格式,具体取决于目标)发射到 ObjectBufferStream 对象中,该对象被刷新以完成该过程。如果正在使用 ObjectCache,则镜像将在此处传递给 ObjectCache。
此时,ObjectBufferStream 包含原始对象镜像。在可以执行代码之前,必须将此镜像中的代码和数据段加载到合适的内存中,必须应用重定位,并且必须完成内存权限和代码缓存失效(如果需要)。
对象加载¶
一旦获得对象镜像(通过代码生成或从 ObjectCache 检索),它将被传递给 RuntimeDyld 以进行加载。 RuntimeDyld 包装器类检查对象以确定其文件格式,并创建 RuntimeDyldELF 或 RuntimeDyldMachO 的实例(两者都派生自 RuntimeDyldImpl 基类),并调用 RuntimeDyldImpl::loadObject 方法来执行实际加载。

RuntimeDyldImpl::loadObject 首先从它接收的 ObjectBuffer 创建一个 ObjectImage 实例。 ObjectImage 包装 ObjectFile 类,是一个辅助类,它解析二进制对象镜像并提供对格式特定标头中包含的信息的访问,包括段、符号和重定位信息。
然后,RuntimeDyldImpl::loadObject 遍历镜像中的符号。收集有关公共符号的信息以供以后使用。对于每个函数或数据符号,关联的段被加载到内存中,并且该符号存储在符号表映射数据结构中。当迭代完成时,为公共符号发出一个段。
接下来,RuntimeDyldImpl::loadObject 遍历对象镜像中的段,并为每个段遍历该段的重定位。对于每个重定位,它调用格式特定的 processRelocationRef 方法,该方法将检查重定位并将其存储在两个数据结构之一中,即基于段的重定位列表映射和外部符号重定位映射。

当 RuntimeDyldImpl::loadObject 返回时,对象的所有代码和数据段都将已加载到内存管理器分配的内存中,并且重定位信息已准备就绪,但尚未应用重定位,并且生成的代码仍未准备好执行。
[目前(截至 2013 年 8 月)MCJIT 引擎将在 loadObject 完成时立即应用重定位。但是,这不应该发生。由于代码可能是为远程目标生成的,因此应让客户端有机会在应用重定位之前重新映射段地址。可以多次应用重定位,但在要重新映射地址的情况下,第一次应用是徒劳的。]
地址重映射¶
在生成初始代码之后和调用 finalizeObject 之前,客户端可以随时重新映射对象中段的地址。通常这样做是因为代码是为外部进程生成的,并且正在映射到该进程的地址空间中。客户端通过调用 MCJIT::mapSectionAddress 来重新映射段地址。这应该在将段内存复制到其新位置之前发生。
当调用 MCJIT::mapSectionAddress 时,MCJIT 将调用传递给 RuntimeDyldImpl(通过其 Dyld 成员)。 RuntimeDyldImpl 将新地址存储在内部数据结构中,但此时不更新代码,因为其他段可能会更改。
当客户端完成重新映射段地址后,它将调用 MCJIT::finalizeObject 以完成重新映射过程。
最终准备¶
当调用 MCJIT::finalizeObject 时,MCJIT 调用 RuntimeDyld::resolveRelocations。此函数将尝试查找任何外部符号,然后应用对象的所有重定位。
通过调用内存管理器的 getPointerToNamedFunction 方法来解析外部符号。内存管理器将返回目标地址空间中请求符号的地址。(请注意,这可能不是主机进程中的有效指针。)然后,RuntimeDyld 将遍历它已存储的与此符号关联的重定位列表,并调用 resolveRelocation 方法,该方法通过格式特定的实现,将重定位应用于加载的段内存。
接下来,RuntimeDyld::resolveRelocations 遍历段列表,并为每个段遍历已保存的引用该符号的重定位列表,并为该列表中的每个条目调用 resolveRelocation。此处的重定位列表是重定位列表,其中与重定位关联的符号位于与列表关联的段中。这些位置中的每一个都将具有一个目标位置,将在该位置应用重定位,该位置可能位于不同的段中。

一旦如上所述应用了重定位,MCJIT 调用 RuntimeDyld::getEHFrameSection,如果返回非零结果,则将段数据传递给内存管理器的 registerEHFrames 方法。这允许内存管理器调用任何所需的目标特定函数,例如向调试器注册 EH 帧信息。
最后,MCJIT 调用内存管理器的 finalizeMemory 方法。在此方法中,内存管理器将使目标代码缓存失效(如果需要),并对其已为代码和数据内存分配的内存页应用最终权限。