MCJIT 设计与实现¶
简介¶
本文档介绍了 MCJIT 执行引擎及 RuntimeDyld 组件的内部工作原理。它是一个高层次的实现概述,展示了代码生成和动态加载流程中对象的流转和交互方式。
引擎创建¶
通常,EngineBuilder 对象用于创建 MCJIT 执行引擎实例。EngineBuilder 的构造参数是 llvm::Module 对象。用户可设置各种选项(包括指定使用 MCJIT 作为目标类型),这些选项稍后会传递给 MCJIT 引擎。值得注意的是 EngineBuilder::setMCJITMemoryManager 函数。如果用户此时没有显式创建 memory manager,则在 MCJIT 实例化时会自动生成默认的内存管理器(具体来说是 SectionMemoryManager)。
设置好所有选项后,用户调用 EngineBuilder::create 创建 MCJIT 引擎实例。如果没有显式为此函数指定 TargetMachine,则会根据用于创建 EngineBuilder 的 Module 的 target triple 自动创建 TargetMachine。

EngineBuilder::create 会调用静态方法 MCJIT::createJIT,将 module、memory manager 和 target machine 的指针一并传递给 MCJIT,并将它们的所有权转交给 MCJIT 对象。
MCJIT 类有成员变量 Dyld,它是 RuntimeDyld 包装类的一个实例。此成员用于 MCJIT 与实际创建的 RuntimeDyldImpl 对象之间的通信(对象加载时创建)。

MCJIT 创建后会保存从 EngineBuilder 接收的 Module 对象的指针,但不会立刻为此 module 生成代码。代码生成会延后,直到显式调用 MCJIT::finalizeObject,或调用如 MCJIT::getPointerToFunction 这种需要代码已生成的功能时才触发。
代码生成¶
如上所述,代码生成被触发时,MCJIT 首先会尝试从其 ObjectCache 成员(若设置过缓存)获取对象文件。如果缓存里没有找到,则 MCJIT 会调用自身的 emitObject 方法。MCJIT::emitObject 会生成一个本地 PassManager 实例和 ObjectBufferStream 实例,然后用 TargetMachine::addPassesToEmitMC 设置好 pass pipeline,最后调用 PassManager::run 处理创建该 MCJIT 的 Module。

PassManager::run 调用导致 MC 代码生成机制,将生成的完整可重定位目标文件(ELF 或 MachO 格式,取决于目标)输出到 ObjectBufferStream 中,最后刷新完成整个流程。如果有 ObjectCache,这时会把 image 保存到缓存。
至此,ObjectBufferStream 里就有原始对象镜像。在代码可执行前,需将 image 里的代码区和数据区加载到适当内存、应用重定位,并完成内存权限和代码缓存失效(如需)。
对象加载¶
一旦取得对象镜像(无论是代码生成还是 ObjectCache 取出),会传递给 RuntimeDyld 加载。RuntimeDyld 包装类检查对象格式并实例化 RuntimeDyldELF 或 RuntimeDyldMachO(均派生自 RuntimeDyldImpl),调用 RuntimeDyldImpl::loadObject 方法做实际加载。

RuntimeDyldImpl::loadObject 从收到的 ObjectBuffer 创建一个 ObjectImage 实例。ObjectImage(封装了 ObjectFile 类)用于解析二进制对象镜像,并提供格式相关的 section、symbol、重定位等信息访问接口。
接下来,RuntimeDyldImpl::loadObject 遍历 image 中所有符号。Common 符号信息会被收集以便后续处理。对于每个函数或数据符号,将相关段(section)加载到内存,并在符号表 map 里记录该符号。当全部遍历完后,会为 common 符号专门生成一节。
然后,RuntimeDyldImpl::loadObject 遍历对象镜像中每个 section,并遍历各 section 的所有重定位项。每个重定位会调用格式相关的 processRelocationRef 方法,该方法分析重定位并把重定位保存到两个数据结构之一:section-based relocation list map 和 external symbol relocation map。

RuntimeDyldImpl::loadObject 返回时,目标对象的代码和数据区都已加载到 memory manager 分配的内存,并且重定位信息已准备好,但此时还没有应用重定位,代码也尚不可执行。
[截至 2013 年 8 月,MCJIT 会在 loadObject 完成后立即应用重定位。这其实不太合理。因为某些情况下代码要为远程目标生成,用户应该先有机会 remap section 地址,再应用重定位。理论上应用多次重定位没错,但如果地址本来要 remap,提前应用纯属浪费。]
地址重映射¶
在生成初始代码到调用 finalizeObject 之间,用户可以 remap 对象里 section 的地址。通常用于跨进程,将代码映射进目标进程地址空间。用户调用 MCJIT::mapSectionAddress 实现 remap,这应发生在 section 内存复制到新位置前。
MCJIT::mapSectionAddress 被调用时,会将调用转发到 RuntimeDyldImpl(通过 Dyld 成员)。RuntimeDyldImpl 存储新地址于内部结构,但此时不会更新已加载代码,因为其它 section 地址也可能改变。
等用户完成所有重映射操作,会调用 MCJIT::finalizeObject 完成 remap 流程。
最终准备¶
MCJIT::finalizeObject 被调用时,MCJIT 调用 RuntimeDyld::resolveRelocations。该函数会查找所有外部符号并应用所有重定位。
解析外部符号时,会调用 memory manager 的 getPointerToNamedFunction 方法,在目标地址空间返回指定符号地址(注意:这在 host 进程内或许不是有效指针)。然后 RuntimeDyld 遍历之前记录的与该符号相关的重定位,依次调用 resolveRelocation(格式相关实现),执行实际的重定位写操作。
接下来,RuntimeDyld::resolveRelocations 遍历各 section,对每个 section 的 relocation list 依次调用 resolveRelocation。该 list 包含所有标记为此 section 内定义符号的重定位,每个重定位实际作用点可能在不同 section。

当上述重定位应用完成后,MCJIT 调用 RuntimeDyld::getEHFrameSection,如果返回非零数据,则将该 section 数据传给 memory manager 的 registerEHFrames 方法。这样 memory manager 能调用任意目标相关的处理,例如调试器注册 EH frame。
最后,MCJIT 调用 memory manager 的 finalizeMemory 方法。在 finalizeMemory 里,如果需要,会失效目标代码缓存,并为 code/data 区设置最终的内存页权限。
原文地址:https://llvm.org/docs/MCJITDesignAndImplementation.html
2549

被折叠的 条评论
为什么被折叠?



