LLVM Logo

ナビゲーション

  • インデックス
  • 次へ |
  • 前へ |
  • LLVM ホーム | 
  • ドキュメント»
  • ユーザーガイド »
  • JITLinkとORCのObjectLinkingLayer

ドキュメント

  • 入門/チュートリアル
  • ユーザーガイド
  • リファレンス

参加する

  • LLVMへの貢献
  • バグレポートの提出
  • メーリングリスト
  • IRC
  • ミートアップとソーシャルイベント

追加リンク

  • FAQ
  • 用語集
  • 出版物
  • Githubリポジトリ

このページ

  • ソースを表示

クイック検索

JITLinkとORCのObjectLinkingLayer¶

  • はじめに

  • JITLinkとObjectLinkingLayer

    • ObjectLinkingLayerプラグイン

  • LinkGraph

  • 汎用リンクアルゴリズム

    • パス

    • JITLinkMemoryManagerによるメモリ管理

    • JITLinkMemoryManagerとセキュリティ

    • エラー処理

  • ORCランタイムとの接続

  • LinkGraphの構築

  • JITリンク

    • RuntimeDyld

  • llvm-jitlinkツール

    • 基本的な使い方

    • 回帰テストユーティリティとしてのllvm-jitlink

    • llvm-jitlink-executorによるリモート実行

    • ハーネスモード

    • JITLinkバックエンド開発者へのヒント

  • ロードマップ

    • JITLinkの可用性と機能ステータス

はじめに¶

このドキュメントは、JITLinkライブラリの設計とAPIの概要を説明することを目的としています。リンキングと再配置可能なオブジェクトファイルに関するある程度の知識を前提としていますが、深い専門知識は必要ありません。セクション、シンボル、再配置が何かを知っていれば、このドキュメントは理解できるはずです。もしそうでない場合は、パッチを送信(LLVMへの貢献)するか、バグを報告してください(LLVMバグレポートの提出方法)。

JITLinkは、JITリンキングのためのライブラリです。ORC JIT APIをサポートするために構築され、ORCのObjectLinkingLayer APIを介して最も一般的にアクセスされます。JITLinkは、静的イニシャライザ、例外処理、スレッドローカル変数、言語ランタイム登録など、各オブジェクトフォーマットで提供されるすべての機能をサポートすることを目指して開発されました。これらの機能をサポートすることで、ORCはこれらの機能に依存するソース言語(例: C++は、静的コンストラクタをサポートするための静的イニシャライザ、例外のためのeh-frame登録、およびスレッドローカルのためのTLVサポートをオブジェクトフォーマットのサポートとして必要とします。SwiftとObjective-Cは多くの機能のために言語ランタイム登録を必要とします)から生成されたコードを実行できます。一部のオブジェクトフォーマット機能のサポートはJITLink内で完全に提供され、他の機能のサポートは(プロトタイプの)ORCランタイムと連携して提供されます。

JITLinkは、以下の機能をサポートすることを目指しており、その一部はまだ開発中です。

  1. 単一の再配置可能なオブジェクトの、ターゲットexecutorプロセスへのプロセス間およびアーキテクチャ間リンキング。

  2. すべてのオブジェクトフォーマット機能のサポート。

  3. オープンなリンカーデータ構造(LinkGraph)とパスシステム。

JITLinkとObjectLinkingLayer¶

