JITLinkとORCのObjectLinkingLayer¶
はじめに¶
このドキュメントは、JITLinkライブラリの設計とAPIの概要を説明することを目的としています。リンキングと再配置可能なオブジェクトファイルに関するある程度の知識を前提としていますが、深い専門知識は必要ありません。セクション、シンボル、再配置が何かを知っていれば、このドキュメントは理解できるはずです。もしそうでない場合は、パッチを送信(LLVMへの貢献)するか、バグを報告してください(LLVMバグレポートの提出方法)。
JITLinkは、JITリンキングのためのライブラリです。ORC JIT APIをサポートするために構築され、ORCのObjectLinkingLayer APIを介して最も一般的にアクセスされます。JITLinkは、静的イニシャライザ、例外処理、スレッドローカル変数、言語ランタイム登録など、各オブジェクトフォーマットで提供されるすべての機能をサポートすることを目指して開発されました。これらの機能をサポートすることで、ORCはこれらの機能に依存するソース言語(例: C++は、静的コンストラクタをサポートするための静的イニシャライザ、例外のためのeh-frame登録、およびスレッドローカルのためのTLVサポートをオブジェクトフォーマットのサポートとして必要とします。SwiftとObjective-Cは多くの機能のために言語ランタイム登録を必要とします)から生成されたコードを実行できます。一部のオブジェクトフォーマット機能のサポートはJITLink内で完全に提供され、他の機能のサポートは(プロトタイプの)ORCランタイムと連携して提供されます。
JITLinkは、以下の機能をサポートすることを目指しており、その一部はまだ開発中です。
単一の再配置可能なオブジェクトの、ターゲットexecutorプロセスへのプロセス間およびアーキテクチャ間リンキング。
すべてのオブジェクトフォーマット機能のサポート。
オープンなリンカーデータ構造(
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
のResourceKey
Kに関連付けられたリソースを削除するリクエストが行われたときに呼び出されます。Error notifyRemovingResources(ResourceKey K)
notifyTransferringResources
は、ResourceKey
SrcKeyに関連付けられたリソースの追跡を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 (たとえば、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
ノードのセットの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
このフェーズは、初期構成(パスパイプラインの設定を含む)が完了するとすぐに、
link
関数によって直ちに呼び出されます。プルーニング前パスを実行します。
これらのパスは、グラフがプルーニングされる前にグラフ上で呼び出されます。この段階では、
LinkGraph
ノードはまだ元の vmaddr を持っています。マークライブパス(JITLinkContext
によって提供される)は、このシーケンスの最後に実行され、ライブシンボルの初期セットをマークします。注目すべきユースケース:ノードのライブのマーク、プルーニングされるグラフデータ(JIT にとって重要だが、リンクプロセスには不要なメタデータなど)へのアクセス/コピー。
LinkGraph
をプルーニング(デッドストリップ)します。ライブシンボルの初期セットから到達できないすべてのシンボルとブロックを削除します。
これにより、JITLink は、オーバーライドされた弱定義や冗長な ODR 定義など、到達できないシンボル/コンテンツを削除できます。
プルーニング後パスを実行します。
これらのパスは、デッドストリッピング後、ただしメモリが割り当てられたり、ノードに最終的なターゲット vmaddr が割り当てられる前に、グラフ上で実行されます。
この段階で実行されるパスは、デッド関数とデータがグラフからストリップされているため、プルーニングの恩恵を受けます。ただし、ターゲットメモリとワーキングメモリがまだ割り当てられていないため、新しいコンテンツをグラフに追加することもできます。
注目すべきユースケース:グローバルオフセットテーブル(GOT)、プロシージャリンケージテーブル(PLT)、およびスレッドローカル変数(TLV)エントリの構築。
非同期的にメモリを割り当てます。
JITLinkContext
のJITLinkMemoryManager
を呼び出して、グラフのワーキングメモリとターゲットメモリの両方を割り当てます。このプロセスの一環として、JITLinkMemoryManager
は、グラフで定義されているすべてのノードのアドレスを、割り当てられたターゲットアドレスに更新します。注:このステップでは、このグラフで定義されているノードのアドレスのみが更新されます。外部シンボルは、引き続きヌルアドレスを持ちます。
フェーズ 2
割り当て後パスを実行します。
これらのパスは、ワーキングメモリとターゲットメモリが割り当てられた後、ただし
JITLinkContext
にグラフ内のシンボルの最終アドレスが通知される前に、グラフ上で実行されます。これにより、これらのパスは、JITLink クライアント(特にシンボル解決のための ORC クエリ)がアクセスを試みる前に、ターゲットアドレスに関連付けられたデータ構造を設定する機会が得られます。注目すべきユースケース:ターゲットアドレスと JIT データ構造間のマッピング(
__dso_handle
とJITDylib*
間のマッピングなど)の設定。割り当てられたシンボルアドレスを
JITLinkContext
に通知します。リンクグラフで
JITLinkContext::notifyResolved
を呼び出して、クライアントがこのグラフに対して行われたシンボルアドレスの割り当てに対応できるようにします。ORC では、これは、このグラフ内のシンボルのアドレスを待ってリンクを進める、並行して実行されている JITLink インスタンスからの保留中のクエリを含む、解決済みシンボルに対する保留中のクエリを通知するために使用されます。外部シンボルを識別し、それらのアドレスを非同期的に解決します。
グラフ内の外部シンボルのターゲットアドレスを解決するために、
JITLinkContext
を呼び出します。
フェーズ 3
外部シンボル解決の結果を適用します。
これにより、すべての外部シンボルのアドレスが更新されます。この時点で、グラフ内のすべてのノードには最終的なターゲットアドレスがありますが、ノードの内容は、オブジェクトファイル内の元のデータを指しています。
フィックスアップ前パスを実行します。
これらのパスは、すべてのノードに最終的なターゲットアドレスが割り当てられた後、ただしノードの内容がワーキングメモリにコピーされて修正される前に、グラフ上で呼び出されます。この段階で実行されるパスは、アドレスレイアウトに基づいて、グラフとコンテンツに最新の最適化を行うことができます。
注目すべきユースケース:GOT と PLT の緩和。割り当てられたメモリレイアウトで直接アクセスできるフィックスアップターゲットの場合、GOT と PLT へのアクセスがバイパスされます。
ブロックの内容をワーキングメモリにコピーし、フィックスアップを適用します。
すべてのブロックコンテンツを(ターゲットレイアウトに従って)割り当てられたワーキングメモリにコピーし、フィックスアップを適用します。グラフブロックは、修正されたコンテンツを指すように更新されます。
フィックスアップ後パスを実行します。
これらのパスは、フィックスアップが適用され、ブロックが修正されたコンテンツを指すように更新された後、グラフ上で呼び出されます。
フィックスアップ後パスは、ブロックの内容を調べて、割り当てられたターゲットアドレスにコピーされる正確なバイトを確認できます。
非同期的にメモリをファイナライズします。
JITLinkMemoryManager
を呼び出して、ワーキングメモリを実行プロセスにコピーし、要求されたアクセス許可を適用します。
フェーズ 3。
グラフが出力されたことをコンテキストに通知します。
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;
すべてのシンボルに対して強力なリンケージとデフォルトの可視性が必要であり、他のリンケージ/可視性の動作は明確に定義されていませんでした。
静的初期化子やスレッドローカルストレージなど、ランタイムサポートを必要とする機能の使用を制約または禁止していました。
これらの制限の結果、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バックエンド開発者向けのヒント¶
assertと
llvm::Error
を自由に使用してください。入力オブジェクトが適切に形成されていると想定しないでください。libObject(または独自のオブジェクト解析コード)によって生成されたエラーを返し、構築時に検証します。コントラクト(assertと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は「アーキテクチャはサポートされていません」というエラーを返します。以下の表では空のセルで表されます。
スケルトン:バックエンドは存在しますが、一般的に使用される再配置をサポートしていません。単純なプログラムでも「サポートされていない再配置」エラーが発生する可能性があります。この状態のバックエンドは、新しい再配置を実装することで簡単に改善できる場合があります。ぜひ参加をご検討ください!
基本:バックエンドは単純なプログラムをサポートしていますが、まだ一般使用の準備ができていません。
使用可能:バックエンドは、少なくとも1つのコードおよび再配置モデルで一般使用できます。
良好:バックエンドはほぼすべての再配置をサポートしています。ネイティブスレッドローカルストレージなどの高度な機能はまだ利用できない場合があります。
完了:バックエンドは、すべての再配置とオブジェクトフォーマット機能をサポートしています。
アーキテクチャ |
ELF |
COFF |
MachO |
---|---|---|---|
arm32 |
スケルトン |
||
arm64 |
使用可能 |
良好 |
|
LoongArch |
良好 |
||
PowerPC 64 |
使用可能 |
||
RISC-V |
良好 |
||
x86-32 |
基本 |
||
x86-64 |
良好 |
使用可能 |
良好 |