MCJIT 設計與實作

簡介

本文檔描述了 MCJIT 執行引擎和 RuntimeDyld 組件的內部工作原理。它旨在作為對實現的高階概述,展示在整個程式碼生成和動態載入過程中物件的流程和交互作用。

引擎建立

在大多數情況下,使用 EngineBuilder 物件來建立 MCJIT 執行引擎的實例。 EngineBuilder 將 llvm::Module 物件作為參數傳遞給其建構函式。然後,客戶端可以設定各種選項,這些選項將在稍後傳遞給 MCJIT 引擎,包括選擇 MCJIT 作為要建立的引擎類型。特別值得一提的是 EngineBuilder::setMCJITMemoryManager 函式。如果客戶端此時未明確建立記憶體管理器,則在實例化 MCJIT 引擎時將建立預設記憶體管理器(特別是 SectionMemoryManager)。

設定選項後,客戶端呼叫 EngineBuilder::create 來建立 MCJIT 引擎的實例。如果客戶端沒有使用將 TargetMachine 作為參數的函式形式,則將根據用於建立 EngineBuilder 的模組關聯的目標三元組建立新的 TargetMachine。

_images/MCJIT-engine-builder.png

EngineBuilder::create 將呼叫靜態 MCJIT::createJIT 函式,並將其指向模組、記憶體管理器和目標機器物件的指標傳遞進去,所有這些物件隨後都將由 MCJIT 物件擁有。

MCJIT 類別有一個成員變數 Dyld,其中包含 RuntimeDyld 包裝類別的實例。此成員將用於 MCJIT 和載入物件時建立的實際 RuntimeDyldImpl 物件之間的通訊。

_images/MCJIT-creation.png

建立後,MCJIT 持有一個指向從 EngineBuilder 接收的模組物件的指標,但它不會立即為該模組生成程式碼。程式碼生成會延遲到明確呼叫 MCJIT::finalizeObject 方法或呼叫需要生成程式碼的函式(例如 MCJIT::getPointerToFunction)時才會進行。

程式碼生成

如上所述,當觸發程式碼生成時,MCJIT 將首先嘗試從其 ObjectCache 成員中檢索物件映像(如果已設定)。如果無法檢索到快取的物件映像,MCJIT 將呼叫其 emitObject 方法。 MCJIT::emitObject 使用本機 PassManager 實例並建立一個新的 ObjectBufferStream 實例,並在呼叫 PassManager::run on the Module 之前將這兩個實例傳遞給 TargetMachine::addPassesToEmitMC。

_images/MCJIT-load.png

PassManager::run 呼叫會導致 MC 程式碼生成機制將完整的可重定位二進制物件映像(根據目標,採用 ELF 或 MachO 格式)發射到 ObjectBufferStream 物件中,然後將其清空以完成該過程。如果正在使用 ObjectCache,則映像將在此處傳遞給 ObjectCache。

此時,ObjectBufferStream 包含原始物件映像。在執行程式碼之前,必須先將此映像中的程式碼和資料區段載入到適當的記憶體中,然後套用重定位,並且必須完成記憶體權限和程式碼快取失效 (如果需要)。

物件載入

物件映像一旦透過程式碼產生或從 ObjectCache 中擷取後,就會傳遞至 RuntimeDyld 進行載入。 RuntimeDyld 包裝函式類別會檢查物件以判斷其檔案格式,並建立 RuntimeDyldELF 或 RuntimeDyldMachO 的執行個體 (兩者皆衍生自 RuntimeDyldImpl 基礎類別),然後呼叫 RuntimeDyldImpl::loadObject 方法來執行實際的載入作業。

_images/MCJIT-dyld-load.png

RuntimeDyldImpl::loadObject 會先從收到的 ObjectBuffer 建立 ObjectImage 執行個體。 ObjectImage 包裝了 ObjectFile 類別,是一個輔助類別,用於剖析二進位物件映像,並提供對格式特定標頭中所含資訊的存取權,包括區段、符號和重定位資訊。