ObjectLinkingLayerは、JITLinkのORCラッパーです。これは、オブジェクトをJITDylibに追加したり、上位のプログラム表現から出力したりできるORCレイヤーです。オブジェクトが出力されると、ObjectLinkingLayerはJITLinkを使用してLinkGraphを構築し(「LinkGraphの構築」を参照)、JITLinkのlink関数を呼び出してグラフをexecutorプロセスにリンクします。

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は、リンクが完了し、コードがexecutorプロセスに出力された後に呼び出されます。必要に応じて、MaterializationResponsibilityの状態を最終化するためにオーバーライドできます。

    Error notifyEmitted(MaterializationResponsibility &MR)
    
  • notifyFailedは、リンクが失敗した場合にいつでも呼び出されます。これをオーバーライドして、失敗に対応できます(例: すでに割り当てられたリソースを解放するなど)。

    Error notifyFailed(MaterializationResponsibility &MR)
    
  • notifyRemovingResourcesは、MaterializationResponsibilityのResourceKeyKに関連付けられたリソースを削除するリクエストが行われたときに呼び出されます。

    Error notifyRemovingResources(ResourceKey K)
    
  • notifyTransferringResourcesは、ResourceKeySrcKeyに関連付けられたリソースの追跡をDstKeyに転送するリクエストが行われた場合/時に呼び出されます。

    void notifyTransferringResources(ResourceKey DstKey,
                                     ResourceKey SrcKey)
    

プラグインの作成者は、リソースの削除または転送、またはリンクの失敗の場合にリソースを安全に管理するために、notifyFailed、notifyRemovingResources、およびnotifyTransferringResourcesメソッドを実装する必要があります。プラグインによって管理されるリソースがない場合、これらのメソッドはError::success()を返すno-opとして実装できます。

プラグインインスタンスは、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 – executorプロセスの仮想アドレス空間でアドレスを割り当てることができるリンクグラフ内のノード。

    絶対シンボルと外部シンボルは、プレーンな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 メソッドを介してアクセスできます。これは、ブロックの開始に必要なアラインメントからのオフセットを表します。これは、ブロックの最小アラインメント要件が、ブロック内の非ゼロオフセットにあるデータに由来する場合をサポートするために必要です。たとえば、ブロックが1バイト(バイトアラインメント)とそれに続く 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 は、これを使用して、誰がシンボルを認識できるかを判断できます。デフォルトスコープのシンボルはグローバルに表示される必要があります。隠しスコープのシンボルは、同じシミュレートされた dylib (たとえば、ORC JITDylib) または実行可能ファイル内の他の定義には表示される必要がありますが、他の場所からは表示されません。ローカルスコープのシンボルは、現在の 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 ノードのセットの2つのセットを持つ二部グラフです。各 Symbol ノードには、ターゲットの Addressable への1つの(暗黙の)エッジがあります。各 Block には、Symbol セットの要素に戻るエッジのセット(空の可能性があり、Edge インスタンスとして表現)があります。一般的なアルゴリズムの利便性とパフォーマンスのために、シンボルとブロックはさらに 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 は、指定されたブロックを指定されたインデックスで 2 つに分割します(たとえば、eh-frame セクションの CFI レコードなど、ブロックに分解可能なレコードが含まれていることがわかっている場合に便利です)。

  • グラフユーティリティ操作

    • getName は、このグラフの名前を返します。これは通常、入力オブジェクトファイルの名前に基づいています。

    • getTargetTriple は、実行プロセスの llvm::Triple を返します。

    • getPointerSize は、実行プロセスにおけるポインタのサイズ(バイト単位)を返します。

    • getEndinaness は、実行プロセスのエンディアンを返します。

    • allocateString は、指定された llvm::Twine からのデータをリンクグラフの内部アロケータにコピーします。これは、パス内で作成されたコンテンツが、そのパスの実行後も存続することを保証するために使用できます。

汎用リンクアルゴリズム¶

JITLink は、JITLink パスを導入することで、特定時点で拡張/変更できる汎用リンクアルゴリズムを提供します。

