LLVM - MCJIT 设计与实现

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。

_images/MCJIT-engine-builder.png

EngineBuilder::create 会调用静态方法 MCJIT::createJIT,将 module、memory manager 和 target machine 的指针一并传递给 MCJIT,并将它们的所有权转交给 MCJIT 对象。

MCJIT 类有成员变量 Dyld,它是 RuntimeDyld 包装类的一个实例。此成员用于 MCJIT 与实际创建的 RuntimeDyldImpl 对象之间的通信(对象加载时创建)。

_images/MCJIT-creation.png

MCJIT 创建后会保存从 EngineBuilder 接收的 Module 对象的指针,但不会立刻为此 module 生成代码。代码生成会延后,直到显式调用 MCJIT::finalizeObject,或调用如 MCJIT::getPointerToFunction 这种需要代码已生成的功能时才触发。

代码生成

如上所述,代码生成被触发时,MCJIT 首先会尝试从其 ObjectCache 成员(若设置过缓存)获取对象文件。如果缓存里没有找到,则 MCJIT 会调用自身的 emitObject 方法。MCJIT::emitObject 会生成一个本地 PassManager 实例和 ObjectBufferStream 实例,然后用 TargetMachine::addPassesToEmitMC 设置好 pass pipeline,最后调用 PassManager::run 处理创建该 MCJIT 的 Module。

_images/MCJIT-load.png

PassManager::run 调用导致 MC 代码生成机制,将生成的完整可重定位目标文件(ELF 或 MachO 格式,取决于目标)输出到 ObjectBufferStream 中,最后刷新完成整个流程。如果有 ObjectCache,这时会把 image 保存到缓存。

至此,ObjectBufferStream 里就有原始对象镜像。在代码可执行前,需将 image 里的代码区和数据区加载到适当内存、应用重定位,并完成内存权限和代码缓存失效(如需)。

对象加载

一旦取得对象镜像(无论是代码生成还是 ObjectCache 取出),会传递给 RuntimeDyld 加载。RuntimeDyld 包装类检查对象格式并实例化 RuntimeDyldELF 或 RuntimeDyldMachO(均派生自 RuntimeDyldImpl),调用 RuntimeDyldImpl::loadObject 方法做实际加载。

_images/MCJIT-dyld-load.png

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。

_images/MCJIT-load-object.png

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。

_images/MCJIT-resolve-relocations.png

当上述重定位应用完成后,MCJIT 调用 RuntimeDyld::getEHFrameSection,如果返回非零数据,则将该 section 数据传给 memory manager 的 registerEHFrames 方法。这样 memory manager 能调用任意目标相关的处理,例如调试器注册 EH frame。

最后,MCJIT 调用 memory manager 的 finalizeMemory 方法。在 finalizeMemory 里,如果需要,会失效目标代码缓存,并为 code/data 区设置最终的内存页权限。

原文地址:https://llvm.org/docs/MCJITDesignAndImplementation.html

05-04
### LLVM 编译器基础设施概述 LLVM 是一种模块化、可重用的编译器和工具链技术集合,最初代表“Low-Level Virtual Machine”,但随着时间推移其含义已被淡化[^2]。它不仅限于构建虚拟机,而是涵盖了多种编译工具和技术,广泛应用于现代编程语言生态中。 #### 安装入门 对于初学者而言,掌握如何安装并配置 LLVM 开发环境至关重要。官方教程提供了详细的入门指南,包括依赖项安装、源码获取以及构建过程说明[^1]。这一步骤确保用户能够顺利运行基础示例程序,并验证环境搭建成功否。 #### 中间表示 (IR) LLVM 的核心之一是其强大的中间层抽象——即 **LLVM Intermediate Representation (IR)**。这种形式独立于具体硬件架构,允许高级别的优化操作在任何目标平台上生效[^3]。开发者可以利用 IR 进行静态分析或者动态执行测试,比如借助 MCJIT 实现即时编译功能[^1]。 #### 自定义 Pass 设计 为了满足特定需求场景下的性能改进或特性增强,学习创建自定义 pass 至关重要。通过继承 `llvm::FunctionPass` 或者其他类型的基类,结合访问模式接口,即可实现个性化的转换逻辑[^1]。下面展示了一个简单的例子来增加函数调用统计: ```cpp #include "llvm/IR/Function.h" #include "llvm/Pass.h" namespace { struct CallCounter : public llvm::FunctionPass { static char ID; CallCounter() : FunctionPass(ID) {} bool runOnFunction(llvm::Function &F) override { int count = 0; for(auto &BB : F){ for(auto &I : BB){ if(I.getOpcode() == llvm::Instruction::Call){ ++count; } } } errs() << "Number of calls in function '" << F.getName() << "' is: " << count << "\n"; return false; // No modification to the IR. } }; } char CallCounter::ID = 0; static llvm::RegisterPass<CallCounter> X("call-counter", "Counts number of call instructions"); ``` 以上代码片段展示了如何遍历函数内部的所有基本块及其指令集,进而计算其中涉及的 CALL 类型数量[^1]。 #### 应用案例研究 除了理论讲解外,《探索 LLVM》还收录了一系列实用范例用于加深理解[^1]。例如解析 C/C++ 文件生成汇编输出;或是针对嵌入式设备定制专属链接脚本等复杂任务均可依靠这套框架完成。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdddn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值