RuntimeDyldImpl::loadObject 接著會逐一查看映像中的符號。會收集有關通用符號的資訊,以供稍後使用。對於每個函式或資料符號,關聯的區段會載入到記憶體中,並且符號會儲存在符號表對應資料結構中。當逐一查看作業完成時,會為通用符號發出一個區段。

接著,RuntimeDyldImpl::loadObject 會逐一查看物件映像中的區段,並針對每個區段逐一查看該區段的重定位。對於每個重定位,它會呼叫格式特定的 processRelocationRef 方法,該方法會檢查重定位並將其儲存在兩個資料結構之一中,即基於區段的重定位清單對應和外部符號重定位對應。

_images/MCJIT-load-object.png

當 RuntimeDyldImpl::loadObject 返回時,物件的所有程式碼和資料區段都已載入到記憶體管理員配置的記憶體中,並且已準備好重定位資訊,但重定位尚未套用,產生的程式碼仍然無法執行。

[目前 (截至 2013 年 8 月),MCJIT 引擎會在 loadObject 完成時立即套用重定位。然而,這不應該發生。因為程式碼可能是為遠端目標產生的,所以應該讓用戶端有機會在套用重定位之前重新對應區段位址。可以多次套用重定位,但在要重新對應位址的情況下,第一次套用是浪費精力。]

位址重新對應

在產生初始程式碼之後和呼叫 finalizeObject 之前的任何時間,用戶端都可以重新對應物件中區段的位址。通常這樣做是因為程式碼是為外部行程產生的,並且正在對應到該行程的位址空間。用戶端透過呼叫 MCJIT::mapSectionAddress 來重新對應區段位址。這應該在區段記憶體複製到其新位置之前發生。

當呼叫 MCJIT::mapSectionAddress 時,MCJIT 會將呼叫傳遞給 RuntimeDyldImpl (透過其 Dyld 成員)。 RuntimeDyldImpl 會將新位址儲存在內部資料結構中,但此時不會更新程式碼,因為其他區段可能會變更。

當用戶端完成區段位址的重新對應時,它會呼叫 MCJIT::finalizeObject 來完成重新對應程序。

最終準備

當呼叫 MCJIT::finalizeObject 時,MCJIT 會呼叫 RuntimeDyld::resolveRelocations。此函式會嘗試找出任何外部符號,然後套用物件的所有重定位。

外部符號會透過呼叫記憶體管理器的 getPointerToNamedFunction 方法來解析。記憶體管理器會在目標位址空間中傳回所請求符號的位址。(請注意,這在主機程序中可能不是有效的指標。)RuntimeDyld 接著會迭代儲存的與此符號關聯的重新定位清單,並呼叫 resolveRelocation 方法,該方法會透過特定格式的實作將重新定位應用於已載入的區段記憶體。

接下來,RuntimeDyld::resolveRelocations 會迭代區段清單,並針對每個區段迭代已儲存的參照該符號的重新定位清單,並針對此清單中的每個項目呼叫 resolveRelocation。此處的重新定位清單是重新定位的清單,其中與重新定位關聯的符號位於與清單關聯的區段中。這些位置中的每一個都有一個目標位置,重新定位將應用於該位置,該位置很可能位於不同的區段中。

_images/MCJIT-resolve-relocations.png

一旦如上所述應用重新定位後,MCJIT 會呼叫 RuntimeDyld::getEHFrameSection,如果傳回非零結果,則將區段資料傳遞給記憶體管理器的 registerEHFrames 方法。這允許記憶體管理器呼叫任何所需的目標特定函數,例如向除錯器註冊 EH 框架資訊。

最後,MCJIT 會呼叫記憶體管理器的 finalizeMemory 方法。在此方法中,記憶體管理器將在必要時使目標程式碼快取失效,並將最終權限應用於其為程式碼和資料記憶體配置的記憶體頁面。