各フェーズの最後に、リンカーはその状態を継続にパッケージ化し、JITLinkContext オブジェクトを呼び出して、(潜在的に高レイテンシーな)非同期操作を実行します:メモリの割り当て、外部シンボルの解決、そして最後に、リンクされたメモリを実行中のプロセスに転送します。

  1. フェーズ 1

    このフェーズは、初期構成(パスパイプラインの設定を含む)が完了するとすぐに、link 関数によって直ちに呼び出されます。

    1. プルーニング前パスを実行します。

      これらのパスは、グラフがプルーニングされる前にグラフ上で呼び出されます。この段階では、LinkGraph ノードはまだ元の vmaddr を持っています。マークライブパス(JITLinkContext によって提供される)は、このシーケンスの最後に実行され、ライブシンボルの初期セットをマークします。

      注目すべきユースケース:ノードのライブのマーク、プルーニングされるグラフデータ(JIT にとって重要だが、リンクプロセスには不要なメタデータなど)へのアクセス/コピー。

    2. LinkGraph をプルーニング(デッドストリップ)します。

      ライブシンボルの初期セットから到達できないすべてのシンボルとブロックを削除します。

      これにより、JITLink は、オーバーライドされた弱定義や冗長な ODR 定義など、到達できないシンボル/コンテンツを削除できます。

    3. プルーニング後パスを実行します。

      これらのパスは、デッドストリッピング後、ただしメモリが割り当てられたり、ノードに最終的なターゲット vmaddr が割り当てられる前に、グラフ上で実行されます。

      この段階で実行されるパスは、デッド関数とデータがグラフからストリップされているため、プルーニングの恩恵を受けます。ただし、ターゲットメモリとワーキングメモリがまだ割り当てられていないため、新しいコンテンツをグラフに追加することもできます。

      注目すべきユースケース:グローバルオフセットテーブル(GOT)、プロシージャリンケージテーブル(PLT)、およびスレッドローカル変数(TLV)エントリの構築。

    4. 非同期的にメモリを割り当てます。

      JITLinkContext の JITLinkMemoryManager を呼び出して、グラフのワーキングメモリとターゲットメモリの両方を割り当てます。このプロセスの一環として、JITLinkMemoryManager は、グラフで定義されているすべてのノードのアドレスを、割り当てられたターゲットアドレスに更新します。

      注:このステップでは、このグラフで定義されているノードのアドレスのみが更新されます。外部シンボルは、引き続きヌルアドレスを持ちます。

  2. フェーズ 2

    1. 割り当て後パスを実行します。

      これらのパスは、ワーキングメモリとターゲットメモリが割り当てられた後、ただし JITLinkContext にグラフ内のシンボルの最終アドレスが通知される前に、グラフ上で実行されます。これにより、これらのパスは、JITLink クライアント(特にシンボル解決のための ORC クエリ)がアクセスを試みる前に、ターゲットアドレスに関連付けられたデータ構造を設定する機会が得られます。

      注目すべきユースケース:ターゲットアドレスと JIT データ構造間のマッピング(__dso_handle と JITDylib* 間のマッピングなど)の設定。

    2. 割り当てられたシンボルアドレスを JITLinkContext に通知します。

      リンクグラフで JITLinkContext::notifyResolved を呼び出して、クライアントがこのグラフに対して行われたシンボルアドレスの割り当てに対応できるようにします。ORC では、これは、このグラフ内のシンボルのアドレスを待ってリンクを進める、並行して実行されている JITLink インスタンスからの保留中のクエリを含む、解決済みシンボルに対する保留中のクエリを通知するために使用されます。

    3. 外部シンボルを識別し、それらのアドレスを非同期的に解決します。

      グラフ内の外部シンボルのターゲットアドレスを解決するために、JITLinkContext を呼び出します。

  3. フェーズ 3

    1. 外部シンボル解決の結果を適用します。

      これにより、すべての外部シンボルのアドレスが更新されます。この時点で、グラフ内のすべてのノードには最終的なターゲットアドレスがありますが、ノードの内容は、オブジェクトファイル内の元のデータを指しています。

    2. フィックスアップ前パスを実行します。

      これらのパスは、すべてのノードに最終的なターゲットアドレスが割り当てられた後、ただしノードの内容がワーキングメモリにコピーされて修正される前に、グラフ上で呼び出されます。この段階で実行されるパスは、アドレスレイアウトに基づいて、グラフとコンテンツに最新の最適化を行うことができます。

      注目すべきユースケース:GOT と PLT の緩和。割り当てられたメモリレイアウトで直接アクセスできるフィックスアップターゲットの場合、GOT と PLT へのアクセスがバイパスされます。

    3. ブロックの内容をワーキングメモリにコピーし、フィックスアップを適用します。

      すべてのブロックコンテンツを(ターゲットレイアウトに従って)割り当てられたワーキングメモリにコピーし、フィックスアップを適用します。グラフブロックは、修正されたコンテンツを指すように更新されます。

    4. フィックスアップ後パスを実行します。

      これらのパスは、フィックスアップが適用され、ブロックが修正されたコンテンツを指すように更新された後、グラフ上で呼び出されます。

      フィックスアップ後パスは、ブロックの内容を調べて、割り当てられたターゲットアドレスにコピーされる正確なバイトを確認できます。

    5. 非同期的にメモリをファイナライズします。

      JITLinkMemoryManager を呼び出して、ワーキングメモリを実行プロセスにコピーし、要求されたアクセス許可を適用します。

  4. フェーズ 3。

    1. グラフが出力されたことをコンテキストに通知します。

      JITLinkContext::notifyFinalized を呼び出し、このグラフのメモリ割り当てのために JITLinkMemoryManager::FinalizedAlloc オブジェクトを引き渡します。これにより、コンテキストはメモリ割り当てを追跡/保持し、新しく出力された定義に対応できます。ORC では、これは ExecutionSession インスタンスの依存関係グラフを更新するために使用されます。これにより、すべての依存関係も出力されている場合、これらのシンボル(およびおそらく他のシンボル)が準備完了になる可能性があります。

