JITLink 和 ORC 的 ObjectLinkingLayer¶
简介¶
本文档旨在提供 JITLink 库的设计和 API 的高级概述。它假设您对链接和可重定位对象文件有一定的了解,但不需要深入的专业知识。如果您知道什么是节、符号和重定位,您应该可以理解本文档。如果不了解,请提交补丁 (为 LLVM 贡献代码) 或提交 Bug (如何提交 LLVM Bug 报告)。
JITLink 是一个用于 JIT 链接 的库。它构建于支持 ORC JIT API,并且最常通过 ORC 的 ObjectLinkingLayer API 访问。JITLink 的开发目标是支持每种对象格式提供的全部功能;包括静态初始化器、异常处理、线程本地变量和语言运行时注册。支持这些功能使 ORC 能够执行从依赖于这些功能的源语言生成的代码(例如,C++ 需要对象格式支持静态初始化器以支持静态构造函数、eh-frame 注册以支持异常以及 TLV 支持以支持线程本地变量;Swift 和 Objective-C 需要语言运行时注册许多功能)。对于某些对象格式功能,支持完全在 JITLink 中提供,而对于其他功能,则与(原型)ORC 运行时协作提供。
JITLink 旨在支持以下功能,其中一些功能仍在开发中
将单个可重定位对象跨进程和跨架构链接到目标执行器进程。
支持所有对象格式功能。
开放链接器数据结构(
LinkGraph
)和传递系统。
JITLink 和 ObjectLinkingLayer¶
ObjectLinkingLayer
是 ORC 对 JITLink 的包装器。它是一个 ORC 层,允许将对象添加到 JITDylib
中,或从某些更高级别的程序表示形式发出。当发出对象时,ObjectLinkingLayer
使用 JITLink 构建 LinkGraph
(参见 构建 LinkGraph)并调用 JITLink 的 link
函数将图链接到执行器进程中。
ObjectLinkingLayer
类提供了一个插件 API,ObjectLinkingLayer::Plugin
,用户可以对其进行子类化,以便在链接时检查和修改 LinkGraph
实例,并对重要的 JIT 事件(例如将对象发出到目标内存)做出反应。这使得许多在 MCJIT 或 RuntimeDyld 下不可能实现的功能和优化成为可能。
ObjectLinkingLayer 插件¶
ObjectLinkingLayer::Plugin
类提供以下方法
modifyPassConfig
在每次即将链接 LinkGraph 时被调用。它可以被重写以安装 JITLink 传递在链接过程中运行。void modifyPassConfig(MaterializationResponsibility &MR, const Triple &TT, 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(ResourceKey K)
notifyTransferringResources
如果/何时请求将与ResourceKey
SrcKey 关联的任何资源的跟踪转移到 DstKey 时被调用。void notifyTransferringResources(ResourceKey DstKey, ResourceKey SrcKey)
插件作者需要实现 notifyFailed
、notifyRemovingResources
和 notifyTransferringResources
方法,以便在资源删除或传输或链接失败的情况下安全地管理资源。如果插件未管理任何资源,则这些方法可以实现为返回 Error::success()
的空操作。
通过调用 addPlugin
方法将插件实例添加到 ObjectLinkingLayer
中 [1]。例如:
// 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,
const Triple &TT,
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(ResourceKey K) override {
return Error::success();
}
void notifyTransferringResources(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
实例。请参阅 构建 LinkGraph)。
可重定位对象格式(例如 COFF、ELF、MachO)在细节上有所不同,但它们有一个共同的目标:用允许它们在虚拟地址空间中重新定位的注释来表示机器级代码和数据。为此,它们通常包含文件内部或外部定义的内容的名称(符号)、必须作为单元移动的内容块(节或子节,具体取决于格式)以及描述如何根据某些目标符号/节的最终地址来修补内容的注释(重定位)。
在高级别上,LinkGraph
类型将这些概念表示为一个装饰图。图中的节点表示符号和内容,边表示重定位。图的每个元素都列在下面
Addressable
– 链接图中可以分配执行器进程虚拟地址空间中的地址的节点。绝对符号和外部符号使用普通的
Addressable
实例表示。对象文件内部定义的内容使用Block
子类表示。Block
– 一个具有Content
(或标记为零填充)的Addressable
节点,一个父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
标志。符号使得能够为内容命名(块和可寻址对象是匿名的),或者使用
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 的去除死代码算法将在删除任何未标记为活动的符号(和块)之前,通过图传播活动标志到所有可到达的符号。
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
创建一个具有给定名称、大小和链接的新可寻址对象和符号。addAbsoluteSymbol
创建一个具有给定名称、地址、大小、链接、范围和活动的新可寻址对象和符号。addCommonSymbol
用于创建具有给定名称、范围、节、初始地址、大小、对齐方式和活动性的零填充块和弱符号的便利函数。addAnonymousSymbol
为给定的块、偏移量、大小、可调用性和活动性创建一个新的匿名符号。addDefinedSymbol
为给定的块创建一个新的符号,包含名称、偏移量、大小、链接、范围、可调用性和活动性。makeExternal
通过创建一个新的可寻址对象并将符号指向它,将以前定义的符号转换为外部符号。不会删除现有的块,但可以通过调用removeBlock
手动删除(如果未引用)。对符号的所有边都保持有效,但现在必须在此LinkGraph
外部定义符号。removeExternalSymbol
删除外部符号及其目标可寻址对象。目标可寻址对象不得被任何其他符号引用。removeAbsoluteSymbol
删除绝对符号及其目标可寻址对象。目标可寻址对象不得被任何其他符号引用。removeDefinedSymbol
删除已定义的符号,但 *不会* 删除其目标块。removeBlock
删除给定的块。splitBlock
在给定索引处将给定块拆分为两个(在已知块包含可分解记录的情况下有用,例如 eh-frame 节中的 CFI 记录)。
图实用程序操作
getName
返回此图的名称,该名称通常基于输入目标文件的名称。getTargetTriple
返回执行器进程的 llvm::Triple。getPointerSize
返回执行器进程中指针的大小(以字节为单位)。getEndinaness
返回执行器进程的字节序。allocateString
将数据从给定的llvm::Twine
复制到链接图的内部分配器中。这可以用来确保在 Pass 内部创建的内容在其执行结束后仍然存在。
通用链接算法¶
JITLink 提供了一个通用链接算法,可以通过引入 JITLink Pass 在某些点进行扩展/修改。
在每个阶段结束时,链接器将其状态打包到一个延续中,并调用 JITLinkContext
对象执行(可能延迟较高)的异步操作:分配内存、解析外部符号,最后将链接的内存传输到正在执行的进程。
阶段 1
此阶段在初始配置(包括 Pass 管道设置)完成后,由
link
函数立即调用。运行预剪枝 Pass。
这些 Pass 在图被剪枝之前被调用。在此阶段,
LinkGraph
节点仍然保留其原始的 vmaddrs。一个标记活跃的 Pass(由JITLinkContext
提供)将在此序列的末尾运行,以标记初始的活跃符号集。值得注意的用例:标记节点活跃、访问/复制将被剪枝的图数据(例如对 JIT 非常重要但链接过程不需要的元数据)。
剪枝(删除未使用的)
LinkGraph
。删除所有无法从初始活跃符号集访问的符号和块。
这允许 JITLink 删除无法访问的符号/内容,包括被覆盖的弱符号和冗余的 ODR 定义。
运行后剪枝 Pass。
这些 Pass 在删除未使用的内容后运行,但在分配内存或节点分配其最终目标 vmaddrs 之前运行。
在此阶段运行的 Pass 获益于剪枝,因为未使用的函数和数据已从图中删除。但是,仍然可以向图中添加新内容,因为目标内存和工作内存尚未分配。
值得注意的用例:构建全局偏移表 (GOT)、过程链接表 (PLT) 和线程局部变量 (TLV) 条目。
异步分配内存。
调用
JITLinkContext
的JITLinkMemoryManager
为图分配工作内存和目标内存。在此过程中,JITLinkMemoryManager
会将图中定义的所有节点的地址更新为其分配的目标地址。注意:此步骤仅更新在此图中定义的节点的地址。外部符号将仍然具有空地址。
阶段 2
运行后分配 Pass。
在分配工作内存和目标内存后,但在
JITLinkContext
被通知图中符号的最终地址之前,这些 Pass 会在图上运行。这使这些 Pass 有机会在任何 JITLink 客户端(尤其是 ORC 查询符号解析)尝试访问它们之前设置与目标地址关联的数据结构。值得注意的用例:设置目标地址和 JIT 数据结构之间的映射,例如
__dso_handle
和JITDylib*
之间的映射。通知
JITLinkContext
已分配的符号地址。在链接图上调用
JITLinkContext::notifyResolved
,允许客户端对为该图做出的符号地址分配做出反应。在 ORC 中,这用于通知任何挂起的已解析符号查询,包括来自并发运行的 JITLink 实例的挂起查询,这些实例已到达下一步并正在等待此图中符号的地址以继续其链接。识别外部符号并异步解析其地址。
调用
JITLinkContext
来解析图中任何外部符号的目标地址。
阶段 3
应用外部符号解析结果。
这更新了所有外部符号的地址。此时,图中的所有节点都具有其最终的目标地址,但是节点内容仍然指向对象文件中的原始数据。
运行预修正 Pass。
在所有节点都分配了其最终目标地址后,但在节点内容被复制到工作内存并进行修正之前,会调用这些 Pass。在此阶段运行的 Pass 可以根据地址布局对图和内容进行后期优化。
值得注意的用例:GOT 和 PLT 松弛,其中对于在分配的内存布局下可以直接访问的修正目标,会绕过 GOT 和 PLT 访问。
将块内容复制到工作内存并应用修正。
将所有块内容复制到已分配的工作内存中(遵循目标布局)并应用修正。图块被更新为指向已修正的内容。
运行后修正 Pass。
在应用修正并将块更新为指向已修正的内容后,会调用这些 Pass。
后修正 Pass 可以检查块内容以查看将复制到分配的目标地址的确切字节。
异步完成内存。
调用
JITLinkMemoryManager
将工作内存复制到执行器进程并应用请求的权限。
阶段 3。
通知上下文图已发出。
调用
JITLinkContext::notifyFinalized
并移交此图内存分配的JITLinkMemoryManager::FinalizedAlloc
对象。这允许上下文跟踪/保存内存分配并对新发出的定义做出反应。在 ORC 中,这用于更新ExecutionSession
实例的依赖关系图,如果其所有依赖项也已发出,则这可能会导致这些符号(以及可能的其他符号)变为就绪。
Pass¶
JITLink Pass 是 std::function<Error(LinkGraph&)>
实例。它们可以自由地检查和修改给定的 LinkGraph
,但要受到其运行的任何阶段的约束(请参阅 通用链接算法)。如果 Pass 返回 Error::success()
,则链接继续。如果 Pass 返回失败值,则链接停止,并通知 JITLinkContext
链接失败。
Pass 可以被 JITLink 后端(例如,MachO/x86-64 将 GOT 和 PLT 构造实现为一个 Pass)和外部客户端(如 ObjectLinkingLayer::Plugin
)使用。
结合开放的 LinkGraph
API,JITLink Pass 能够实现强大的新功能。例如
松弛优化 - 预修正 Pass 可以检查 GOT 访问和 PLT 调用,并识别条目目标和访问的地址足够接近以直接访问的情况。在这种情况下,Pass 可以重写包含块的指令流并更新修正边以使访问直接。
代码如下所示
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();
}
元数据注册 - 后分配 Pass 可用于记录目标中节的地址范围。这可用于在内存完成后在目标中注册元数据(例如异常处理帧、语言元数据)。
Error registerEHFrameSection(LinkGraph &G) {
if (auto *Sec = G.findSectionByName("__eh_frame")) {
SectionRange SR(*Sec);
registerEHFrameSection(SR.getStart(), SR.getEnd());
}
return Error::success();
}
记录稍后修改的调用站点 - 后分配 Pass 可以记录对特定函数的所有调用的调用站点,允许稍后在运行时更新这些调用站点(例如,用于检测或启用函数的延迟编译,但仍可在编译后直接调用)。
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 的小型代码模型要求模拟动态库的所有代码和数据都分配在 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*
,一个需要为其分配内存的 LinkGraph
的引用,以及一个在构建 InFlightAlloc
之后运行的回调函数。 JITLinkMemoryManager
实现可以(可选地)使用 JD
参数来管理每个模拟动态库的内存池(因为代码模型约束通常是在每个动态库的基础上施加的,而不是跨动态库的)[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 系统的安全性:执行器进程可以被沙盒化,在虚拟机中运行,甚至在完全独立的机器上运行。
JITLink 的内存管理器接口足够灵活,可以实现性能和安全性之间的各种权衡。例如,在必须对代码页面进行签名的系统(防止更新代码)上,内存管理器可以在链接后释放工作内存页面,以释放运行 JITLink 的进程中的内存。或者,在允许 RWX 页面的系统上,内存管理器可以通过将页面标记为 RWX 来将相同的页面用于工作内存和目标内存,从而允许在不增加额外开销的情况下就地修改代码。最后,如果不允许 RWX 页面,但允许物理内存页面的双虚拟映射,则内存管理器可以在 JITLink 进程中双映射物理页面为 RW-,在执行器进程中映射为 R-X,允许从 JITLink 进程修改,但不允许从执行器进程修改(以双映射的额外管理开销为代价)。
错误处理¶
JITLink 广泛使用 llvm::Error
类型(有关详细信息,请参阅 LLVM 程序员手册 中的错误处理部分)。链接过程本身、所有传递、内存管理器接口以及对 JITLinkContext
的操作都可能失败。鼓励链接图构造实用程序(尤其是对象格式的解析器)在应用之前验证输入并验证修复(例如,使用范围检查)。
任何错误都将停止链接过程并通知上下文失败。在 ORC 中,报告的失败将传播到等待失败链接提供的定义的查询,并通过依赖关系图的边传播到等待依赖符号的任何查询。
与 ORC 运行时的连接¶
ORC 运行时(目前正在开发中)旨在为高级 JIT 功能提供运行时支持,包括需要在执行器中执行非平凡操作的对象格式功能(例如,运行初始化程序、管理线程本地存储、向语言运行时注册等)。
ORC 运行时对对象格式功能的支持通常需要运行时(在执行器进程中执行)和 JITLink(在 JIT 进程中运行,并且可以检查 LinkGraphs 以确定必须在执行器中执行哪些操作)之间的协作。例如:ORC 运行时中 MachO 静态初始化程序的执行由 jit_dlopen
函数执行,该函数回叫到 JIT 进程以请求 __mod_init
部分的地址范围列表以进行遍历。此列表由 MachOPlatformPlugin
收集,该插件安装了一个传递,以在每个对象链接到目标时记录此信息。
构建 LinkGraph¶
客户端通常访问和操作由 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 对编译的代码施加了一些限制
它必须使用大型代码模型,并且通常限制可用的重定位模型,以限制必须支持的重定位类型。
它要求所有符号都具有强链接和默认可见性——其他链接/可见性的行为没有明确定义。
它限制和/或禁止使用需要运行时支持的功能,例如静态初始化程序或线程本地存储。
由于这些限制,并非 LLVM 支持的所有语言功能都可以在 MCJIT 下工作,并且要在 JIT 下加载的对象必须编译为目标它(阻止在 JIT 下使用来自其他来源的预编译代码)。
RuntimeDyld 也对链接过程本身提供了非常有限的可见性:客户端可以访问节大小的保守估计(RuntimeDyld 将存根大小和填充估计捆绑到节大小值中)以及最终的重定位字节,但无法访问 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 编译的主函数提供参数。
% 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
将扫描给定的检查文件以查找表单为 # 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
选项允许将一组输入对象指定为测试利用程序,而常规目标文件则隐式地视为要测试的对象。利用程序集中符号的定义会覆盖测试集中的定义,并且来自利用程序的外部引用会导致测试集中局部符号的自动作用域提升(这些对常用链接器规则的修改是通过 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
的行为,我们可以通过编写一个测试利用程序来测试它
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++)的模拟要复杂得多:任何涉及类的代码往往都有很多非平凡的表面积(例如 vtable),需要非常小心地模拟。
JITLink 后端开发人员提示¶
充分利用 assert 和
llvm::Error
。**不要**假设输入对象格式良好:返回 libObject(或您自己的对象解析代码)产生的任何错误,并在构建时进行验证。仔细考虑契约(应使用断言和 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 月)。
支持级别
无:没有后端。JITLink 将返回“架构不受支持”错误。在下表中以空单元格表示。
骨架:存在后端,但不支持常用的重定位。即使是简单的程序也可能会触发“不支持的重定位”错误。处于此状态的后端可能易于通过实现新的重定位来改进。考虑参与进来!
基本:后端支持简单的程序,但尚未准备好用于一般用途。
可用:后端至少对一种代码和重定位模型可用。
良好:后端支持几乎所有重定位。高级功能(如本地线程局部存储)可能尚不可用。
完整:后端支持所有重定位和对象格式功能。
架构 |
ELF |
COFF |
MachO |
---|---|---|---|
arm32 |
骨架 |
||
arm64 |
可用 |
良好 |
|
LoongArch |
良好 |
||
PowerPC 64 |
可用 |
||
RISC-V |
良好 |
||
x86-32 |
基本 |
||
x86-64 |
良好 |
可用 |
良好 |