JITLink 與 ORC 的 ObjectLinkingLayer¶
簡介¶
本文旨在提供 JITLink 程式庫的設計與 API 的高階概述。它假設您對連結和可重定位物件檔案有一定的熟悉度,但不需深厚的專業知識。如果您知道區段 (section)、符號 (symbol) 和重定位 (relocation) 是什麼,那麼您應該會覺得本文容易理解。如果不是,請提交修補程式 (貢獻 LLVM) 或回報錯誤 (如何提交 LLVM 錯誤報告)。
JITLink 是一個用於 JIT 連結 的程式庫。它的建立是為了支援 ORC JIT API,並且最常透過 ORC 的 ObjectLinkingLayer API 存取。開發 JITLink 的目的是為了支援每種物件格式提供的完整功能集;包括靜態初始化器、例外處理、執行緒區域變數和語言執行階段註冊。支援這些功能使 ORC 能夠執行從依賴這些功能的來源語言產生的程式碼 (例如,C++ 需要物件格式支援靜態初始化器以支援靜態建構子、用於例外的 eh-frame 註冊,以及用於執行緒區域變數的 TLV 支援;Swift 和 Objective-C 需要語言執行階段註冊來支援許多功能)。對於某些物件格式功能,支援完全在 JITLink 內部提供,而對於其他功能,則與 (原型) ORC 執行階段合作提供。
JITLink 旨在支援以下功能,其中一些仍在開發中
將單個可重定位物件跨程序和跨架構連結到目標執行器程序。
支援所有物件格式功能。
開放連結器資料結構 (
LinkGraph
) 和 pass 系統。
JITLink 與 ObjectLinkingLayer¶
ObjectLinkingLayer
是 ORC 為 JITLink 提供的包裝器。它是一個 ORC 層,允許將物件新增到 JITDylib
,或從某些更高等級的程式表示法發出。當發出物件時,ObjectLinkingLayer
使用 JITLink 建構 LinkGraph
(請參閱 建構 LinkGraph) 並呼叫 JITLink 的 link
函數,將圖連結到執行器程序中。
ObjectLinkingLayer
類別提供了一個外掛程式 API,ObjectLinkingLayer::Plugin
,使用者可以子類別化它,以便在連結時檢查和修改 LinkGraph
實例,並對重要的 JIT 事件 (例如物件被發出到目標記憶體) 做出反應。這啟用了許多在 MCJIT 或 RuntimeDyld 下不可能實現的功能和優化。
ObjectLinkingLayer 外掛程式¶
ObjectLinkingLayer::Plugin
類別提供以下方法
modifyPassConfig
在每次即將連結 LinkGraph 時被呼叫。它可以被覆寫以安裝在連結過程中運行的 JITLink Pass (遍歷)。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
實例也可以手動建立。請參閱 建構 LinkGraph)。
可重定位物件格式 (例如 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
標誌。符號使命名內容 (區塊和可定址物件是匿名的) 或使用
Edge
作為目標內容成為可能。Name
表示為llvm::StringRef
(如果符號沒有名稱,則等於llvm::StringRef()
),並且可以透過getName
方法存取。Linkage
是 Strong 或 Weak 之一,並且可以透過getLinkage
方法存取。JITLinkContext
可以使用此標誌來確定是否應保留或捨棄此符號定義。Scope
是 Default、Hidden 或 Local 之一,並且可以透過getScope
方法存取。JITLinkContext
可以使用它來確定誰應該能夠看到該符號。具有預設作用域的符號應該是全域可見的。具有隱藏作用域的符號應該對同一模擬動態函式庫 (例如 ORCJITDylib
) 或可執行檔中的其他定義可見,但不能從其他地方看到。具有本地作用域的符號應該僅在目前的LinkGraph
中可見。Callable
是一個布林值,如果可以呼叫此符號,則設定為 true,並且可以透過isCallable
方法存取。這可以用於自動引入用於延遲編譯的呼叫 Stub。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
回傳執行器程序中指標的大小 (以位元組為單位)。getEndianness
回傳執行器程序的位元組序。allocateString
將資料從給定的llvm::Twine
複製到連結圖的內部分配器中。這可以用於確保在 pass 內部建立的內容比該 pass 的執行時間更長。
通用連結演算法¶
JITLink 提供了一種通用連結演算法,可以透過引入 JITLink Pass (遍歷) 在某些點進行擴展/修改。
在每個階段結束時,連結器將其狀態封裝到一個續傳中,並呼叫 JITLinkContext
物件來執行 (可能高延遲的) 非同步操作:分配記憶體、解析外部符號,以及最終將連結的記憶體傳輸到執行程序。
階段 1
當初始配置 (包括 pass 管線設定) 完成後,
link
函數會立即呼叫此階段。執行預先修剪遍歷。
這些 pass 在圖被修剪之前在圖上呼叫。在此階段,
LinkGraph
節點仍然具有其原始 vmaddr。JITLinkContext
提供的標記存活 pass (mark-live pass) 將在此序列結束時運行,以標記初始的存活符號集合。值得注意的使用案例:標記節點為存活、存取/複製將被修剪的圖資料 (例如,對 JIT 很重要但連結過程不需要的元資料)。
修剪 (移除無效程式碼)
LinkGraph
。移除所有無法從初始存活符號集合到達的符號和區塊。
這允許 JITLink 移除無法到達的符號/內容,包括覆寫的弱定義和冗餘 ODR 定義。
執行後修剪遍歷。
這些 pass 在移除無效程式碼後但在分配記憶體或節點分配其最終目標 vmaddr 之前在圖上運行。
在此階段運行的 pass 受益於修剪,因為無效函數和資料已從圖中移除。但是,由於尚未分配目標記憶體和工作記憶體,因此仍然可以將新內容新增到圖中。
值得注意的使用案例:建構全域偏移表 (GOT)、程序連結表 (PLT) 和執行緒區域變數 (TLV) 條目。
非同步分配記憶體。
呼叫
JITLinkContext
的JITLinkMemoryManager
為圖分配工作記憶體和目標記憶體。作為此過程的一部分,JITLinkMemoryManager
將所有在圖中定義的節點的位址更新為其分配的目標位址。注意:此步驟僅更新在此圖中定義的節點的位址。外部符號仍然具有空位址。
階段 2
執行後分配遍歷。
這些 pass 在分配工作記憶體和目標記憶體之後,但在將圖中符號的最終位址通知
JITLinkContext
之前在圖上運行。這讓這些 pass 有機會在任何 JITLink 客戶端 (尤其是用於符號解析的 ORC 查詢) 嘗試存取它們之前,設定與目標位址相關聯的資料結構。值得注意的使用案例:設定目標位址和 JIT 資料結構之間的映射,例如
__dso_handle__
和JITDylib*
之間的映射。將分配的符號位址通知
JITLinkContext
。在連結圖上呼叫
JITLinkContext::notifyResolved
,允許客戶端對為此圖進行的符號位址分配做出反應。在 ORC 中,這用於通知任何待處理的已解析符號查詢,包括來自並行運行的 JITLink 實例的待處理查詢,這些實例已到達下一步並正在等待此圖中符號的位址以繼續其連結。識別外部符號並非同步解析其位址。
呼叫
JITLinkContext
以解析圖中任何外部符號的目標位址。
階段 3
應用外部符號解析結果。
這會更新所有外部符號的位址。此時,圖中的所有節點都具有其最終目標位址,但是節點內容仍然指向物件檔案中的原始資料。
執行預先修復遍歷。
這些 pass 在所有節點都已分配其最終目標位址之後但在將節點內容複製到工作記憶體並修復之前在圖上呼叫。在此階段運行的 pass 可以根據位址佈局對圖和內容進行後期優化。
顯著的使用案例: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();
}
記錄呼叫點以供後續變更 – 後分配階段可以記錄對特定函數的所有呼叫的呼叫點,允許稍後在運行時更新這些呼叫點(例如,用於 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 的小型代碼模型要求模擬動態函式庫的所有代碼和數據都分配在 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 系統的安全性:執行器程序可以沙盒化、在 VM 中運行,甚至在完全單獨的機器上運行。
JITLink 的記憶體管理器介面足夠靈活,可以允許在性能和安全性之間進行一系列權衡。例如,在代碼頁面必須簽署(防止代碼被更新)的系統上,記憶體管理器可以在連結後解分配工作記憶體頁面,以釋放運行 JITLink 的程序中的記憶體。或者,在允許 RWX 頁面的系統上,記憶體管理器可以通過將它們標記為 RWX,對工作記憶體和目標記憶體使用相同的頁面,允許就地修改代碼而無需進一步的開銷。最後,如果不允許 RWX 頁面,但允許物理記憶體頁面的雙虛擬映射,則記憶體管理器可以將物理頁面雙重映射為 JITLink 程序中的 RW- 和執行器程序中的 R-X,允許從 JITLink 程序進行修改,但不允許從執行器進行修改(以雙重映射的額外管理開銷為代價)。
錯誤處理¶
JITLink 廣泛使用 llvm::Error
類型(有關詳細資訊,請參閱 LLVM 程式設計手冊 的錯誤處理章節)。連結過程本身、所有階段、記憶體管理器介面以及 JITLinkContext
上的操作都允許失敗。連結圖形建構工具(尤其是物件格式的解析器)被鼓勵驗證輸入,並在應用之前驗證修復(例如,使用範圍檢查)。
任何錯誤都將停止連結過程並通知上下文失敗。在 ORC 中,報告的失敗會傳播到等待失敗連結提供的定義的查詢,以及通過相依性圖形的邊緣傳播到任何等待相依符號的查詢。
與 ORC 運行時的連接¶
ORC 運行時(目前正在開發中)旨在為高級 JIT 功能提供運行時支援,包括需要在執行器中執行非平凡動作的物件格式功能(例如,運行初始化器、管理線程本地存儲、向語言運行時註冊等)。
ORC 運行時對物件格式功能的支援通常需要運行時(在執行器程序中執行)和 JITLink(在 JIT 程序中運行並可以檢查 LinkGraph 以確定必須在執行器中採取的行動)之間的合作。例如:在 ORC 運行時中執行 MachO 靜態初始化器是由 jit_dlopen
函數執行的,該函數回呼到 JIT 程序以請求要遍歷的 __mod_init
區段的位址範圍列表。此列表由 MachOPlatformPlugin
整理,後者安裝一個階段來記錄每個物件在連結到目標時的此資訊。
建構 LinkGraphs¶
客戶端通常存取和操作由 ObjectLinkingLayer
實例為它們創建的 LinkGraph
實例,但它們可以手動創建
通過直接建構和填充
LinkGraph
實例。通過使用
createLinkGraph
函數族從包含物件檔的記憶體內緩衝區創建LinkGraph
。這就是ObjectLinkingLayer
通常創建LinkGraph
的方式。
當物件格式和架構都預先知道時,可以使用
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 將 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 <進入點名稱>
選項指定替代進入點。
其他選項可以通過調用 llvm-jitlink -help
找到。
llvm-jitlink 作為回歸測試工具¶
llvm-jitlink
的主要目標之一是為 JITLink 啟用可讀的回歸測試。為此,它支援兩個選項
-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
會將給定的物件連結到其自己的程序中,但可以通過兩個選項覆蓋此行為
-oop-executor[=/executor路徑]
選項告訴 llvm-jitlink
執行給定的執行器(預設為 llvm-jitlink-executor
),並通過檔案描述符與其通訊,檔案描述符以 filedescs=<in-fd>,<out-fd>
格式作為第一個參數傳遞給執行器。
-oop-executor-connect=<主機>:<埠>
選項告訴 llvm-jitlink
通過 TCP 連接到給定主機和埠上已運行的執行器。要使用此選項,您需要使用 listen=<主機>:<埠>
作為第一個參數手動啟動 llvm-jitlink-executor
。
Harness 模式¶
-harness
選項允許將一組輸入物件指定為測試 harness,而常規物件檔則隱式地被視為要測試的物件。harness 集中符號的定義會覆蓋測試集中的定義,並且來自 harness 的外部參考會導致自動範圍提升測試集中的本地符號(對通常連結器規則的這些修改是通過 llvm-jitlink
在看到 -harness
選項時安裝的 ObjectLinkingLayer::Plugin
完成的)。
通過這些修改,我們可以通過模擬這些函數的被調用者來選擇性地測試物件檔中的函數。例如,假設我們有一個物件檔 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++)的 Mock 要棘手得多:任何涉及類別的代碼都傾向於具有大量非平凡的表面積(例如 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 將返回 “架構不支援” 錯誤。以下表格中的空單元格表示。
骨架:後端存在,但不支援常用的重定位。即使是簡單的程式也可能觸發 “不支援的重定位” 錯誤。通過實作新的重定位,可以很容易地改進此狀態下的後端。考慮參與!
基本:後端支援簡單的程式,但尚未準備好用於一般用途。
可用:後端可用於至少一種代碼和重定位模型的一般用途。
良好:後端支援幾乎所有重定位。像原生線程本地存儲這樣的高級功能可能尚不可用。
完整:後端支援所有重定位和物件格式功能。
架構 |
ELF |
COFF |
MachO |
---|---|---|---|
arm32 |
骨架 |
||
arm64 |
可用 |
良好 |
|
LoongArch |
良好 |
||
PowerPC 64 |
可用 |
||
RISC-V |
良好 |
||
x86-32 |
基本 |
||
x86-64 |
良好 |
可用 |
良好 |