パス¶

JITLink パスは、std::function<Error(LinkGraph&)> インスタンスです。これらは、実行中のフェーズの制約に従って、指定された LinkGraph を自由に検査および変更できます(汎用リンクアルゴリズムを参照)。パスが Error::success() を返すと、リンクが続行されます。パスが失敗値を返すと、リンクが停止され、JITLinkContext にリンクが失敗したことが通知されます。

パスは、JITLink バックエンド(たとえば、MachO/x86-64 は GOT と PLT の構築をパスとして実装します)と、ObjectLinkingLayer::Plugin などの外部クライアントの両方で使用できます。

オープンな LinkGraph API と組み合わせて、JITLink パスは、強力な新機能の実装を可能にします。例えば

  • 緩和の最適化 – フィックスアップ前パスは、GOT へのアクセスと PLT の呼び出しを調べて、エントリターゲットのアドレスとアクセスが、直接アクセスできるほど近い状況を特定できます。この場合、パスは、含まれているブロックの命令ストリームを書き換え、アクセスを直接にするようにフィックスアップエッジを更新できます。

    このコードは次のようになります

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();
}
  • メタデータ登録 – 割り当て後パスを使用して、ターゲット内のセクションのアドレス範囲を記録できます。これは、メモリがファイナライズされたら、ターゲットにメタデータ(例外処理フレーム、言語メタデータなど)を登録するために使用できます。

Error registerEHFrameSection(LinkGraph &G) {
  if (auto *Sec = G.findSectionByName("__eh_frame")) {
    SectionRange SR(*Sec);
    registerEHFrameSection(SR.getStart(), SR.getEnd());
  }

  return Error::success();
}
  • 後で変更するためのコールサイトの記録 – 割り当て後パスは、特定の関数へのすべての呼び出しのコールサイトを記録して、それらのコールサイトを後で実行時に更新できるようにします(たとえば、インストルメンテーションの場合、または関数を遅延コンパイルできるようにしますが、コンパイル後も直接呼び出すことができます)。

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 リンクには、2 種類のメモリの割り当てが必要です:JIT プロセス内のワーキングメモリと、実行プロセス内のターゲットメモリ(これらのプロセスとメモリ割り当ては、ユーザーが JIT をどのように構築したいかに応じて、同じである場合もあります)。また、これらの割り当ては、ターゲットプロセスで要求されたコードモデルに準拠する必要があります(たとえば、MachO/x86-64 のスモールコードモデルでは、シミュレートされた dylib のすべてのコードとデータが 4 GB 内に割り当てられる必要があります)。最後に、メモリマネージャーは、メモリをターゲットアドレス空間に転送し、メモリ保護を適用する責任を負うことが自然です。メモリマネージャーは、エグゼキュータとの通信方法を知っている必要があり、共有と保護の割り当ては、ホストオペレーティングシステムの仮想メモリ管理 API を介して(セキュリティのために同じマシン上のプロセス間で実行するという一般的なケースで)効率的に管理できることが多いためです。

