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 旨在支援以下功能,其中一些功能仍在開發中
將單個可重定位物件跨行程和跨架構連結到目標「執行器」行程。
支援所有物件格式功能。
開啟連結器資料結構(
LinkGraph
)和 Pass 系統。
JITLink 與 ObjectLinkingLayer¶
ObjectLinkingLayer
是 ORC 的 JITLink 包裝器。它是 ORC 層,允許將物件添加到 JITDylib
,或從某些更高級別的程式表示形式發出。當發出物件時,ObjectLinkingLayer
使用 JITLink 建構 LinkGraph
(請參閱 建構 LinkGraph)並呼叫 JITLink 的 link
函式將圖形連結到執行器行程。
ObjectLinkingLayer
類別提供了一個插件 API,即 ObjectLinkingLayer::Plugin
,使用者可以將其子類別化,以便在連結時檢查和修改 LinkGraph
實例,並對重要的 JIT 事件(例如將物件發送到目標記憶體)做出反應。這實現了許多在 MCJIT 或 RuntimeDyld 下無法實現的功能和優化。
物件連結層插件¶
ObjectLinkingLayer::Plugin
類別提供以下方法
每次要連結 LinkGraph 時都會呼叫
modifyPassConfig
。可以覆寫它以安裝 JITLink *Passes* 以在連結過程中執行。void modifyPassConfig(MaterializationResponsibility &MR, const Triple &TT, jitlink::PassConfiguration &Config)
在連結開始之前會呼叫
notifyLoaded
,如果需要,可以覆寫它以設定給定MaterializationResponsibility
的任何初始狀態。void notifyLoaded(MaterializationResponsibility &MR)
在連結完成且程式碼已發送到執行程式之後,會呼叫
notifyEmitted
。如果需要,可以覆寫它以完成MaterializationResponsibility
的狀態。Error notifyEmitted(MaterializationResponsibility &MR)
如果連結在任何時候失敗,則會呼叫
notifyFailed
。可以覆寫它以對失敗做出反應(例如釋放任何已分配的資源)。Error notifyFailed(MaterializationResponsibility &MR)
當提出請求以移除與
MaterializationResponsibility
的ResourceKey
*K* 相關聯的任何資源時,會呼叫notifyRemovingResources
。Error notifyRemovingResources(ResourceKey K)
如果/當提出請求以將與
ResourceKey
*SrcKey* 相關聯的任何資源的追蹤傳輸到 *DstKey* 時,會呼叫notifyTransferringResources
。void notifyTransferringResources(ResourceKey DstKey, ResourceKey SrcKey)
插件作者必須實作 notifyFailed
、notifyRemovingResources
和 notifyTransferringResources
方法,以便在資源移除或傳輸或連結失敗的情況下安全地管理資源。如果插件沒有管理任何資源,則可以將這些方法實作為不執行任何操作並返回 Error::success()
。
透過呼叫 addPlugin
方法將插件實例添加到 ObjectLinkingLayer
[1]。例如:
// Plugin class to print the set of defined symbols in an object when that
// object is linked.
class MyPlugin : public ObjectLinkingLayer::Plugin {
public:
// Add passes to print the set of defined symbols after dead-stripping.
void modifyPassConfig(MaterializationResponsibility &MR,
const Triple &TT,
jitlink::PassConfiguration &Config) override {
Config.PostPrunePasses.push_back([this](jitlink::LinkGraph &G) {
return printAllSymbols(G);
});
}
// Implement mandatory overrides:
Error notifyFailed(MaterializationResponsibility &MR) override {
return Error::success();
}
Error notifyRemovingResources(ResourceKey K) override {
return Error::success();
}
void notifyTransferringResources(ResourceKey DstKey,
ResourceKey SrcKey) override {}
// JITLink pass to print all defined symbols in G.
Error printAllSymbols(LinkGraph &G) {
for (auto *Sym : G.defined_symbols())
if (Sym->hasName())
dbgs() << Sym->getName() << "\n";
return Error::success();
}
};
// Create our LLJIT instance using a custom object linking layer setup.
// This gives us a chance to install our plugin.
auto J = ExitOnErr(LLJITBuilder()
.setObjectLinkingLayerCreator(
[](ExecutionSession &ES, const Triple &T) {
// Manually set up the ObjectLinkingLayer for our LLJIT
// instance.
auto OLL = std::make_unique<ObjectLinkingLayer>(
ES, std::make_unique<jitlink::InProcessMemoryManager>());
// Install our plugin:
OLL->addPlugin(std::make_unique<MyPlugin>());
return OLL;
})
.create());
// Add an object to the JIT. Nothing happens here: linking isn't triggered
// until we look up some symbol in our object.
ExitOnErr(J->addObject(loadFromDisk("main.o")));
// Plugin triggers here when our lookup of main triggers linking of main.o
auto MainSym = J->lookup("main");
連結圖¶
JITLink 將所有可重定位物件格式映射到一個通用的 LinkGraph
類型,該類型旨在使連結快速簡便(LinkGraph
實例也可以手動建立。請參閱 建構連結圖)。
可重定位物件格式(例如 COFF、ELF、MachO)的細節有所不同,但它們有一個共同的目標:以允許在虛擬位址空間中重定位的註釋來表示機器級別的代碼和資料。為此,它們通常包含在檔案內部或外部定義的內容的名稱(符號)、必須作為一個單元移動的內容塊(節或子節,取決於格式),以及描述如何根據某些目標符號/節的最終位址來修補內容的註釋(重定位)。
在較高的層級上,LinkGraph
類型將這些概念表示為一個帶有修飾的圖形。圖形中的節點表示符號和內容,邊表示重定位。圖形的每個元素都列在這裡
Addressable
– 連結圖形中的一個節點,可以在執行程序的虛擬位址空間中分配一個位址。絕對符號和外部符號使用普通的
Addressable
實例來表示。物件檔案內部定義的內容使用Block
子類來表示。Block
– 一個具有Content
(或標記為零填充)、父Section
、Size
、Alignment
(和AlignmentOffset
)和Edge
實例列表的Addressable
節點。區塊為必須在目標位址空間中保持連續的二進制內容(一個*佈局單元*)提供了一個容器。許多有趣的
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
是 *強* 或 *弱* 其中之一,並且可以透過getLinkage
方法存取。JITLinkContext
可以使用此標記來確定是否應該保留或捨棄此符號定義。Scope
是 *預設*、*隱藏* 或 *局部* 其中之一,並且可以透過getScope
方法存取。JITLinkContext
可以使用此標記來確定誰應該能夠看到該符號。具有預設範圍的符號應該是全局可見的。具有隱藏範圍的符號應該對同一個模擬動態連結庫(例如 ORCJITDylib
)或可執行檔中的其他定義可見,但對其他地方不可見。具有局部範圍的符號應該只在當前的LinkGraph
中可見。Callable
是一個布林值,如果可以呼叫此符號,則設置為 true,並且可以透過isCallable
方法存取。這可以用於自動引入用於延遲編譯的呼叫存根。Live
是一個布林值,可以設置為將此符號標記為根節點,以用於程式碼移除的目的(請參閱 通用連結演算法)。JITLink 的程式碼移除演算法會在刪除任何未標記為活動的符號(和區塊)之前,將活動標記透過圖形傳播到所有可到達的符號。
Edge
–Offset
(隱含地從包含的Block
的開頭開始)、Kind
(描述重定位類型)、Target
和Addend
的四元組。邊(Edge)表示區塊(Block)和符號(Symbol)之間的重定位(Relocation),以及偶爾存在的其他關係。
可以使用
getOffset
訪問的偏移量(Offset)
,是從包含該邊(Edge)
的區塊(Block)
的開頭算起的偏移量。可以使用
getKind
訪問的種類(Kind)
是一種重定位類型 - 它描述了根據目標(Target)
的地址,應該對給定偏移量(Offset)
處的區塊內容進行哪些更改(如果有)。可以使用
getTarget
訪問的目標(Target)
是一個指向符號(Symbol)
的指標,表示其地址與邊的種類(Kind)
指定的修正計算相關。可以使用
getAddend
訪問的加數(Addend)
是一個常數,其解釋由邊的種類(Kind)
決定。
區段(Section)
- 一組符號(Symbol)
实例,以及一組區塊(Block)
实例,具有一個名稱(Name)
、一組保護標誌(ProtectionFlags)
和一個序號(Ordinal)
。區段可以輕鬆地迭代與源目標文件中特定區段關聯的符號或區塊。
blocks()
返回一個迭代器,用於迭代區段中定義的區塊集(作為Block*
指標)。symbols()
返回一個迭代器,用於迭代區段中定義的符號集(作為Symbol*
指標)。名稱(Name)
表示為llvm::StringRef
,可以使用getName
方法訪問。保護標誌(ProtectionFlags)
表示為 sys::Memory::ProtectionFlags 枚舉,可以使用getProtectionFlags
方法訪問。這些標誌描述了區段是否可讀、可寫、可執行或這些的組合。最常見的組合是RW-
表示可寫數據,R--
表示常量數據,以及R-X
表示程式碼。可以使用
getOrdinal
訪問的區段序號(SectionOrdinal)
是一個用於相對於其他區段對區段進行排序的數字。它通常用於在佈局內存時保留區段在區塊(一組具有相同內存保護的區段)內的順序。
給圖論學者的說明:LinkGraph
是二分圖(bipartite),具有一組 Symbol
節點和一組 Addressable
節點。每個 Symbol
節點都有一個(隱式)邊緣指向其目標 Addressable
。每個 Block
都有一組邊緣(可能為空,表示為 Edge
实例)返回到 Symbol
集合的元素。為了方便和提高常用算法的性能,符號和區塊會進一步分組為 Sections
。
LinkGraph
本身提供用於構建、移除和迭代區段、符號和區塊的操作。它還提供與鏈接過程相關的元數據和工具。
圖元素操作
sections
返回圖中所有區段的迭代器。findSectionByName
返回指向具有給定名稱的區段的指針(作為Section*
),如果存在,否則返回 nullptr。blocks
返回圖中所有區塊的迭代器(跨所有區段)。defined_symbols
返回圖中所有已定義符號的迭代器(跨所有區段)。external_symbols
返回圖中所有外部符號的迭代器。absolute_symbols
返回圖中所有絕對符號的迭代器。createSection
使用給定的名稱和保護標誌創建一個區段。createContentBlock
使用給定的初始內容、父區段、地址、對齊方式和對齊偏移量創建一個區塊。createZeroFillBlock
使用給定的區塊大小、父區段、地址、對齊方式和對齊偏移量創建一個零填充區塊。addExternalSymbol
使用給定的名稱、大小和鏈接創建一個新的可寻址和符號。addAbsoluteSymbol
使用給定的名稱、地址、大小、鏈接、範圍和活躍性創建一個新的可寻址和符號。addCommonSymbol
是一種便捷函數,用於使用給定的名稱、範圍、區段、初始地址、大小、對齊方式和活躍性創建一個零填充區塊和弱符號。addAnonymousSymbol
為給定的區塊、偏移量、大小、可調用性和活躍性創建一個新的匿名符號。addDefinedSymbol
為給定的區塊創建一個新的符號,並具有名稱、偏移量、大小、鏈接、範圍、可調用性和活躍性。makeExternal
通過創建一個新的可寻址並將符號指向它,將先前定義的符號轉換為外部符號。不會刪除現有的區塊,但可以通過調用removeBlock
手動移除(如果未被引用)。指向符號的所有邊緣保持有效,但現在必須在這個LinkGraph
之外定義符號。removeExternalSymbol
移除外部符號及其目標可寻址。目標可寻址不能被任何其他符號引用。removeAbsoluteSymbol
會移除絕對符號及其目標可定址區塊。目標可定址區塊不得被任何其他符號所參考。removeDefinedSymbol
會移除已定義的符號,但*不會*移除其目標區塊。removeBlock
會移除指定的區塊。splitBlock
會在指定的索引處將指定的區塊分成兩個(在已知區塊包含可分解記錄的情況下很有用,例如 eh-frame 區段中的 CFI 記錄)。
圖形工具操作
getName
會傳回此圖形的的名稱,該名稱通常基於輸入物件檔案的名稱。getTargetTriple
會傳回執行程式處理序的 llvm::Triple。getPointerSize
會傳回執行程式處理序中指標的大小(以位元組為單位)。getEndinaness
會傳回執行程式處理序的位元組順序。allocateString
會將資料從指定的llvm::Twine
複製到連結圖形的內部配置器。這可以用於確保在傳遞內建立的內容在其執行後仍然有效。
通用連結演算法¶
JITLink 提供了一個通用的連結演算法,可以通過引入 JITLink 流程 在某些點進行擴展/修改。
在每個階段結束時,連結器會將其狀態打包成一個*延續*,並呼叫 JITLinkContext
物件來執行(可能具有高延遲的)非同步操作:分配記憶體、解析外部符號,最後將連結的記憶體傳輸到執行程式處理序。
階段 1
一旦初始配置(包括流程管道設定)完成,
link
函數就會立即呼叫此階段。執行預先修剪流程。
在修剪圖形之前,會在圖形上呼叫這些流程。在此階段,
LinkGraph
節點仍然具有其原始的虛擬位址。標記活動流程(由JITLinkContext
提供)將在此序列結束時執行,以標記活動符號的初始集合。值得注意的用例:標記活動節點、存取/複製將被修剪的圖形資料(例如,對 JIT 很重要但連結處理序不需要的元資料)。
修剪(去除無用程式碼)
LinkGraph
。移除無法從活動符號的初始集合到達的所有符號和區塊。
這允許 JITLink 移除無法到達的符號/內容,包括覆蓋的弱定義和冗餘的 ODR 定義。
執行後修剪流程。
這些流程在去除無用程式碼後但在分配記憶體或為節點分配最終目標虛擬位址之前在圖形上執行。
在此階段執行的流程受益於修剪,因為已從圖形中刪除了無用的函數和資料。但是,仍然可以將新內容添加到圖形中,因為尚未分配目標記憶體和工作記憶體。
值得注意的用例:建置全域偏移表 (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 連結需要配置兩種記憶體: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
方法會取得一個表示目標模擬 dylib 的 JITLinkDylib*
、一個必須分配給 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
回呼。
已完成的分配(由 FinalizedAlloc
物件表示)可以透過呼叫 JITLinkMemoryManager::dealloc
方法來釋放。此方法會取得一個 FinalizedAlloc
物件向量,因為通常會同時釋放多個物件,這使我們可以批次處理這些請求以傳輸到執行程序。
JITLink 提供了此介面的一個簡單的程序內實作:InProcessMemoryManager
。它會分配一次分頁,並將其重複用作工作記憶體和目標記憶體。
ORC 提供了一個能夠跨程序的 MapperJITLinkMemoryManager
,它可以使用共享記憶體或基於 ORC-RPC 的通訊將內容傳輸到執行程序。
JITLinkMemoryManager 與安全性¶
JITLink 能夠連結 JIT 編譯的程式碼,供個別的執行器程序使用,這項能力可用於提升 JIT 系統的安全性:執行器程序可以在沙盒中執行、在虛擬機器中執行,甚至可以在完全獨立的機器上執行。
JITLink 的記憶體管理員介面相當靈活,允許在效能和安全性之間進行各種權衡。舉例來說,在必須簽署程式碼頁面(防止程式碼被更新)的系統上,記憶體管理員可以在連結後釋放工作記憶體頁面,以釋放執行 JITLink 的程序中的記憶體。或者,在允許 RWX 頁面的系統上,記憶體管理員可以將相同的頁面同時用於工作記憶體和目標記憶體,方法是將其標記為 RWX,允許直接修改程式碼,而不會產生額外負擔。最後,如果不允許 RWX 頁面,但允許實體記憶體頁面的雙重虛擬對應,則記憶體管理員可以在 JITLink 程序中將實體頁面雙重對應為 RW-,在執行器程序中對應為 R-X,允許從 JITLink 程序進行修改,但不允許從執行器程序進行修改(代價是雙重對應會產生額外的管理負擔)。
錯誤處理¶
JITLink 廣泛使用 llvm::Error
類型(如需詳細資訊,請參閱 LLVM 程式設計師手冊 的錯誤處理章節)。連結程序本身、所有階段、記憶體管理員介面以及對 JITLinkContext
的操作都允許失敗。建議連結圖建構工具程式(尤其是物件格式的剖析器)在應用之前驗證輸入,並驗證修正程式(例如使用範圍檢查)。
任何錯誤都會停止連結程序,並通知環境失敗。在 ORC 中,報告的失敗會傳播到等待失敗連結提供定義的查詢,也會透過相依圖的邊緣傳播到任何等待相依符號的查詢。
與 ORC 執行階段的連線¶
ORC 執行階段(目前正在開發中)旨在為進階 JIT 功能提供執行階段支援,包括需要在執行器中執行重要動作的物件格式功能(例如執行初始化器、管理執行緒區域儲存體、向語言執行階段註冊等等)。
ORC 執行階段對物件格式功能的支援通常需要執行階段(在執行器程序中執行)和 JITLink(在 JIT 程序中執行,並且可以檢查 LinkGraphs 以確定必須在執行器中採取哪些動作)之間的合作。舉例來說:在 ORC 執行階段中執行 MachO 靜態初始化器是由 jit_dlopen
函式執行的,該函式會回呼 JIT 程序,以要求提供要遍歷的 __mod_init
區段的位址範圍清單。此清單由 MachOPlatformPlugin
整理,該外掛會安裝一個階段,以便在每個物件連結到目標時記錄此資訊。
建構 LinkGraphs¶
用戶端通常會存取和操作由 ObjectLinkingLayer
執行個體為其建立的 LinkGraph
執行個體,但也可以透過下列方式手動建立這些執行個體
直接建構和填入
LinkGraph
執行個體。通過使用
createLinkGraph
函數家族,可以從包含目標文件(object file)的內存緩衝區中創建LinkGraph
。這通常是ObjectLinkingLayer
創建LinkGraphs
的方式。
- 在以下情況下可以使用
createLinkGraph_<Object-Format>_<Architecture>
:事先已知目標文件格式和架構。
當事先已知目標文件格式但架構未知時,可以使用
createLinkGraph_<Object-Format>
。在這種情況下,將通過檢查目標文件頭來確定架構。當目標文件格式和架構均未知時,可以使用
createLinkGraph
。在這種情況下,將檢查目標文件頭以確定格式和架構。
JIT 連結¶
JIT 連結器的概念是在 LLVM 早期版本的 JIT API,MCJIT 中引入的。在 MCJIT 中,「RuntimeDyld」組件通過在通常編譯器管道的末尾添加一個內存連結步驟,實現了 LLVM 作為內存編譯器的重複使用。MCJIT 並沒有像編譯器通常那樣將可重定位目標文件(relocatable object file)轉儲到磁碟,而是將它們傳遞給 RuntimeDyld 以連結到目標進程。
這種連結方法不同於標準的「靜態」或「動態」連結
「靜態連結器」將一個或多個可重定位目標文件作為輸入,並將它們連結到磁碟上的可執行文件或動態連結庫。
「動態連結器」將重定位應用於已加載到內存中的可執行文件和動態連結庫。
「JIT 連結器」一次處理一個可重定位目標文件,並將其連結到目標進程,通常使用上下文對象允許連結的代碼解析目標中的符號。
RuntimeDyld¶
為了保持 RuntimeDyld 的實現簡單,MCJIT 對編譯後的代碼施加了一些限制
它必須使用大代碼模型,並且經常限制可用的重定位模型,以限制必須支持的重定位類型。
它要求所有符號都具有強連結和默認可見性 - 其他連結/可見性的行為沒有明確定義。
它限制和/或禁止使用需要運行時支持的功能,例如靜態初始化器或線程本地存儲。
由於這些限制,並非 LLVM 支持的所有語言功能都能在 MCJIT 下正常工作,並且要加載到 JIT 下的目標文件必須編譯為以其為目標(排除在 JIT 下使用來自其他來源的預編譯代碼)。
RuntimeDyld 對連結過程本身的可見性也非常有限:客戶端可以訪問區段大小的保守估計值(RuntimeDyld 將存根大小和填充估計值捆綁到區段大小值中)和最終重定位的位元組,但無法訪問 RuntimeDyld 的內部對象表示。
消除這些限制和局限性是開發 JITLink 的主要動機之一。
llvm-jitlink 工具¶
llvm-jitlink
工具是 JITLink 庫的命令行包裝器。它加載一組可重定位目標文件,然後使用 JITLink 連結它們。根據使用的選項,它將執行它們,或驗證連結的內存。
llvm-jitlink
工具最初旨在通過提供一個簡單的測試環境來輔助 JITLink 開發。
基本用法¶
預設情況下,llvm-jitlink
會連結命令列中傳遞的物件集,然後搜尋「main」函式並執行它。
% cat hello-world.c
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("hello, world!\n");
return 0;
}
% clang -c -o hello-world.o hello-world.c
% llvm-jitlink hello-world.o
Hello, World!
可以指定多個物件,並且可以使用 -args 選項將參數提供給 JIT 處理的 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
將掃描給定的檢查檔案中格式為 # 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 在給定的主機和埠上連接到已在執行的執行程式。要使用此選項,您需要使用 listen=<host>:<port>
作為第一個參數手動啟動 llvm-jitlink-executor
。
Harness 模式¶
-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
之外解析,兩個 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++)則要困難得多:任何涉及類別的程式碼往往都有很多非平凡的表面區域(例如虛擬函數表),需要非常小心地模擬。
給 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 重定位解析程式碼綁定。大部分程式碼都是通用的,應該拆分到一個 ELFLinkGraphBuilder 基類中,類似於現有的通用 MachOLinkGraphBuilder。
實作對 arm32 的支援。
實作對其他新架構的支援。
JITLink 可用性和功能狀態¶
下表描述了各種格式/架構組合的 JITLink 後端的狀態(截至 2023 年 7 月)。
支援級別
無:沒有後端。JITLink 將返回「不支援的架構」錯誤。在下表中以空單元格表示。
骨架:後端存在,但不支援常用的重定位。即使是簡單的程式也可能觸發「不支援的重定位」錯誤。處於此狀態的後端可能很容易通過實作新的重定位來改進。考慮參與進來!
基本:後端支援簡單的程式,但尚未準備好供一般使用。
可用:後端至少適用於一種程式碼和重定位模型的一般使用。
良好:後端支援幾乎所有的重定位。進階功能(如原生執行緒區域儲存體)可能尚不可用。
完整:後端支援所有重定位和物件格式功能。
架構 |
ELF |
COFF |
MachO |
---|---|---|---|
arm32 |
骨架 |
||
arm64 |
可用 |
良好 |
|
LoongArch |
良好 |
||
PowerPC 64 |
可用 |
||
RISC-V |
良好 |
||
x86-32 |
基本 |
||
x86-64 |
良好 |
可用 |
良好 |