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 方法。在此方法中,内存管理器将根据需要使目标代码缓存失效,并对为代码和数据内存分配的内存页应用最终权限。