これらの要件を満たすために、JITLinkMemoryManager は次の設計を採用しています。メモリマネージャー自体には、非同期操作のための 2 つの仮想メソッドのみがあります(それぞれ、同期的に呼び出すための便利なオーバーロードがあります)。

/// 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;

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;

  1. すべてのシンボルに対して強力なリンケージとデフォルトの可視性が必要であり、他のリンケージ/可視性の動作は明確に定義されていませんでした。

  2. 静的初期化子やスレッドローカルストレージなど、ランタイムサポートを必要とする機能の使用を制約または禁止していました。

これらの制限の結果、LLVMでサポートされているすべての言語機能がMCJITで動作するわけではなく、JITでロードされるオブジェクトはそれをターゲットとするようにコンパイルする必要がありました(JITで他のソースからのプリコンパイル済みコードを使用することはできませんでした)。

RuntimeDyldは、リンキングプロセス自体への可視性も非常に限られていました。クライアントはセクションサイズの控えめな推定値(RuntimeDyldはスタブサイズとパディングの推定値をセクションサイズの値にバンドルしていました)と最終的に再配置されたバイトにアクセスできましたが、RuntimeDyldの内部オブジェクト表現にはアクセスできませんでした。

これらの制限と限界を排除することが、JITLink開発の主な動機の1つでした。

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 <エントリ ポイント 名>オプションを使用して指定できます。

その他のオプションは、llvm-jitlink -helpを呼び出すことで確認できます。

回帰テストユーティリティとしてのllvm-jitlink¶

llvm-jitlinkの主な目的の1つは、JITLinkの読みやすい回帰テストを可能にすることでした。これを行うために、2つのオプションをサポートしています。

-noexecオプションは、エントリポイントを検索した後、実行を試みる前に停止するようにllvm-jitlinkに指示します。リンクされたコードは実行されないため、リンク先のターゲットにアクセスできない場合でも、他のターゲット用にリンクするために使用できます(この場合、-define-absまたは-phony-externalsオプションを使用して、不足している定義を指定できます)。

-check <チェックファイル>オプションは、ワーキングメモリに対して一連のjitlink-check式を実行するために使用できます。これは通常、-noexecと組み合わせて使用されます。目的は、コードを実行するのではなく、JIT化されたメモリを検証することであるため、-noexecを使用すると、現在のプロセスからサポートされている任意のターゲットアーキテクチャ用にリンクできます。-checkモードでは、llvm-jitlinkは、# jitlink-check: <expr>形式の行について、指定されたチェックファイルをスキャンします。この使用例は、llvm/test/ExecutionEngine/JITLinkにあります。

llvm-jitlink-executorによるリモート実行¶

デフォルトでは、llvm-jitlinkは指定されたオブジェクトを独自のプロセスにリンクしますが、これは2つのオプションで上書きできます。

-oop-executor[=/path/to/executor]オプションは、指定されたexecutor(デフォルトはllvm-jitlink-executor)を実行し、filedescs=<in-fd>,<out-fd>形式の最初の引数としてexecutorに渡すファイル記述子を介してexecutorと通信するようにllvm-jitlinkに指示します。

-oop-executor-connect=<host>:<port>オプションは、指定されたホストとポートでTCPを介して既に実行中のexecutorに接続するようにllvm-jitlinkに指示します。このオプションを使用するには、最初の引数としてlisten=<host>:<port>を指定してllvm-jitlink-executorを手動で起動する必要があります。

