JITLink 和 ORC 的 ObjectLinkingLayer¶
简介¶
本文档旨在提供 JITLink 库的设计和 API 的高级概述。它假设您对链接和可重定位对象文件有一定的了解,但不应需要深入的专业知识。如果您知道节(section)、符号(symbol)和重定位(relocation)是什么,那么您应该会觉得本文档易于理解。如果不是,请提交补丁(为 LLVM 做贡献)或提交错误报告(如何提交 LLVM 错误报告)。
JITLink 是一个用于 JIT 链接的库。它旨在支持 ORC JIT APIs,并且最常通过 ORC 的 ObjectLinkingLayer API 访问。开发 JITLink 的目的是为了支持每种对象格式提供的全套功能;包括静态初始化器、异常处理、线程局部变量和语言运行时注册。支持这些功能使 ORC 能够执行从依赖这些功能的源语言生成的代码(例如,C++ 需要对象格式支持静态初始化器以支持静态构造函数,eh-frame 注册用于异常,TLV 支持用于线程局部变量;Swift 和 Objective-C 需要语言运行时注册以实现许多功能)。对于某些对象格式功能,支持完全在 JITLink 中提供,而对于其他功能,则与(原型)ORC 运行时合作提供。
JITLink 旨在支持以下功能,其中一些仍在开发中
将单个可重定位对象跨进程和跨架构链接到目标执行器进程。
支持所有对象格式功能。
开放的链接器数据结构 (
LinkGraph
) 和 pass 系统。
JITLink 和 ObjectLinkingLayer¶
ObjectLinkingLayer
是 ORC 对 JITLink 的包装器。它是一个 ORC 层,允许将对象添加到 JITDylib
,或从某些更高级别的程序表示形式发出。当发出对象时,ObjectLinkingLayer
使用 JITLink 构建 LinkGraph
(参见 构建 LinkGraphs)并调用 JITLink 的 link
函数将图链接到执行器进程中。
ObjectLinkingLayer
类提供了一个插件 API,ObjectLinkingLayer::Plugin
,用户可以对其进行子类化,以便在链接时检查和修改 LinkGraph
实例,并对重要的 JIT 事件(例如,对象被发出到目标内存)做出反应。这实现了许多在 MCJIT 或 RuntimeDyld 下不可能实现的功能和优化。
ObjectLinkingLayer 插件¶
ObjectLinkingLayer::Plugin
类提供以下方法
modifyPassConfig
在每次要链接 LinkGraph 时调用。可以重写它以安装 JITLink Passes 以在链接过程中运行。void modifyPassConfig(MaterializationResponsibility &MR, jitlink::LinkGraph &G, jitlink::PassConfiguration &Config)
notifyLoaded
在链接开始之前调用,如果需要,可以重写它以设置给定MaterializationResponsibility
的任何初始状态。void notifyLoaded(MaterializationResponsibility &MR)
notifyEmitted
在链接完成并且代码已发出到执行器进程后调用。如果需要,可以重写它以完成MaterializationResponsibility
的状态。Error notifyEmitted(MaterializationResponsibility &MR)
notifyFailed
如果链接在任何时候失败,则调用。可以重写它以对失败做出反应(例如,取消分配任何已分配的资源)。Error notifyFailed(MaterializationResponsibility &MR)
notifyRemovingResources
当请求删除与MaterializationResponsibility
的ResourceKey
K 关联的任何资源时调用。Error notifyRemovingResources(JITDylib &JD, ResourceKey K)
notifyTransferringResources
如果/当请求将与ResourceKey
SrcKey 关联的任何资源的跟踪转移到 DstKey 时调用。void notifyTransferringResources(JITDylib &JD, ResourceKey DstKey, ResourceKey SrcKey)
插件作者需要实现 notifyFailed
、notifyRemovingResources
和 notifyTransferringResources
方法,以便在资源删除或转移或链接失败的情况下安全地管理资源。如果插件不管理任何资源,则可以将这些方法实现为无操作,返回 Error::success()
。
通过调用 addPlugin
方法 [1] 将插件实例添加到 ObjectLinkingLayer
。例如:
// Plugin class to print the set of defined symbols in an object when that
// object is linked.
class MyPlugin : public ObjectLinkingLayer::Plugin {
public:
// Add passes to print the set of defined symbols after dead-stripping.
void modifyPassConfig(MaterializationResponsibility &MR,
jitlink::LinkGraph &G,
jitlink::PassConfiguration &Config) override {
Config.PostPrunePasses.push_back([this](jitlink::LinkGraph &G) {
return printAllSymbols(G);
});
}
// Implement mandatory overrides:
Error notifyFailed(MaterializationResponsibility &MR) override {
return Error::success();
}
Error notifyRemovingResources(JITDylib &JD, ResourceKey K) override {
return Error::success();
}
void notifyTransferringResources(JITDylib &JD, ResourceKey DstKey,
ResourceKey SrcKey) override {}
// JITLink pass to print all defined symbols in G.
Error printAllSymbols(LinkGraph &G) {
for (auto *Sym : G.defined_symbols())
if (Sym->hasName())
dbgs() << Sym->getName() << "\n";
return Error::success();
}
};
// Create our LLJIT instance using a custom object linking layer setup.
// This gives us a chance to install our plugin.
auto J = ExitOnErr(LLJITBuilder()
.setObjectLinkingLayerCreator(
[](ExecutionSession &ES, const Triple &T) {
// Manually set up the ObjectLinkingLayer for our LLJIT
// instance.
auto OLL = std::make_unique<ObjectLinkingLayer>(
ES, std::make_unique<jitlink::InProcessMemoryManager>());
// Install our plugin:
OLL->addPlugin(std::make_unique<MyPlugin>());
return OLL;
})
.create());
// Add an object to the JIT. Nothing happens here: linking isn't triggered
// until we look up some symbol in our object.
ExitOnErr(J->addObject(loadFromDisk("main.o")));
// Plugin triggers here when our lookup of main triggers linking of main.o
auto MainSym = J->lookup("main");
LinkGraph¶
JITLink 将所有可重定位对象格式映射到通用的 LinkGraph
类型,该类型旨在使链接快速而简单(LinkGraph
实例也可以手动创建。请参阅 构建 LinkGraphs)。
可重定位对象格式(例如 COFF、ELF、MachO)在细节上有所不同,但具有共同的目标:表示机器级代码和数据,并带有允许它们在虚拟地址空间中重定位的注释。为此,它们通常包含用于文件内部或外部定义的内容的名称(符号)、必须作为一个单元移动的内容块(节或子节,取决于格式),以及描述如何根据某些目标符号/节的最终地址修补内容的注释(重定位)。
在高层次上,LinkGraph
类型将这些概念表示为装饰图。图中的节点表示符号和内容,边表示重定位。图中每个元素列举如下
Addressable
– 链接图中的一个节点,可以在执行器进程的虚拟地址空间中分配地址。绝对符号和外部符号使用纯
Addressable
实例表示。对象文件内部定义的内容使用Block
子类表示。Block
– 一个Addressable
节点,具有Content
(或标记为零填充)、父Section
、Size
、Alignment
(和AlignmentOffset
)以及Edge
实例列表。块为二进制内容提供了一个容器,这些二进制内容必须在目标地址空间中保持连续(一个布局单元)。对
LinkGraph
实例的许多有趣的低级操作涉及检查或修改块内容或边。Content
表示为llvm::StringRef
,可通过getContent
方法访问。内容仅适用于内容块,不适用于零填充块(使用isZeroFill
进行检查,并且在只需要块大小时首选getSize
,因为它适用于零填充块和内容块)。Section
表示为Section&
引用,可通过getSection
方法访问。Section
类在下面更详细地描述。Size
表示为size_t
,可通过getSize
方法访问,适用于内容块和零填充块。Alignment
表示为uint64_t
,可通过getAlignment
方法访问。它表示块起始位置的最小对齐要求(以字节为单位)。AlignmentOffset
表示为uint64_t
,可通过getAlignmentOffset
方法访问。它表示块起始位置对齐要求的偏移量。这是为了支持最小对齐要求来自块内某个非零偏移量的数据的块。例如,如果一个块由一个字节(字节对齐)和一个 uint64_t(8 字节对齐)组成,则该块将具有 8 字节对齐,对齐偏移量为 7。Edge
实例列表。此列表的迭代器范围由edges
方法返回。Edge
类在下面更详细地描述。
Symbol
– 来自Addressable
(通常是Block
)的偏移量,具有可选的Name
、Linkage
、Scope
、Callable
标志和Live
标志。符号使命名内容(块和 addressable 是匿名的)或使用
Edge
定位内容成为可能。Name
表示为llvm::StringRef
(如果符号没有名称,则等于llvm::StringRef()
),可通过getName
方法访问。Linkage
是 Strong 或 Weak 之一,可通过getLinkage
方法访问。JITLinkContext
可以使用此标志来确定是否应保留或删除此符号定义。Scope
是 Default、Hidden 或 Local 之一,可通过getScope
方法访问。JITLinkContext
可以使用它来确定谁应该能够看到该符号。具有默认作用域的符号应该是全局可见的。具有隐藏作用域的符号应该对同一模拟动态库(例如,ORCJITDylib
)或可执行文件中的其他定义可见,但不能从其他地方可见。具有本地作用域的符号应该仅在当前的LinkGraph
中可见。Callable
是一个布尔值,如果此符号可以调用,则设置为 true,并且可通过isCallable
方法访问。这可以用于自动化引入用于延迟编译的调用桩。Live
是一个布尔值,可以设置它以将此符号标记为死代码消除的根(参见 通用链接算法)。JITLink 的死代码消除算法将在删除任何未标记为 live 的符号(和块)之前,通过图将 live 标志传播到所有可达符号。
Edge
– 一个四元组,包含Offset
(隐式地来自包含Block
的起始位置)、Kind
(描述重定位类型)、Target
和Addend
。边表示块和符号之间的重定位,偶尔也表示其他关系。
Offset
,可通过getOffset
访问,是从包含Edge
的Block
的起始位置的偏移量。Kind
,可通过getKind
访问,是一种重定位类型 – 它描述了应根据Target
的地址对给定Offset
处的块内容进行哪些类型的更改(如果有)。Target
,可通过getTarget
访问,是指向Symbol
的指针,表示其地址与边的Kind
指定的修复计算相关的符号。Addend
,可通过getAddend
访问,是一个常量,其解释由边的Kind
确定。
Section
– 一组Symbol
实例,加上一组Block
实例,具有Name
、一组ProtectionFlags
和Ordinal
。节使迭代与源对象文件中特定节关联的符号或块变得容易。
blocks()
返回节中定义的块集合的迭代器(作为Block*
指针)。symbols()
返回节中定义的符号集合的迭代器(作为Symbol*
指针)。Name
表示为llvm::StringRef
,可通过getName
方法访问。ProtectionFlags
表示为 sys::Memory::ProtectionFlags 枚举,可通过getProtectionFlags
方法访问。这些标志描述了节是否可读、可写、可执行或这些的某种组合。最常见的组合是RW-
用于可写数据,R--
用于常量数据,以及R-X
用于代码。SectionOrdinal
,可通过getOrdinal
访问,是一个用于对节相对于其他节进行排序的数字。它通常用于在布置内存时保留段(一组具有相同内存保护的节)内的节顺序。
对于图论家:LinkGraph
是二分的,一组 Symbol
节点和一组 Addressable
节点。每个 Symbol
节点都有一个(隐式)边指向其目标 Addressable
。每个 Block
都有一组边(可能为空,表示为 Edge
实例)返回到 Symbol
集的元素。为了方便和常用算法的性能,符号和块进一步分组到 Sections
中。
LinkGraph
本身提供了用于构造、删除和迭代节、符号和块的操作。它还提供了与链接过程相关的元数据和实用程序
图元素操作
sections
返回图中所有节的迭代器。findSectionByName
返回指向具有给定名称的节的指针(作为Section*
),如果它存在,否则返回 nullptr。blocks
返回图中所有块的迭代器(跨所有节)。defined_symbols
返回图中所有已定义符号的迭代器(跨所有节)。external_symbols
返回图中所有外部符号的迭代器。absolute_symbols
返回图中所有绝对符号的迭代器。createSection
创建具有给定名称和保护标志的节。createContentBlock
创建具有给定初始内容、父节、地址、对齐方式和对齐偏移量的块。createZeroFillBlock
创建具有给定大小、父节、地址、对齐方式和对齐偏移量的零填充块。addExternalSymbol
创建一个新的 addressable 和符号,具有给定的名称、大小和链接。addAbsoluteSymbol
创建一个新的 addressable 和符号,具有给定的名称、地址、大小、链接、作用域和活跃度。addCommonSymbol
便利函数,用于创建具有给定名称、作用域、节、初始地址、大小、对齐方式和活跃度的零填充块和弱符号。addAnonymousSymbol
为给定的块创建一个新的匿名符号,具有偏移量、大小、可调用性和活跃度。addDefinedSymbol
为给定的块创建一个新的符号,具有名称、偏移量、大小、链接、作用域、可调用性和活跃度。makeExternal
通过创建一个新的 addressable 并将符号指向它,将以前定义的符号转换为外部符号。现有块不会被删除,但可以通过调用removeBlock
手动删除(如果未被引用)。指向该符号的所有边仍然有效,但该符号现在必须在此LinkGraph
之外定义。removeExternalSymbol
删除外部符号及其目标 addressable。目标 addressable 不得被任何其他符号引用。removeAbsoluteSymbol
删除绝对符号及其目标 addressable。目标 addressable 不得被任何其他符号引用。removeDefinedSymbol
删除已定义的符号,但不删除其目标块。removeBlock
删除给定的块。splitBlock
在给定的索引处将给定的块拆分为两个(当已知块包含可分解的记录时很有用,例如 eh-frame 节中的 CFI 记录)。
图实用程序操作
getName
返回此图的名称,该名称通常基于输入对象文件的名称。getTargetTriple
返回执行器进程的 llvm::Triple。getPointerSize
返回执行器进程中指针的大小(以字节为单位)。getEndianness
返回执行器进程的字节序。allocateString
将数据从给定的llvm::Twine
复制到链接图的内部分配器中。这可以用于确保在 pass 的执行结束后,在 pass 内部创建的内容仍然存在。
通用链接算法¶
JITLink 提供了一种通用链接算法,可以通过引入 JITLink Passes 在某些点进行扩展/修改。
在每个阶段结束时,链接器将其状态打包到一个延续中,并调用 JITLinkContext
对象来执行(可能是高延迟的)异步操作:分配内存、解析外部符号,最后将链接的内存传输到执行进程。
阶段 1
一旦初始配置(包括 pass 管道设置)完成,
link
函数立即调用此阶段。运行预剪枝 passes。
这些 passes 在图被剪枝之前在图上调用。在这个阶段,
LinkGraph
节点仍然具有其原始 vmaddr。一个标记-live pass(由JITLinkContext
提供)将在此序列结束时运行,以标记初始的 live 符号集。值得注意的用例:标记节点为 live,访问/复制将被剪枝的图数据(例如,对 JIT 很重要但不链接过程不需要的元数据)。
剪枝(死代码消除)
LinkGraph
。删除从初始 live 符号集无法访问的所有符号和块。
这允许 JITLink 删除无法访问的符号/内容,包括被覆盖的弱定义和冗余 ODR 定义。
运行后剪枝 passes。
这些 passes 在死代码消除后但在分配内存或节点分配其最终目标 vmaddr 之前在图上运行。
在此阶段运行的 passes 受益于剪枝,因为死函数和数据已从图中删除。但是,仍然可以将新内容添加到图中,因为尚未分配目标内存和工作内存。
值得注意的用例:构建全局偏移表 (GOT)、过程链接表 (PLT) 和线程局部变量 (TLV) 条目。
异步分配内存。
调用
JITLinkContext
的JITLinkMemoryManager
为图分配工作内存和目标内存。作为此过程的一部分,JITLinkMemoryManager
将图中定义的所有节点的地址更新为其分配的目标地址。注意:此步骤仅更新在此图中定义的节点的地址。外部符号仍将具有空地址。
阶段 2
运行后分配 passes。
这些 passes 在分配工作内存和目标内存之后但在将图中符号的最终地址通知
JITLinkContext
之前在图上运行。这使这些 passes 有机会在任何 JITLink 客户端(尤其是 ORC 查询符号解析)尝试访问它们之前设置与目标地址关联的数据结构。值得注意的用例:设置目标地址和 JIT 数据结构之间的映射,例如
__dso_handle
和JITDylib*
之间的映射。将分配的符号地址通知
JITLinkContext
。在链接图上调用
JITLinkContext::notifyResolved
,允许客户端对为此图进行的符号地址分配做出反应。在 ORC 中,这用于通知对已解析符号的任何挂起查询,包括来自并发运行的 JITLink 实例的挂起查询,这些实例已达到下一步,并且正在等待此图中符号的地址以继续其链接。识别外部符号并异步解析其地址。
调用
JITLinkContext
以解析图中任何外部符号的目标地址。
阶段 3
应用外部符号解析结果。
这会更新所有外部符号的地址。此时,图中的所有节点都具有其最终目标地址,但是节点内容仍然指向对象文件中的原始数据。
运行预修复 passes。
这些 passes 在分配所有节点的最终目标地址之后但在将节点内容复制到工作内存并进行修复之前在图上调用。在此阶段运行的 passes 可以根据地址布局对图和内容进行后期优化。
值得注意的用例:GOT 和 PLT 放松,其中对于在分配的内存布局下可直接访问的修复目标,绕过 GOT 和 PLT 访问。
将块内容复制到工作内存并应用修复。
将所有块内容复制到分配的工作内存中(遵循目标布局)并应用修复。图块更新为指向已修复的内容。
运行后修复 passes。
这些 passes 在应用修复并且块更新为指向已修复内容之后在图上调用。
后修复 passes 可以检查块内容,以查看将复制到分配的目标地址的确切字节。
异步完成内存。
调用
JITLinkMemoryManager
以将工作内存复制到执行器进程并应用请求的权限。
阶段 3。
通知上下文图已发出。
调用
JITLinkContext::notifyFinalized
并移交此图内存分配的JITLinkMemoryManager::FinalizedAlloc
对象。 这允许上下文跟踪/持有内存分配并对新发出的定义做出反应。 在 ORC 中,这用于更新ExecutionSession
实例的依赖关系图,如果其所有依赖项也已发出,这可能会导致这些符号(以及可能的其他符号)变为就绪状态。
Passes(Passes)¶
JITLink passes 是 std::function<Error(LinkGraph&)>
实例。 它们可以自由地检查和修改给定的 LinkGraph
,但须遵守它们运行阶段的约束(请参阅 通用链接算法)。 如果 pass 返回 Error::success()
,则链接继续。 如果 pass 返回失败值,则链接停止,并且通知 JITLinkContext
链接失败。
JITLink 后端(例如 MachO/x86-64 将 GOT 和 PLT 构造实现为 pass)和外部客户端(如 ObjectLinkingLayer::Plugin
)都可以使用 Passes。
结合开放的 LinkGraph
API,JITLink passes 能够实现强大的新功能。 例如
Relaxation optimizations(松弛优化)– pre-fixup pass 可以检查 GOT 访问和 PLT 调用,并识别入口目标地址和访问地址足够接近以直接访问的情况。 在这种情况下,pass 可以重写包含块的指令流,并更新 fixup 边以使访问直接。
此功能的代码如下所示
Error relaxGOTEdges(LinkGraph &G) {
for (auto *B : G.blocks())
for (auto &E : B->edges())
if (E.getKind() == x86_64::GOTLoad) {
auto &GOTTarget = getGOTEntryTarget(E.getTarget());
if (isInRange(B.getFixupAddress(E), GOTTarget)) {
// Rewrite B.getContent() at fixup address from
// MOVQ to LEAQ
// Update edge target and kind.
E.setTarget(GOTTarget);
E.setKind(x86_64::PCRel32);
}
}
return Error::success();
}
Metadata registration(元数据注册)– Post allocation passes 可用于记录目标中节的地址范围。 这可用于在内存最终确定后在目标中注册元数据(例如,异常处理帧、语言元数据)。
Error registerEHFrameSection(LinkGraph &G) {
if (auto *Sec = G.findSectionByName("__eh_frame")) {
SectionRange SR(*Sec);
registerEHFrameSection(SR.getStart(), SR.getEnd());
}
return Error::success();
}
Record call sites for later mutation(记录调用点以供后续修改)– post-allocation pass 可以记录对特定函数的所有调用点,从而允许稍后在运行时更新这些调用点(例如,用于 instrumentation(检测),或使函数能够延迟编译,但在编译后仍可直接调用)。
StringRef FunctionName = "foo";
std::vector<ExecutorAddr> CallSitesForFunction;
auto RecordCallSites =
[&](LinkGraph &G) -> Error {
for (auto *B : G.blocks())
for (auto &E : B.edges())
if (E.getKind() == CallEdgeKind &&
E.getTarget().hasName() &&
E.getTraget().getName() == FunctionName)
CallSitesForFunction.push_back(B.getFixupAddress(E));
return Error::success();
};
使用 JITLinkMemoryManager 进行内存管理¶
JIT 链接需要分配两种类型的内存:JIT 进程中的工作内存和执行进程中的目标内存(这些进程和内存分配可能是一个相同的,具体取决于用户希望如何构建其 JIT)。 它还需要这些分配符合目标进程中请求的代码模型(例如,MachO/x86-64 的 Small 代码模型要求模拟 dylib 的所有代码和数据都分配在 4Gb 内)。 最后,让内存管理器负责将内存传输到目标地址空间并应用内存保护是很自然的,因为内存管理器必须知道如何与执行器通信,并且由于共享和保护分配通常可以通过主机操作系统的虚拟内存管理 API 有效地管理(在跨进程在同一台机器上为安全运行的常见情况下)。
为了满足这些要求,JITLinkMemoryManager
采用以下设计: 内存管理器本身只有两个用于异步操作的虚方法(每个方法都有用于同步调用的便捷重载)
/// Called when allocation has been completed.
using OnAllocatedFunction =
unique_function<void(Expected<std::unique_ptr<InFlightAlloc>)>;
/// Called when deallocation has completed.
using OnDeallocatedFunction = unique_function<void(Error)>;
/// Call to allocate memory.
virtual void allocate(const JITLinkDylib *JD, LinkGraph &G,
OnAllocatedFunction OnAllocated) = 0;
/// Call to deallocate memory.
virtual void deallocate(std::vector<FinalizedAlloc> Allocs,
OnDeallocatedFunction OnDeallocated) = 0;
allocate
方法接受一个 JITLinkDylib*
,表示目标模拟 dylib,一个对必须为其分配内存的 LinkGraph
的引用,以及一个在构造 InFlightAlloc
后运行的回调。 JITLinkMemoryManager
实现可以(可选地)使用 JD
参数来管理每个模拟 dylib 的内存池(因为代码模型约束通常是按每个 dylib 而不是跨 dylib 强加的)[2]。 LinkGraph
描述了我们需要为其分配内存的目标文件。 分配器必须为图中定义的所有块分配工作内存,为执行进程内存中的每个块分配地址空间,并更新块的地址以反映此分配。 块内容应复制到工作内存,但尚无需传输到执行器内存(这将在内容修复后完成)。 JITLinkMemoryManager
实现可以完全负责这些步骤,或者使用 BasicLayout
实用程序将任务简化为为段分配工作内存和执行器内存:由权限、对齐方式、内容大小和零填充大小定义的内存块。 分配步骤完成后,内存管理器应构造一个 InFlightAlloc
对象来表示分配,然后将此对象传递给 OnAllocated
回调。
InFlightAlloc
对象具有两个虚方法
using OnFinalizedFunction = unique_function<void(Expected<FinalizedAlloc>)>;
using OnAbandonedFunction = unique_function<void(Error)>;
/// Called prior to finalization if the allocation should be abandoned.
virtual void abandon(OnAbandonedFunction OnAbandoned) = 0;
/// Called to transfer working memory to the target and apply finalization.
virtual void finalize(OnFinalizedFunction OnFinalized) = 0;
如果链接成功到最终确定步骤,则链接过程将在 InFlightAlloc
对象上调用 finalize
方法,否则它将调用 abandon
以指示链接期间发生了一些错误。 对 InFlightAlloc::finalize
方法的调用应导致分配的内容从工作内存传输到执行器内存,并运行权限。 对 abandon
的调用应导致两种类型的内存都被释放。
在成功最终确定时,InFlightAlloc::finalize
方法应构造一个 FinalizedAlloc
对象(一个不透明的 uint64_t id,JITLinkMemoryManager
可以使用它来标识用于释放的执行器内存),并将其传递给 OnFinalized
回调。
可以通过调用 JITLinkMemoryManager::dealloc
方法来释放最终确定的分配(由 FinalizedAlloc
对象表示)。 此方法接受 FinalizedAlloc
对象的向量,因为同时释放多个对象是很常见的,这允许我们批量处理这些请求以传输到执行进程。
JITLink 提供了此接口的简单进程内实现:InProcessMemoryManager
。 它分配页面一次,并将它们重新用作工作内存和目标内存。
ORC 提供了跨进程的 MapperJITLinkMemoryManager
,它可以使用共享内存或基于 ORC-RPC 的通信将内容传输到执行进程。
JITLinkMemoryManager 和安全性¶
JITLink 为单独的执行器进程链接 JIT 代码的能力可用于提高 JIT 系统的安全性: 执行器进程可以被沙盒化,在 VM 中运行,甚至在完全独立的机器上运行。
JITLink 的内存管理器接口足够灵活,可以允许在性能和安全性之间进行一系列权衡。 例如,在代码页必须签名的系统上(防止代码被更新),内存管理器可以在链接后释放工作内存页,以释放运行 JITLink 的进程中的内存。 或者,在允许 RWX 页面的系统上,内存管理器可以将相同的页面用于工作内存和目标内存,方法是将它们标记为 RWX,从而允许就地修改代码而无需额外的开销。 最后,如果 RWX 页面不允许但物理内存页面的双虚拟映射允许,则内存管理器可以将物理页面双重映射为 JITLink 进程中的 RW- 和执行器进程中的 R-X,从而允许从 JITLink 进程进行修改,但不允许从执行器进行修改(以双重映射的额外管理开销为代价)。
错误处理¶
JITLink 广泛使用 llvm::Error
类型(有关详细信息,请参阅 LLVM 程序员手册 的错误处理部分)。 链接过程本身、所有 passes、内存管理器接口以及 JITLinkContext
上的操作都允许失败。 鼓励链接图构造实用程序(尤其是对象格式的解析器)在应用之前验证输入并验证 fixups(例如,使用范围检查)。
任何错误都将停止链接过程并通知上下文失败。 在 ORC 中,报告的失败会传播到对失败链接提供的定义挂起的查询,以及通过依赖关系图的边传播到等待依赖符号的任何查询。
与 ORC 运行时的连接¶
ORC 运行时(目前正在开发中)旨在为高级 JIT 功能提供运行时支持,包括需要在执行器中执行非平凡操作的对象格式功能(例如,运行初始化程序、管理线程本地存储、向语言运行时注册等)。
ORC 运行时对对象格式功能的支持通常需要运行时(在执行器进程中执行)和 JITLink(在 JIT 进程中运行并可以检查 LinkGraphs 以确定必须在执行器中执行哪些操作)之间的合作。 例如: MachO 静态初始化程序在 ORC 运行时中的执行由 jit_dlopen
函数执行,该函数回调到 JIT 进程以请求要遍历的 __mod_init
节的地址范围列表。 此列表由 MachOPlatformPlugin
整理,它安装一个 pass 以记录每个对象在链接到目标时的此信息。
构造 LinkGraphs¶
客户端通常访问和操作由 ObjectLinkingLayer
实例为它们创建的 LinkGraph
实例,但它们可以手动创建
通过直接构造和填充
LinkGraph
实例。通过使用
createLinkGraph
函数族从包含目标文件的内存缓冲区创建LinkGraph
。 这就是ObjectLinkingLayer
通常创建LinkGraphs
的方式。
当对象格式和架构都提前已知时,可以使用
createLinkGraph_<Object-Format>_<Architecture>
。当对象格式提前已知,但架构未知时,可以使用
createLinkGraph_<Object-Format>
。 在这种情况下,架构将通过检查对象标头来确定。当对象格式和架构都提前未知时,可以使用
createLinkGraph
。 在这种情况下,将检查对象标头以确定格式和架构。
JIT 链接¶
JIT 链接器概念是在 LLVM 早期一代的 JIT API MCJIT 中引入的。 在 MCJIT 中,RuntimeDyld 组件通过在通常的编译器管道末尾添加内存中链接步骤,实现了 LLVM 作为内存中编译器的重用。 MCJIT 没有像编译器通常那样将可重定位对象转储到磁盘,而是将它们传递给 RuntimeDyld 以链接到目标进程中。
这种链接方法与标准的静态或动态链接不同
静态链接器将一个或多个可重定位对象文件作为输入,并将它们链接到磁盘上的可执行文件或动态库中。
动态链接器将重定位应用于已加载到内存中的可执行文件和动态库。
JIT 链接器一次接受一个可重定位对象文件,并将其链接到目标进程中,通常使用上下文对象来允许链接的代码解析目标中的符号。
RuntimeDyld¶
为了保持 RuntimeDyld 实现的简单性,MCJIT 对编译代码施加了一些限制
它必须使用 Large 代码模型,并且经常限制可用的重定位模型,以限制必须支持的重定位类型。
它要求所有符号都具有强链接和默认可见性 – 其他链接/可见性的行为没有明确定义。
它约束和/或禁止使用需要运行时支持的功能,例如静态初始化程序或线程本地存储。
由于这些限制,并非所有 LLVM 支持的语言功能都在 MCJIT 下工作,并且要在 JIT 下加载的对象必须编译为目标(排除在 JIT 下使用来自其他来源的预编译代码)。
RuntimeDyld 还提供了对链接过程本身非常有限的可见性: 客户端可以访问节大小的保守估计值(RuntimeDyld 将 stub 大小和填充估计值捆绑到节大小值中)和最终重定位的字节,但无法访问 RuntimeDyld 的内部对象表示。
消除这些限制和局限性是开发 JITLink 的主要动机之一。
llvm-jitlink 工具¶
llvm-jitlink
工具是 JITLink 库的命令行包装器。 它加载一组可重定位对象文件,然后使用 JITLink 链接它们。 根据使用的选项,它将执行它们或验证链接的内存。
llvm-jitlink
工具最初旨在通过提供简单的测试环境来帮助 JITLink 开发。
基本用法¶
默认情况下,llvm-jitlink
将链接在命令行上传递的对象集,然后搜索 “main” 函数并执行它
% cat hello-world.c
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("hello, world!\n");
return 0;
}
% clang -c -o hello-world.o hello-world.c
% llvm-jitlink hello-world.o
Hello, World!
可以指定多个对象,并且可以使用 -args 选项为 JIT 化的 main 函数提供参数
% cat print-args.c
#include <stdio.h>
void print_args(int argc, char *argv[]) {
for (int i = 0; i != argc; ++i)
printf("arg %i is \"%s\"\n", i, argv[i]);
}
% cat print-args-main.c
void print_args(int argc, char *argv[]);
int main(int argc, char *argv[]) {
print_args(argc, argv);
return 0;
}
% clang -c -o print-args.o print-args.c
% clang -c -o print-args-main.o print-args-main.c
% llvm-jitlink print-args.o print-args-main.o -args a b c
arg 0 is "a"
arg 1 is "b"
arg 2 is "c"
可以使用 -entry <entry point name>
选项指定备用入口点。
其他选项可以通过调用 llvm-jitlink -help
找到。
llvm-jitlink 作为回归测试实用程序¶
llvm-jitlink
的主要目标之一是为 JITLink 启用可读的回归测试。 为此,它支持两个选项
-noexec
选项告诉 llvm-jitlink 在查找入口点之后但在尝试执行之前停止。 由于链接的代码未执行,因此即使您无权访问要链接的目标,也可以使用它来链接其他目标(在这种情况下,可以使用 -define-abs
或 -phony-externals
选项来提供任何缺少的定义)。
-check <check-file>
选项可用于针对工作内存运行一组 jitlink-check
表达式。 它通常与 -noexec
结合使用,因为目的是验证 JIT 化的内存而不是运行代码,并且 -noexec
允许我们从当前进程链接任何受支持的目标架构。 在 -check
模式下,llvm-jitlink
将扫描给定的 check-file 中形式为 # jitlink-check: <expr>
的行。 有关此用法的示例,请参阅 llvm/test/ExecutionEngine/JITLink
。
通过 llvm-jitlink-executor 进行远程执行¶
默认情况下,llvm-jitlink
会将其给定对象链接到自己的进程中,但这可以通过两个选项覆盖
-oop-executor[=/path/to/executor]
选项告诉 llvm-jitlink
执行给定的执行器(默认为 llvm-jitlink-executor
),并通过文件描述符与其通信,文件描述符以 filedescs=<in-fd>,<out-fd>
格式作为第一个参数传递给执行器。
-oop-executor-connect=<host>:<port>
选项告诉 llvm-jitlink
通过 TCP 连接到给定主机和端口上已在运行的执行器。 要使用此选项,您需要手动启动 llvm-jitlink-executor
,并将 listen=<host>:<port>
作为第一个参数。
Harness 模式¶
-harness
选项允许将一组输入对象指定为测试 harness,常规对象文件隐式地被视为要测试的对象。 harness 集中符号的定义会覆盖测试集中符号的定义,并且来自 harness 的外部引用会导致测试集中本地符号的自动作用域提升(对通常的链接器规则的这些修改是通过 ObjectLinkingLayer::Plugin
完成的,该插件在 llvm-jitlink
看到 -harness
选项时安装)。
通过这些修改,我们可以通过模拟这些函数的被调用者来有选择地测试对象文件中的函数。 例如,假设我们有一个对象文件 test_code.o
,从以下 C 源代码编译而来(我们无需访问它)
void irrelevant_function() { irrelevant_external(); }
int function_to_mock(int X) {
return /* some function of X */;
}
static void function_to_test() {
...
int Y = function_to_mock();
printf("Y is %i\n", Y);
}
如果我们想知道当我们更改 function_to_mock
的行为时 function_to_test
的行为方式,我们可以通过编写测试 harness 来测试它
void function_to_test();
int function_to_mock(int X) {
printf("used mock utility function\n");
return 42;
}
int main(int argc, char *argv[]) {
function_to_test():
return 0;
}
在正常情况下,这些对象无法链接在一起: function_to_test
是静态的,无法在 test_code.o
外部解析,两个 function_to_mock
函数将导致重复定义错误,并且 irrelevant_external
未定义。 但是,使用 -harness
和 -phony-externals
,我们可以使用以下命令运行此代码
% clang -c -o test_code_harness.o test_code_harness.c
% llvm-jitlink -phony-externals test_code.o -harness test_code_harness.o
used mock utility function
Y is 42
-harness
选项可能对想要对构建产品执行一些非常晚期的测试以验证编译后的代码是否按预期运行的人员感兴趣。 在基本的 C 测试用例中,这相对简单。 更复杂语言(例如 C++)的 mocks 更棘手: 任何涉及类的代码都倾向于具有大量非平凡的表面积(例如 vtables),这将需要非常小心地进行模拟。
JITLink 后端开发人员的提示¶
大量使用 assert 和
llvm::Error
。 不要假设输入对象格式良好: 返回 libObject(或您自己的对象解析代码)产生的任何错误,并在构造时进行验证。 仔细考虑 contract(应该使用 asserts 和 llvm_unreachable 验证)和环境错误(应该生成llvm::Error
实例)之间的区别。不要假设您正在进程内链接。 在
LinkGraph
中读取/写入内容时,请使用 libSupport 的大小调整的、特定于字节序的类型。
作为 “最低可行” JITLink 包装器,llvm-jitlink
工具对于引入新的 JITLink 后端的开发人员来说是宝贵的资源。 标准工作流程是从将不受支持的对象扔给该工具并查看返回的错误开始,然后修复该错误(您通常可以根据其他格式或架构的现有代码合理猜测应该做什么)。
在 LLVM 的调试版本中,-debug-only=jitlink
选项会在链接过程中转储来自 JITLink 库的日志。 这些日志对于一眼发现某些错误很有用。 -debug-only=llvm_jitlink
选项会转储来自 llvm-jitlink
工具的日志,这对于调试测试用例(它通常比 -debug-only=jitlink
更简洁)和工具本身都很有用。
-oop-executor
和 -oop-executor-connect
选项有助于测试跨进程和跨架构用例的处理。
路线图¶
JITLink 正在积极开发中。 到目前为止,工作重点是 MachO 实现。 在 LLVM 12 中,对 x86-64 上的 ELF 提供有限的支持。
主要的未完成项目包括
重构架构支持以最大限度地跨格式共享。
所有格式都应该能够为每个受支持的架构共享大部分架构特定的代码(尤其是重定位)。
重构 ELF 链接图构造。
ELF 的链接图构造目前在 ELF_x86_64.cpp 文件中实现,并与 x86-64 重定位解析代码相关联。 大部分代码是通用的,应该像现有的通用 MachOLinkGraphBuilder 一样拆分为 ELFLinkGraphBuilder 基类。
实现对 arm32 的支持。
实现对其他新架构的支持。
JITLink 可用性和功能状态¶
下表描述了各种格式/架构组合的 JITlink 后端的状态(截至 2023 年 7 月)。
支持级别
None(无): 没有后端。 JITLink 将返回 “architecture not supported(不支持的架构)” 错误。 在下表中用空单元格表示。
Skeleton(骨架): 后端存在,但不支持常用重定位。 即使是简单的程序也可能触发 “unsupported relocation(不支持的重定位)” 错误。 此状态下的后端可能很容易通过实现新的重定位来改进。 考虑参与进来!
Basic(基本): 后端支持简单的程序,但尚未准备好全面使用。
Usable(可用): 后端可用于至少一种代码和重定位模型的通用用途。
Good(良好): 后端支持几乎所有重定位。 诸如 native thread local storage(原生线程本地存储)之类的高级功能可能尚不可用。
Complete(完整): 后端支持所有重定位和对象格式功能。
架构 |
ELF |
COFF |
MachO |
---|---|---|---|
arm32 |
骨架 |
||
arm64 |
可用 |
良好 |
|
LoongArch |
良好 |
||
PowerPC 64 |
可用 |
||
RISC-V |
良好 |
||
x86-32 |
基本 |
||
x86-64 |
良好 |
可用 |
良好 |