LLVM Logo

导航

  • 索引
  • 下一个 |
  • 上一个 |
  • LLVM 主页 | 
  • 文档»
  • 用户指南 »
  • JITLink 和 ORC 的 ObjectLinkingLayer

文档

  • 入门/教程
  • 用户指南
  • 参考

参与其中

  • 为 LLVM 做贡献
  • 提交错误报告
  • 邮件列表
  • Discord
  • 聚会和社交活动

附加链接

  • FAQ
  • 术语表
  • 出版物
  • Github 仓库

本页

  • 显示源代码

快速搜索

JITLink 和 ORC 的 ObjectLinkingLayer¶

  • 简介

  • JITLink 和 ObjectLinkingLayer

    • ObjectLinkingLayer 插件

  • LinkGraph

  • 通用链接算法

    • Passes

    • 使用 JITLinkMemoryManager 进行内存管理

    • JITLinkMemoryManager 和安全性

    • 错误处理

  • 与 ORC 运行时的连接

  • 构建 LinkGraphs

  • JIT 链接

    • RuntimeDyld

  • llvm-jitlink 工具

    • 基本用法

    • llvm-jitlink 作为回归测试实用程序

    • 通过 llvm-jitlink-executor 进行远程执行

    • Harness 模式

    • JITLink 后端开发人员的提示

  • 路线图

    • JITLink 可用性和功能状态

简介¶