ハーネスモード¶

-harnessオプションを使用すると、一連の入力オブジェクトをテストハーネスとして指定でき、通常のオブジェクトファイルは暗黙的にテスト対象のオブジェクトとして扱われます。ハーネスセット内のシンボルの定義は、テストセット内の定義を上書きし、ハーネスからの外部参照は、テストセット内のローカルシンボルの自動スコープ昇格を引き起こします(通常のリンカー規則に対するこれらの変更は、llvm-jitlinkが-harnessオプションを認識したときにインストールするObjectLinkingLayer::Pluginを介して行われます)。

これらの変更により、関数をモックする対象の関数をモックすることで、オブジェクトファイル内の関数を選択的にテストできます。たとえば、次のCソースからコンパイルされたオブジェクトファイルtest_code.oがあると仮定します(アクセスする必要はありません)。

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の外部で解決できません。2つの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++など)のモックははるかに複雑です。クラスを含むコードは、モックに細心の注意を払う必要がある多くの非自明な表面領域(vtablesなど)を持つ傾向があります。

JITLinkバックエンド開発者向けのヒント¶

  1. assertとllvm::Errorを自由に使用してください。入力オブジェクトが適切に形成されていると想定しないでください。libObject(または独自のオブジェクト解析コード)によって生成されたエラーを返し、構築時に検証します。コントラクト(assertと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月現在)。

サポートレベル

  • なし:バックエンドはありません。JITLinkは「アーキテクチャはサポートされていません」というエラーを返します。以下の表では空のセルで表されます。

  • スケルトン:バックエンドは存在しますが、一般的に使用される再配置をサポートしていません。単純なプログラムでも「サポートされていない再配置」エラーが発生する可能性があります。この状態のバックエンドは、新しい再配置を実装することで簡単に改善できる場合があります。ぜひ参加をご検討ください!

  • 基本:バックエンドは単純なプログラムをサポートしていますが、まだ一般使用の準備ができていません。

  • 使用可能:バックエンドは、少なくとも1つのコードおよび再配置モデルで一般使用できます。

  • 良好:バックエンドはほぼすべての再配置をサポートしています。ネイティブスレッドローカルストレージなどの高度な機能はまだ利用できない場合があります。

  • 完了:バックエンドは、すべての再配置とオブジェクトフォーマット機能をサポートしています。

表 109 可用性とステータス¶

アーキテクチャ

ELF

COFF

MachO

arm32

スケルトン

arm64

使用可能

良好

LoongArch

良好

PowerPC 64

使用可能

RISC-V

良好

x86-32

基本

x86-64

良好

使用可能

良好

[1]

完全な動作例については、llvm/examples/OrcV2Examples/LLJITWithObjectLinkingLayerPluginを参照してください。

[2]

隠されたスコープ付きシンボルがなければ、JITLinkMemoryManager::allocateへのJITLinkDylib*引数をなくし、すべてのオブジェクトをメモリレイアウトの目的で別々のシミュレートされたダイナミックライブラリとして扱うことができたでしょう。隠されたシンボルは、外部シンボルへの範囲内アクセスを生成することでこれを壊し、アクセスとシンボルを互いの範囲内に割り当てる必要があります。とはいえ、シミュレートされた各ダイナミックライブラリに事前に予約されたアドレス範囲プールを提供することで、すべてのダイナミックライブラリ内参照に対して緩和最適化が確実に機能し、パフォーマンスにとって有利になります(事前にアドレス範囲を予約することで発生するオーバーヘッドは別として)。

ナビゲーション

  • インデックス
  • 次へ |
  • 前へ |
  • LLVM ホーム | 
  • ドキュメント»
  • ユーザガイド »
  • JITLinkとORCのObjectLinkingLayer
© Copyright 2003-2024, LLVM Project. 最終更新日: 2024-08-31. Sphinx 7.1.2を使用して作成されました。