本文档旨在提供 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 旨在支持以下功能,其中一些仍在开发中

  1. 将单个可重定位对象跨进程和跨架构链接到目标执行器进程。

  2. 支持所有对象格式功能。

  3. 开放的链接器数据结构 (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 可以使用它来确定谁应该能够看到该符号。具有默认作用域的符号应该是全局可见的。具有隐藏作用域的符号应该对同一模拟动态库(例如,ORC JITDylib)或可执行文件中的其他定义可见,但不能从其他地方可见。具有本地作用域的符号应该仅在当前的 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. 阶段 1

    一旦初始配置(包括 pass 管道设置)完成,link 函数立即调用此阶段。

    1. 运行预剪枝 passes。

      这些 passes 在图被剪枝之前在图上调用。在这个阶段,LinkGraph 节点仍然具有其原始 vmaddr。一个标记-live pass(由 JITLinkContext 提供)将在此序列结束时运行,以标记初始的 live 符号集。

      值得注意的用例:标记节点为 live,访问/复制将被剪枝的图数据(例如,对 JIT 很重要但不链接过程不需要的元数据)。

    2. 剪枝(死代码消除)LinkGraph。

      删除从初始 live 符号集无法访问的所有符号和块。

      这允许 JITLink 删除无法访问的符号/内容,包括被覆盖的弱定义和冗余 ODR 定义。

    3. 运行后剪枝 passes。

      这些 passes 在死代码消除后但在分配内存或节点分配其最终目标 vmaddr 之前在图上运行。

      在此阶段运行的 passes 受益于剪枝,因为死函数和数据已从图中删除。但是,仍然可以将新内容添加到图中,因为尚未分配目标内存和工作内存。

      值得注意的用例:构建全局偏移表 (GOT)、过程链接表 (PLT) 和线程局部变量 (TLV) 条目。

    4. 异步分配内存。

      调用 JITLinkContext 的 JITLinkMemoryManager 为图分配工作内存和目标内存。作为此过程的一部分,JITLinkMemoryManager 将图中定义的所有节点的地址更新为其分配的目标地址。

      注意:此步骤仅更新在此图中定义的节点的地址。外部符号仍将具有空地址。

  2. 阶段 2

    1. 运行后分配 passes。

      这些 passes 在分配工作内存和目标内存之后但在将图中符号的最终地址通知 JITLinkContext 之前在图上运行。这使这些 passes 有机会在任何 JITLink 客户端(尤其是 ORC 查询符号解析)尝试访问它们之前设置与目标地址关联的数据结构。

      值得注意的用例:设置目标地址和 JIT 数据结构之间的映射,例如 __dso_handle 和 JITDylib* 之间的映射。

    2. 将分配的符号地址通知 JITLinkContext。

      在链接图上调用 JITLinkContext::notifyResolved,允许客户端对为此图进行的符号地址分配做出反应。在 ORC 中,这用于通知对已解析符号的任何挂起查询,包括来自并发运行的 JITLink 实例的挂起查询,这些实例已达到下一步,并且正在等待此图中符号的地址以继续其链接。

    3. 识别外部符号并异步解析其地址。

      调用 JITLinkContext 以解析图中任何外部符号的目标地址。

  3. 阶段 3

    1. 应用外部符号解析结果。

      这会更新所有外部符号的地址。此时,图中的所有节点都具有其最终目标地址,但是节点内容仍然指向对象文件中的原始数据。

    2. 运行预修复 passes。

      这些 passes 在分配所有节点的最终目标地址之后但在将节点内容复制到工作内存并进行修复之前在图上调用。在此阶段运行的 passes 可以根据地址布局对图和内容进行后期优化。

      值得注意的用例:GOT 和 PLT 放松,其中对于在分配的内存布局下可直接访问的修复目标,绕过 GOT 和 PLT 访问。

    3. 将块内容复制到工作内存并应用修复。

      将所有块内容复制到分配的工作内存中(遵循目标布局)并应用修复。图块更新为指向已修复的内容。

    4. 运行后修复 passes。

      这些 passes 在应用修复并且块更新为指向已修复内容之后在图上调用。

      后修复 passes 可以检查块内容,以查看将复制到分配的目标地址的确切字节。

    5. 异步完成内存。

      调用 JITLinkMemoryManager 以将工作内存复制到执行器进程并应用请求的权限。

  4. 阶段 3。

    1. 通知上下文图已发出。

      调用 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 实例,但它们可以手动创建

  1. 通过直接构造和填充 LinkGraph 实例。

  2. 通过使用 createLinkGraph 函数族从包含目标文件的内存缓冲区创建 LinkGraph。 这就是 ObjectLinkingLayer 通常创建 LinkGraphs 的方式。

  1. 当对象格式和架构都提前已知时,可以使用 createLinkGraph_<Object-Format>_<Architecture>。

  2. 当对象格式提前已知,但架构未知时,可以使用 createLinkGraph_<Object-Format>。 在这种情况下,架构将通过检查对象标头来确定。

  3. 当对象格式和架构都提前未知时,可以使用 createLinkGraph。 在这种情况下,将检查对象标头以确定格式和架构。

JIT 链接¶

JIT 链接器概念是在 LLVM 早期一代的 JIT API MCJIT 中引入的。 在 MCJIT 中,RuntimeDyld 组件通过在通常的编译器管道末尾添加内存中链接步骤,实现了 LLVM 作为内存中编译器的重用。 MCJIT 没有像编译器通常那样将可重定位对象转储到磁盘,而是将它们传递给 RuntimeDyld 以链接到目标进程中。

这种链接方法与标准的静态或动态链接不同

静态链接器将一个或多个可重定位对象文件作为输入,并将它们链接到磁盘上的可执行文件或动态库中。

动态链接器将重定位应用于已加载到内存中的可执行文件和动态库。

JIT 链接器一次接受一个可重定位对象文件,并将其链接到目标进程中,通常使用上下文对象来允许链接的代码解析目标中的符号。

RuntimeDyld¶

为了保持 RuntimeDyld 实现的简单性,MCJIT 对编译代码施加了一些限制

  1. 它必须使用 Large 代码模型,并且经常限制可用的重定位模型,以限制必须支持的重定位类型。

  2. 它要求所有符号都具有强链接和默认可见性 – 其他链接/可见性的行为没有明确定义。

  3. 它约束和/或禁止使用需要运行时支持的功能,例如静态初始化程序或线程本地存储。

由于这些限制,并非所有 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 后端开发人员的提示¶

  1. 大量使用 assert 和 llvm::Error。 不要假设输入对象格式良好: 返回 libObject(或您自己的对象解析代码)产生的任何错误,并在构造时进行验证。 仔细考虑 contract(应该使用 asserts 和 llvm_unreachable 验证)和环境错误(应该生成 llvm::Error 实例)之间的区别。

  2. 不要假设您正在进程内链接。 在 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(完整): 后端支持所有重定位和对象格式功能。

表 117 可用性和状态¶

架构

ELF

COFF

MachO

arm32

骨架

arm64

可用

良好

LoongArch

良好

PowerPC 64

可用

RISC-V

良好

x86-32

基本

x86-64

良好

可用

良好

[1]

有关完整的实际操作示例,请参阅 llvm/examples/OrcV2Examples/LLJITWithObjectLinkingLayerPlugin。

[2]

如果不是因为隐藏作用域符号,我们可以消除 JITLinkDylib* 参数到 JITLinkMemoryManager::allocate,并将每个对象视为单独的模拟 dylib 以进行内存布局。 隐藏符号通过生成对外部符号的范围内访问来破坏这一点,要求访问和符号分配在彼此的范围内。 也就是说,为每个模拟 dylib 提供预留的地址范围池可以保证 relaxation optimizations(松弛优化)将对所有 dylib 内引用生效,这对性能有利(以预先保留地址范围所引入的任何开销为代价)。

导航

  • 索引
  • 下一个 |
  • 上一个 |
  • LLVM 主页 | 
  • 文档»
  • 用户指南 »
  • JITLink 和 ORC 的 ObjectLinkingLayer
© Copyright 2003-2025, LLVM Project。 上次更新于 2025-03-10。 使用 Sphinx 7.2.6 创建。