LLVM 目標獨立程式碼產生器¶
警告
這項功能仍在開發中。
簡介¶
LLVM 目標獨立程式碼產生器是一個框架,提供了一套可重複使用的元件,用於將 LLVM 內部表示轉譯為指定目標的機器碼,可以是組譯形式(適用於靜態編譯器)或二進位機器碼格式(適用於 JIT 編譯器)。LLVM 目標獨立程式碼產生器由六個主要元件組成
抽象目標描述 介面,用於擷取機器各個面向的重要屬性,而與其使用方式無關。這些介面定義在
include/llvm/Target/
中。用於表示 正在為目標產生的程式碼 的類別。這些類別的設計理念是抽象到足以表示 *任何* 目標機器的機器碼。這些類別定義在
include/llvm/CodeGen/
中。在此層級上,明確公開了「常數池項目」和「跳轉表」等概念。用於在物件檔案層級表示程式碼的類別和演算法,即 MC 層。這些類別表示組譯層級的結構,例如標籤、區段和指令。在此層級上,不存在「常數池項目」和「跳轉表」等概念。
用於實作原生程式碼產生各個階段(暫存器配置、排程、堆疊框架表示等)的 目標獨立演算法。此程式碼位於
lib/CodeGen/
中。特定目標的 抽象目標描述介面的實作。這些機器描述使用 LLVM 提供的元件,並且可以選擇性地提供自訂目標特定傳遞,以建構特定目標的完整程式碼產生器。目標描述位於
lib/Target/
中。目標獨立 JIT 元件。LLVM JIT 完全獨立於目標(它使用
TargetJITInfo
結構來處理目標特定的問題)。目標獨立 JIT 的程式碼位於lib/ExecutionEngine/JIT
中。
根據您感興趣的程式碼產生器部分,不同的部分對您而言將會有用。無論如何,您應該熟悉 目標描述 和 機器碼表示 類別。如果您想為新目標新增後端,則需要 實作目標描述 類別,並瞭解 LLVM 程式碼表示。如果您有興趣實作新的 程式碼產生演算法,則它應該只依賴目標描述和機器碼表示類別,以確保它是可移植的。
程式碼產生器中的必要元件¶
LLVM 程式碼產生器包含兩個部分:高階程式碼產生器介面,以及一組可用於建構目標特定後端的可重複使用元件。兩個最重要的介面( TargetMachine 以及 DataLayout )是後端融入 LLVM 系統唯二需要定義的介面,但如果要使用可重複使用的程式碼產生器元件,則必須定義其他介面。
這種設計有兩個重要含義。首先,LLVM 可以支援完全非傳統的程式碼產生目標。例如,C 後端不需要暫存器配置、指令選擇或系統提供的任何其他標準元件。因此,它只實現這兩個介面,並執行自己的工作。請注意,自 LLVM 3.1 版本起,C 後端已從主線中移除。另一個這樣的程式碼產生器範例是(純粹假設的)將 LLVM 轉換為 GCC RTL 形式並使用 GCC 為目標發出機器碼的後端。
這種設計也意味著可以在 LLVM 系統中設計和實現完全不同的程式碼產生器,而無需使用任何內建元件。完全不建議這樣做,但對於不符合 LLVM 機器描述模型的完全不同的目標(例如 FPGA)可能是必需的。
程式碼產生器的高階設計¶
LLVM 目標獨立程式碼產生器旨在為標準基於暫存器的微處理器支援高效且高品質的程式碼產生。在此模型中,程式碼產生分為以下幾個階段
指令選擇 — 此階段決定以目標指令集表示輸入 LLVM 程式碼的有效方法。此階段以目標指令集產生程式的初始程式碼,然後使用 SSA 形式的虛擬暫存器和表示由於目標約束或呼叫慣例所需的任何暫存器分配的物理暫存器。此步驟將 LLVM 程式碼轉換為目標指令的 DAG。
排程和形成 — 此階段採用指令選擇階段產生的目標指令 DAG,確定指令的順序,然後以 MachineInstrs 的形式發出指令,並按照該順序排列。請注意,我們在指令選擇章節中描述了這一點,因為它在SelectionDAG上運行。
基於 SSA 的機器碼優化 — 這個可選階段包含一系列對指令選擇器產生的 SSA 形式進行操作的機器碼優化。模組化排程或窺孔優化等優化在此處發揮作用。
暫存器配置 — 目標程式碼從 SSA 形式的無限虛擬暫存器文件轉換為目標使用的具體暫存器文件。此階段引入溢出程式碼並從程式中消除所有虛擬暫存器引用。
序言/結尾程式碼插入 — 一旦為函數產生了機器碼並且已知所需的堆疊空間量(用於 LLVM alloca 和溢出槽),就可以插入函數的序言和結尾程式碼,並且可以消除“抽象堆疊位置引用”。此階段負責實現框架指標消除和堆疊打包等優化。
後期機器碼最佳化 — 在此處可以找到對「最終」機器碼進行操作的最佳化,例如溢出程式碼排程和窺孔最佳化。
程式碼發出 — 最後階段實際輸出當前函式的程式碼,格式可以是目標組合語言或機器碼。
程式碼產生器的設計基於一個假設:指令選擇器將使用最佳模式匹配選擇器來創建高質量的原生指令序列。基於模式展開和激進迭代窺孔最佳化的替代程式碼產生器設計速度要慢得多。此設計允許使用不同複雜程度的組件來執行編譯的任何步驟,從而實現高效編譯(對於 JIT 環境很重要)和積極的最佳化(在離線產生程式碼時使用)。
除了這些階段之外,目標實現可以在流程中插入任意的目標特定遍。例如,X86 目標使用一個特殊的遍來處理 80x87 浮點堆疊架構。其他具有特殊要求的目標可以根據需要通過自定義遍來支持。
使用 TableGen 進行目標描述¶
目標描述類需要對目標架構進行詳細描述。這些目標描述通常具有大量共同信息(例如,add
指令和 sub
指令幾乎相同)。為了盡可能地提取出共同點,LLVM 程式碼產生器使用 TableGen 概述 工具來描述目標機器的各個部分,允許使用特定領域和特定目標的抽象來減少重複量。
隨著 LLVM 的不斷開發和完善,我們計劃將越來越多的目標描述轉移到 .td
形式。這樣做給我們帶來了很多好處。最重要的是它使移植 LLVM 變得更容易,因為它減少了必須編寫的 C++ 程式碼量,以及在人們可以開始工作之前需要理解的程式碼產生器的表面積。其次,它使更改更容易。特別是,如果表和其他東西都是由 tblgen
發出的,我們只需要在一個地方(tblgen
)進行更改即可將所有目標更新到新的介面。
目標描述類¶
LLVM 目標描述類(位於 include/llvm/Target
目錄中)提供了獨立於任何特定客戶端的目標機器的抽象描述。這些類旨在捕捉目標的*抽象*屬性(例如它具有的指令和寄存器),並且不包含任何特定的程式碼產生演算法。
所有目標描述類( DataLayout 類除外)都設計為由具體目標實現子類化,並實現虛擬方法。為了獲得這些實現, TargetMachine 類提供了應該由目標實現的訪問器。
TargetMachine
類別¶
TargetMachine
類別提供虛擬方法,這些方法用於透過 get*Info
方法(getInstrInfo
、getRegisterInfo
、getFrameInfo
等等)存取各種目標描述類別的目標特定實作。這個類別設計為由具體目標實作(例如,X86TargetMachine
)進行特化,該實作會實作各種虛擬方法。唯一需要的目標描述類別是 DataLayout 類別,但如果要使用程式碼產生器元件,則也應該實作其他介面。
DataLayout
類別¶
DataLayout
類別是唯一需要的目標描述類別,而且它是唯一不可擴展的類別(您無法從中衍生新的類別)。DataLayout
指定有關目標如何配置結構記憶體的資訊、各種資料類型的對齊需求、目標中指標的大小,以及目標是小端序還是大端序。
TargetLowering
類別¶
SelectionDAG 型指令選擇器主要使用 TargetLowering
類別來描述 LLVM 程式碼應該如何降低為 SelectionDAG 作業。除此之外,這個類別還指示
要為各種
ValueType
使用的初始暫存器類別,目標機器原生支援哪些作業,
setcc
作業的回傳類型,用於移位量的類型,以及
各種高階特性,例如將除以常數轉換為乘法序列是否有利。
TargetRegisterInfo
類別¶
TargetRegisterInfo
類別用於描述目標的暫存器檔案以及暫存器之間的任何交互作用。
暫存器在程式碼產生器中以無符號整數表示。實體暫存器(目標描述中實際存在的那些)是唯一的較小數字,而虛擬暫存器通常較大。請注意,暫存器 #0
保留為旗標值。
處理器描述中的每個暫存器都有一個關聯的 TargetRegisterDesc
項目,該項目提供暫存器的文字名稱(用於組譯輸出和除錯傾印)和一組別名(用於指示一個暫存器是否與另一個暫存器重疊)。
除了每個暫存器的描述外,《TargetRegisterInfo
類別》還公開了一組處理器特定的暫存器類別(TargetRegisterClass
類別的實例)。每個暫存器類別都包含具有相同屬性的暫存器集(例如,它們都是 32 位元整數暫存器)。指令選擇器建立的每個 SSA 虛擬暫存器都有一個關聯的暫存器類別。當暫存器分配器執行時,它會將虛擬暫存器替換為集合中的實體暫存器。
這些類別的目標特定實作是從暫存器檔案的 TableGen 概觀 描述自動產生的。
《TargetInstrInfo
類別》¶
TargetInstrInfo
類別用於描述目標支援的機器指令。描述定義了諸如操作碼的助記符、運算元數量、隱式暫存器使用和定義列表、指令是否具有一定的目標獨立屬性(存取記憶體、可交換等)以及是否持有任何目標特定旗標之類的內容。
《TargetFrameLowering
類別》¶
TargetFrameLowering
類別用於提供有關目標堆疊框架佈局的資訊。它包含堆疊增長方向、進入每個函數時已知的堆疊對齊方式以及到本地區域的偏移量。到本地區域的偏移量是從函數輸入上的堆疊指標到可以儲存函數數據(本地變數、溢出位置)的第一個位置的偏移量。
《TargetSubtarget
類別》¶
TargetSubtarget
類別用於提供有關目標特定晶片組的資訊。子目標通知程式碼產生支援哪些指令、指令延遲和指令執行行程;也就是使用哪些處理單元、以什麼順序以及持續多長時間。
《TargetJITInfo
類別》¶
TargetJITInfo
類別公開了一個抽象介面,即時程式碼產生器使用該介面執行特定於目標的活動,例如發出存根。如果 TargetMachine
支援 JIT 程式碼產生,則它應該透過 getJITInfo
方法提供其中一個物件。
機器碼描述類別¶
在高階,LLVM 程式碼會被轉換為由 構成的機器特定表示形式 MachineFunction ,
MachineBasicBlock 以及
MachineInstr 實例(定義於 include/llvm/CodeGen
中)。這種表示方式完全與目標無關,以最抽象的形式表示指令:操作碼和一系列運算元。此表示形式旨在同時支援機器碼的 SSA 表示形式,以及暫存器分配的非 SSA 形式。
MachineInstr
類別¶
目標機器指令以 MachineInstr
類別的實例表示。這個類別是一種非常抽象的機器指令表示方式。具體來說,它只會追蹤操作碼編號和一組運算元。
操作碼編號只是一個簡單的無符號整數,只有對特定後端才有意義。目標的所有指令都應該定義在目標的 *InstrInfo.td
檔案中。操作碼列舉值是從此描述自動生成的。 MachineInstr
類別沒有任何關於如何解釋指令的資訊(即指令的語義);為此,您必須參考
TargetInstrInfo 類別。
機器指令的運算元可以是幾種不同類型:暫存器參考、常數整數、基本區塊參考等。此外,機器運算元應標記為值的定義或使用(儘管只有暫存器允許作為定義)。
按照慣例,LLVM 程式碼產生器會對指令運算元進行排序,以便所有暫存器定義都在暫存器使用之前,即使在通常以其他順序列印的架構上也是如此。例如,SPARC 加法指令:「add %i1, %i2, %i3
」將「%i1」和「%i2」暫存器相加,並將結果儲存在「%i3」暫存器中。在 LLVM 程式碼產生器中,運算元應儲存為「%i3, %i1, %i2
」:目標在前。
將目標(定義)運算元保留在運算元清單的開頭有幾個優點。特別是,除錯印表機會像這樣列印指令
%r3 = add %i1, %i2
此外,如果第一個運算元是定義,則更容易建立指令,其唯一定義是第一個運算元。
使用 MachineInstrBuilder.h
函式¶
機器指令是使用 BuildMI
函式建立的,這些函式位於 include/llvm/CodeGen/MachineInstrBuilder.h
檔案中。 BuildMI
函式可以輕鬆建立任意機器指令。 BuildMI
函式的用法如下所示
// Create a 'DestReg = mov 42' (rendered in X86 assembly as 'mov DestReg, 42')
// instruction and insert it at the end of the given MachineBasicBlock.
const TargetInstrInfo &TII = ...
MachineBasicBlock &MBB = ...
DebugLoc DL;
MachineInstr *MI = BuildMI(MBB, DL, TII.get(X86::MOV32ri), DestReg).addImm(42);
// Create the same instr, but insert it before a specified iterator point.
MachineBasicBlock::iterator MBBI = ...
BuildMI(MBB, MBBI, DL, TII.get(X86::MOV32ri), DestReg).addImm(42);
// Create a 'cmp Reg, 0' instruction, no destination reg.
MI = BuildMI(MBB, DL, TII.get(X86::CMP32ri8)).addReg(Reg).addImm(42);
// Create an 'sahf' instruction which takes no operands and stores nothing.
MI = BuildMI(MBB, DL, TII.get(X86::SAHF));
// Create a self looping branch instruction.
BuildMI(MBB, DL, TII.get(X86::JNE)).addMBB(&MBB);
如果需要新增定義運算元(除了可選的目標暫存器之外),則必須明確標記為定義
MI.addReg(Reg, RegState::Define);
固定(預先分配的)暫存器¶
程式碼產生器需要意識到的一個重要問題,就是固定暫存器的存在。特別是在指令流中,暫存器分配器*必須*將特定值安排在特定暫存器中的情況時常發生。這可能是由於指令集的限制(例如,X86 只能使用 EAX
/EDX
暫存器進行 32 位元除法),或是像呼叫慣例等外部因素所致。在任何情況下,指令選擇器都應該發出在需要時將虛擬暫存器複製到實體暫存器或從實體暫存器複製出來的程式碼。
例如,請考慮這個簡單的 LLVM 範例
define i32 @test(i32 %X, i32 %Y) {
%Z = sdiv i32 %X, %Y
ret i32 %Z
}
X86 指令選擇器可能會為 div
和 ret
產生以下機器碼
;; Start of div
%EAX = mov %reg1024 ;; Copy X (in reg1024) into EAX
%reg1027 = sar %reg1024, 31
%EDX = mov %reg1027 ;; Sign extend X into EDX
idiv %reg1025 ;; Divide by Y (in reg1025)
%reg1026 = mov %EAX ;; Read the result (Z) out of EAX
;; Start of ret
%EAX = mov %reg1026 ;; 32-bit return value goes in EAX
ret
在程式碼產生結束時,暫存器分配器會合併暫存器並刪除產生的識別移動,產生以下程式碼
;; X is in EAX, Y is in ECX
mov %EAX, %EDX
sar %EDX, 31
idiv %ECX
ret
這種方法非常通用(如果它可以處理 X86 架構,它就可以處理任何東西!),並且允許將所有關於指令流的目標特定知識隔離在指令選擇器中。請注意,實體暫存器應該具有較短的生命週期,以便產生良好的程式碼,並且所有實體暫存器在進入和離開基本區塊時(在暫存器分配之前)都被假定為失效。因此,如果您需要一個值在基本區塊邊界上保持活動狀態,它*必須*位於虛擬暫存器中。
被呼叫覆寫的暫存器¶
某些機器指令(例如呼叫)會覆寫大量的實體暫存器。您可以使用 MO_RegisterMask
運算元,而不是為所有暫存器新增 <def,dead>
運算元。暫存器遮罩運算元包含一個已保留暫存器的位元遮罩,而其他所有暫存器都被視為已被指令覆寫。
SSA 形式的機器碼¶
MachineInstr
最初是以 SSA 形式選擇的,並且在暫存器分配發生之前一直保持 SSA 形式。在大多數情況下,這非常簡單,因為 LLVM 已經是 SSA 形式;LLVM PHI 節點會變成機器碼 PHI 節點,並且虛擬暫存器只允許有一個定義。
在暫存器分配之後,機器碼就不再是 SSA 形式,因為程式碼中沒有虛擬暫存器了。
MachineBasicBlock
類別¶
MachineBasicBlock
類別包含機器指令清單 ( MachineInstr 執行個體)。它大致對應於輸入到指令選擇器的 LLVM 程式碼,但可以是一對多對應(即一個 LLVM 基本區塊可以對應到多個機器基本區塊)。MachineBasicBlock
類別有一個「getBasicBlock
」方法,它會傳回它來自的 LLVM 基本區塊。
MachineFunction
類別¶
MachineFunction
類別包含一個機器基本區塊的列表( MachineBasicBlock 實例)。它與輸入到指令選擇器的 LLVM 函數一對一對應。除了基本區塊列表之外,MachineFunction
還包含一個 MachineConstantPool
、一個 MachineFrameInfo
、一個 MachineFunctionInfo
和一個 MachineRegisterInfo
。如需更多資訊,請參閱 include/llvm/CodeGen/MachineFunction.h
。
MachineInstr Bundles
¶
LLVM 程式碼產生器可以將指令序列建模為 MachineInstr bundles。一個 MI bundle 可以建模一個 VLIW group / pack,其中包含任意數量的平行指令。它也可以用來建模一個不能合法分開的指令序列(可能具有資料相依性)(例如 ARM Thumb2 IT blocks)。
從概念上講,MI bundle 是一個 MI,其中嵌入了許多其他 MI
--------------
| Bundle | ---------
-------------- \
| ----------------
| | MI |
| ----------------
| |
| ----------------
| | MI |
| ----------------
| |
| ----------------
| | MI |
| ----------------
|
--------------
| Bundle | --------
-------------- \
| ----------------
| | MI |
| ----------------
| |
| ----------------
| | MI |
| ----------------
| |
| ...
|
--------------
| Bundle | --------
-------------- \
|
...
MI bundle 支援不會改變 MachineBasicBlock 和 MachineInstr 的物理表示。所有 MI(包括頂級和嵌套的 MI)都儲存為 MI 的序列列表。「bundled」MI 會標記為「InsideBundle」旗標。具有特殊 BUNDLE 操作碼的頂級 MI 用於表示 bundle 的開始。將 BUNDLE MI 與不在 bundle 中也不代表 bundle 的個別 MI 混合使用是合法的。
MachineInstr passes 應該將 MI bundle 作為單一單元進行操作。成員方法已被教導正確處理 bundle 和 bundle 內的 MI。MachineBasicBlock 迭代器已被修改為跳過 bundled MI,以強制執行 bundle 作為單一單元的概念。MachineBasicBlock 中已添加另一個迭代器 instr_iterator,以允許 passes 迭代 MachineBasicBlock 中的所有 MI,包括嵌套在 bundle 中的 MI。頂級 BUNDLE 指令必須具有正確的暫存器 MachineOperand 集合,這些 MachineOperand 代表 bundled MI 的累積輸入和輸出。
針對 VLIW 架構打包/捆綁 MachineInstrs 通常應作為暫存器分配超級過程的一部分來完成。更具體地說,決定哪些 MI 應該捆綁在一起的過程應該在程式碼產生器退出 SSA 形式之後完成(即在雙地址過程、PHI 消除和複製合併之後)。此類 bundle 應在虛擬暫存器被重寫為實體暫存器後完成(即添加 BUNDLE MI 以及輸入和輸出暫存器 MachineOperands)。這消除了將虛擬暫存器運算元添加到 BUNDLE 指令的需要,這將有效地使虛擬暫存器定義和使用列表加倍。Bundles 可以使用虛擬暫存器並以 SSA 形式形成,但不一定適用於所有用例。
「MC」層¶
MC 層用於在原始機器程式碼級別表示和處理程式碼,沒有「高階」資訊,如「常數池」、「跳轉表」、「全域變數」或類似的東西。在此級別上,LLVM 處理標籤名稱、機器指令和物件檔案中的區段等內容。這一層中的程式碼用於許多重要目的:程式碼產生器的尾部使用它來寫入 .s 或 .o 檔案,llvm-mc 工具也使用它來實現獨立的機器程式碼組譯器和反組譯器。
本節介紹一些重要的類別。還有一些重要的子系統在這一層交互作用,本手冊稍後將對其進行介紹。
MCStreamer
API¶
MCStreamer 最好被理解為一個組譯器 API。 它是一個以不同方式*實現*的抽象 API(例如,輸出 .s 檔案、輸出 ELF .o 檔案等),但其 API 直接對應於你在 .s 檔案中看到的內容。 MCStreamer 對應每個指令都有一個方法,例如 EmitLabel、EmitSymbolAttribute、switchSection、emitValue(用於 .byte、.word)等,它們直接對應於組譯層級的指令。 它還有一個 EmitInstruction 方法,用於將 MCInst 輸出到串流器。
這個 API 對兩個客戶端來說最重要:llvm-mc 獨立組譯器實際上是一個解析器,它解析一行,然後在 MCStreamer 上呼叫一個方法。 在程式碼產生器中,程式碼產生器的程式碼發射階段會將較高層級的 LLVM IR 和 Machine* 建構降低到 MC 層,並透過 MCStreamer 發射指令。
在 MCStreamer 的實現方面,有兩種主要的實現:一種用於寫出 .s 檔案 (MCAsmStreamer),另一種用於寫出 .o 檔案 (MCObjectStreamer)。 MCAsmStreamer 是一個簡單的實現,它為每個方法列印一個指令(例如 EmitValue -> .byte
),而 MCObjectStreamer 則實現了一個完整的組譯器。
對於目標特定的指令,MCStreamer 有一個 MCTargetStreamer 實例。 每個需要它的目標都定義一個繼承自它的類別,它很像 MCStreamer 本身:它對應每個指令都有一個方法,並且有兩個繼承自它的類別,一個目標物件串流器和一個目標組譯串流器。 目標組譯串流器只列印它 (emitFnStart -> .fnstart
),而物件串流器則為它實現組譯器邏輯。
要讓 llvm 使用這些類別,目標初始化必須呼叫 TargetRegistry::RegisterAsmStreamer 和 TargetRegistry::RegisterMCObjectStreamer,並傳遞回呼函式來配置相應的目標串流器,並將其傳遞給 createAsmStreamer 或適當的物件串流器建構函式。
MCContext
類別¶
MCContext 類別是 MC 層中各種唯一資料結構的擁有者,包括符號、區段等。 因此,這是您用來建立符號和區段的類別。 此類別不能被繼承。
MCSymbol
類別¶
MCSymbol 類別表示組譯檔案中的符號(又稱標籤)。 有兩種有趣的符號:組譯器暫存符號和一般符號。 組譯器暫存符號由組譯器使用和處理,但在產生物件檔案時會被丟棄。 這種區別通常透過在標籤前面加上前綴來表示,例如,在 MachO 中,“L”標籤是組譯器暫存標籤。
MCSymbols 由 MCContext 建立並在其中唯一化。 這表示可以比較 MCSymbols 的指標相等性,以確定它們是否是相同的符號。 請注意,指標不相等並不能保證標籤最終會位於不同的地址。 將如下內容輸出到 .s 檔案是完全合法的
foo:
bar:
.byte 4
在這種情況下,foo 和 bar 符號都將具有相同的地址。
MCSection
類別¶
「MCSection
」類別表示物件檔案特有的區段。它是由物件檔案特定的實作(例如 MCSectionMachO
、MCSectionCOFF
、MCSectionELF
)繼承而來,並由 MCContext 建立和唯一化。MCStreamer 具有當前區段的概念,可以使用 SwitchToSection 方法更改(這對應於 .s 檔案中的「.section」指令)。
「MCInst
」類別¶
「MCInst
」類別是指令的目標獨立表示。它是一個簡單的類別(比 MachineInstr 簡單得多),包含目標特定的操作碼和 MCOperand 向量。而 MCOperand 是一個簡單的 discriminated union,具有三種情況:1) 簡單的立即數,2) 目標暫存器 ID,3) 符號運算式(例如「Lfoo-Lbar+42
」)作為 MCExpr。
MCInst 是用於在 MC 層表示機器指令的通用貨幣。它是指令編碼器、指令印表機使用的類型,以及組譯器解析器和反組譯器產生的類型。
物件檔案格式¶
MC 層的物件寫入器支援各種物件格式。由於物件格式的目標特定方面,每個目標僅支援 MC 層支援的格式子集。大多數目標支援發出 ELF 物件。其他廠商特定的物件通常僅在該廠商支援的目標上支援(即 MachO 僅在 Darwin 支援的目標上支援,而 XCOFF 僅在支援 AIX 的目標上支援)。此外,某些目標具有自己的物件格式(即 DirectX、SPIR-V 和 WebAssembly)。
下表擷取了 LLVM 中物件檔案支援的快照
表 101 物件檔案格式¶ 格式
支援的目標
COFF
AArch64、ARM、X86
DXContainer
DirectX
ELF
AArch64、AMDGPU、ARM、AVR、BPF、CSKY、Hexagon、Lanai、LoongArch、M86k、MSP430、MIPS、PowerPC、RISCV、SPARC、SystemZ、VE、X86
GOFF
SystemZ
MachO
AArch64、ARM、X86
SPIR-V
SPIRV
WASM
WebAssembly
XCOFF
PowerPC
目標獨立程式碼產生演算法¶
本節說明 程式碼產生器的高階設計 中描述的階段。它解釋了它們的工作原理以及其設計背後的一些原理。
指令選擇¶
指令選擇是將呈現給程式碼產生器的 LLVM 程式碼轉換為目標特定機器指令的過程。文獻中有幾種眾所周知的方法可以做到這一點。LLVM 使用基於 SelectionDAG 的指令選擇器。
DAG 指令選擇器的部分內容是從目標描述(*.td
)檔案產生的。我們的目標是從這些 .td
檔案產生整個指令選擇器,儘管目前仍有一些事情需要自訂 C++ 程式碼。
GlobalISel 是另一個指令選擇框架。
SelectionDAG 簡介¶
SelectionDAG 提供了一種程式碼表示的抽象概念,可以使用自動化技術(例如基於動態規劃的最優模式匹配選擇器)來選擇指令。它也非常適合程式碼生成的其它階段;特別是指令排程(SelectionDAG 與排程 DAG 在選擇後非常接近)。此外,SelectionDAG 提供了一種主機表示形式,可以在其中執行各種非常低階別(但與目標無關)的優化;這些優化需要有關目標有效支援的指令的廣泛資訊。
SelectionDAG 是一個有向無環圖,其節點是 SDNode
類別的實例。 SDNode
的主要有效負載是其操作碼(Opcode),它指示節點執行的操作以及操作的操作數。各種操作節點類型在 include/llvm/CodeGen/ISDOpcodes.h
檔案的頂部描述。
雖然大多數操作定義單一值,但圖中的每個節點都可以定義多個值。例如,一個組合的 div/rem 操作將同時定義被除數和餘數。許多其他情況也需要多個值。每個節點還具有一定數量的操作數,這些操作數是指向定義所用值的節點的邊。因為節點可以定義多個值,所以邊由 SDValue
類別的實例表示,該類別是一個 <SDNode, unsigned>
對,分別指示正在使用的節點和結果值。 SDNode
生成的每個值都有一個關聯的 MVT
(機器值類型),指示值的類型。
SelectionDAG 包含兩種不同類型的值:表示資料流的值和表示控制流依賴關係的值。資料值是具有整數或浮點值類型的簡單邊。控制邊表示為「鏈」邊,其類型為 MVT::Other
。這些邊在具有副作用(例如載入、儲存、呼叫、返回等)的節點之間提供排序。所有具有副作用的節點都應將令牌鏈作為輸入,並產生一個新的令牌鏈作為輸出。按照慣例,令牌鏈輸入始終是操作數 #0,而鏈結果始終是操作產生的最後一個值。但是,在指令選擇之後,機器節點的鏈位於指令的操作數之後,並且可能後跟黏合節點。
SelectionDAG 具有指定的「入口」和「根」節點。入口節點始終是一個操作碼為 ISD::EntryToken
的標記節點。根節點是令牌鏈中的最後一個具有副作用的節點。例如,在單一基本塊函數中,它將是返回節點。
SelectionDAG 的一個重要概念是「合法」與「非法」DAG 的概念。目標的合法 DAG 是僅使用支援的操作和支援的類型的 DAG。例如,在 32 位 PowerPC 上,具有 i1、i8、i16 或 i64 類型值的 DAG 將是非法的,使用 SREM 或 UREM 操作的 DAG 也是如此。 合法化類型 和 合法化操作 階段負責將非法 DAG 轉換為合法 DAG。
SelectionDAG 指令選擇過程¶
基於 SelectionDAG 的指令選擇包括以下步驟
建構初始 DAG — 此階段會執行從輸入 LLVM 代碼到不合法 SelectionDAG 的簡單轉換。
最佳化 SelectionDAG — 此階段會對 SelectionDAG 執行簡單的最佳化以簡化它,並識別支援這些中繼操作的目標的中繼指令(如旋轉和
div
/rem
對)。這使得產生的代碼更有效率,並簡化了 從 DAG 選擇指令 階段(如下)。合法化 SelectionDAG 類型 — 此階段會轉換 SelectionDAG 節點以消除目標上不支援的任何類型。
最佳化 SelectionDAG — 執行 SelectionDAG 最佳化器以清理類型合法化所暴露的冗餘。
合法化 SelectionDAG 操作 — 此階段會轉換 SelectionDAG 節點以消除目標上不支援的任何操作。
最佳化 SelectionDAG — 執行 SelectionDAG 最佳化器以消除操作合法化引入的低效率。
從 DAG 選擇指令 — 最後,目標指令選擇器會將 DAG 操作與目標指令進行匹配。此過程會將目標獨立的輸入 DAG 轉換為另一個目標指令的 DAG。
SelectionDAG 排程和形成 — 最後階段會為目標指令 DAG 中的指令分配線性順序,並將其發射到正在編譯的 MachineFunction 中。此步驟使用傳統的預處理排程技術。
在完成所有這些步驟後,SelectionDAG 會被銷毀,並執行其餘的代碼生成過程。
調試這些步驟最常用的方法之一是使用 -debug-only=isel
,它會在每個步驟之後列印出 DAG 以及其他信息,如調試信息。或者,-debug-only=isel-dump
只顯示 DAG 轉儲,但可以使用 -filter-print-funcs=<函數名稱>
按函數名稱過濾結果。
將這裡發生的事情視覺化的一個好方法是利用一些 LLC 命令行選項。以下選項會彈出一個窗口,在特定時間顯示 SelectionDAG(如果在使用此選項時只在控制台中看到錯誤打印出來,則可能 需要配置系統 以添加對它的支持)。
-view-dag-combine1-dags
在構建 DAG 之後、第一次最佳化過程之前顯示 DAG。-view-legalize-dags
在合法化之前顯示 DAG。-view-dag-combine2-dags
在第二次最佳化過程之前顯示 DAG。-view-isel-dags
在選擇階段之前顯示 DAG。-view-sched-dags
在排程之前顯示 DAG。
-view-sunit-dags
顯示排程器的依賴關係圖。此圖表基於最終的 SelectionDAG,必須一起排程的節點捆綁到單個排程單元節點中,並省略了立即操作數和其他與排程無關的節點。
選項 -filter-view-dags
允許選擇您感興趣的可視化基本塊的名稱,並過濾所有先前的 view-*-dags
選項。
初始 SelectionDAG 建構¶
初始 SelectionDAG 是由 SelectionDAGBuilder
類別從 LLVM 輸入進行簡單的窺孔展開而來。這個步驟的目的是盡可能地將低階、目標特定的細節暴露給 SelectionDAG。這個步驟大多是硬編碼的(例如,LLVM add
會變成 SDNode add
,而 getelementptr
會展開成明顯的算術運算)。這個步驟需要目標特定的鉤子來降低呼叫、返回、可變參數等的層級。對於這些功能,將使用 目標降低 介面。
SelectionDAG 合法化類型階段¶
合法化階段負責將 DAG 轉換為僅使用目標原生支援的類型。
將不受支援的純量類型值轉換為受支援的類型值主要有兩種方法:將小類型轉換為大類型(「提升」),以及將大整數類型分解為較小的類型(「擴展」)。例如,目標可能要求將所有 f32 值提升為 f64,並將所有 i1/i8/i16 值提升為 i32。相同的目標可能要求將所有 i64 值擴展為 i32 值對。這些更改可以根據需要插入符號和零擴展,以確保最終程式碼與輸入具有相同的行為。
將不受支援的向量類型值轉換為受支援的類型值主要有兩種方法:拆分向量類型(如果需要可以多次拆分),直到找到合法的類型,以及通過在末尾添加元素來擴展向量類型,以將其擴展為合法的類型(「擴充」)。如果向量一路拆分到單元素部分,並且沒有找到受支援的向量類型,則元素將被轉換為純量(「純量化」)。
目標實作通過在其 TargetLowering
建構函數中呼叫 addRegisterClass
方法來告知合法化器哪些類型受支援(以及為其使用哪個暫存器類別)。
SelectionDAG 合法化階段¶
合法化階段負責將 DAG 轉換為僅使用目標原生支援的操作。
目標通常有一些奇怪的限制,例如不支援對每個受支援的資料類型執行所有操作(例如,X86 不支援位元組條件移動,PowerPC 不支援從 16 位元記憶體位置進行符號擴展載入)。合法化通過以下方式處理這些問題:通過開放編碼另一系列操作來模擬操作(「擴展」),通過將一種類型提升為支援操作的更大類型(「提升」),或者通過使用目標特定的鉤子來實作合法化(「自訂」)。
目標實作通過在其 TargetLowering
建構函數中呼叫 setOperationAction
方法來告知合法化器哪些操作不受支援(以及要採取上述哪三種操作)。
如果目標具有合法的向量類型,則預計它會使用這些類型為 shufflevector IR 指令的常見形式產生高效的機器碼。這可能需要對從 shufflevector IR 建立的 SelectionDAG 向量操作進行自訂合法化。應該處理的 shufflevector 形式包括
向量選擇 — 向量的每個元素都是從兩個輸入向量中對應的元素中選取的。這個操作在目標組合語言中也稱為「混合」或「位元選擇」。這種洗牌類型會直接映射到
shuffle_vector
SelectionDAG 節點。插入子向量 — 從索引 0 開始將向量放入較長的向量類型中。這種洗牌類型會直接映射到
insert_subvector
SelectionDAG 節點,並且index
運算元設為 0。提取子向量 — 從索引 0 開始從較長的向量類型中提取向量。這種洗牌類型會直接映射到
extract_subvector
SelectionDAG 節點,並且index
運算元設為 0。splat — 向量的所有元素都具有相同的純量元素。這個操作在目標組合語言中也稱為「廣播」或「複製」。shufflevector IR 指令可能會變更向量長度,因此這個操作可能會映射到多個 SelectionDAG 節點,包括
shuffle_vector
、concat_vectors
、insert_subvector
和extract_subvector
。
在 Legalize 階段出現之前,我們要求每個目標 選擇器 都要支援並處理每個運算子和類型,即使它們本身不支援。Legalize 階段的引入讓所有正規化模式都能在目標之間共用,並且因為它仍然採用 DAG 的形式,因此可以輕鬆最佳化正規化程式碼。
SelectionDAG 最佳化階段:DAG 組合器¶
SelectionDAG 最佳化階段會在程式碼產生過程中執行多次,在建構 DAG 之後以及每次合法化之後都會執行一次。階段的第一次執行允許清除初始程式碼(例如執行取決於知道運算子具有受限類型輸入的最佳化)。階段的後續執行會清除 Legalize 階段產生的雜亂程式碼,這讓 Legalize 可以非常簡單(它可以專注於讓程式碼合法化,而不是專注於產生「良好」且合法的程式碼)。
執行的一種重要最佳化類別是最佳化插入的符號和零擴充指令。我們目前使用臨時技術,但將來可能會改用更嚴謹的技術。以下是一些關於這個主題的好文章
「加寬整數算術」
Kevin Redwine 和 Norman Ramsey
2004 年國際編譯器建構會議 (CC)
「有效的符號擴充消除」
Motohiro Kawahito、Hideaki Komatsu 和 Toshio Nakatani
ACM SIGPLAN 2002 年程式語言設計與實作會議論文集。
SelectionDAG 選擇階段¶
選擇階段是指令選擇中大部分目標特定程式碼的部分。這個階段會將合法的 SelectionDAG 作為輸入,將目標支援的指令與此 DAG 進行模式匹配,並產生新的目標程式碼 DAG。例如,請考慮以下 LLVM 片段
%t1 = fadd float %W, %X
%t2 = fmul float %t1, %Y
%t3 = fadd float %t2, %Z
此 LLVM 程式碼對應於 SelectionDAG,其外觀基本上如下所示
(fadd:f32 (fmul:f32 (fadd:f32 W, X), Y), Z)
如果目標支援浮點乘加 (FMA) 運算,則其中一個加法可以與乘法合併。例如,在 PowerPC 上,指令選擇器的輸出可能看起來像這個 DAG
(FMADDS (FADDS W, X), Y, Z)
FMADDS
指令是一種三元指令,它會將前兩個運算元相乘,然後加上第三個運算元(以單精度浮點數表示)。FADDS
指令是一種簡單的二元單精度加法指令。為了執行此模式匹配,PowerPC 後端包含以下指令定義
def FMADDS : AForm_1<59, 29,
(ops F4RC:$FRT, F4RC:$FRA, F4RC:$FRC, F4RC:$FRB),
"fmadds $FRT, $FRA, $FRC, $FRB",
[(set F4RC:$FRT, (fadd (fmul F4RC:$FRA, F4RC:$FRC),
F4RC:$FRB))]>;
def FADDS : AForm_2<59, 21,
(ops F4RC:$FRT, F4RC:$FRA, F4RC:$FRB),
"fadds $FRT, $FRA, $FRB",
[(set F4RC:$FRT, (fadd F4RC:$FRA, F4RC:$FRB))]>;
指令定義中突出顯示的部分表示用於匹配指令的模式。DAG 運算符(例如 fmul
/fadd
)定義在 include/llvm/Target/TargetSelectionDAG.td
檔案中。「F4RC
」是輸入和結果值的暫存器類別。
TableGen DAG 指令選擇器產生器會讀取 .td
檔案中的指令模式,並自動為您的目標建置模式匹配程式碼的部分內容。它具有以下優點
在編譯器編譯時,它會分析您的指令模式,並告訴您模式是否有意義。
它可以處理模式匹配運算元的任意約束。特別是,它可以直接說明「匹配任何 13 位元符號擴充值的立即數」之類的內容。例如,請參閱 PowerPC 後端中的
immSExt16
和相關的tblgen
類別。它知道已定義模式的幾個重要識別。例如,它知道加法是可交換的,因此它允許上述
FMADDS
模式匹配「(fadd X, (fmul Y, Z))
」以及「(fadd (fmul X, Y), Z)
」,而目標作者不必特別處理這種情況。它具有功能齊全的類型推論系統。特別是,您應該很少需要明確告訴系統模式部分是什麼類型。在上述
FMADDS
案例中,我們不必告訴tblgen
模式中的所有節點都是 'f32' 類型。它能夠從F4RC
具有 'f32' 類型的事實推斷和傳播此知識。目標可以定義自己的(並依賴內置的)「模式片段」。模式片段是可重複使用的模式塊,在編譯器編譯時會內嵌到您的模式中。例如,整數「
(not x)
」運算實際上被定義為擴展為「(xor x, -1)
」的模式片段,因為 SelectionDAG 沒有原生「not
」運算。目標可以根據需要定義自己的速記片段。有關範例,請參閱「not
」和「ineg
」的定義。除了指令之外,目標還可以指定使用「Pat」類別映射到一個或多個指令的任意模式。例如,PowerPC 無法透過一條指令將任意整數立即數載入暫存器。為了告訴 tblgen 如何做到這一點,它定義了
// Arbitrary immediate support. Implement in terms of LIS/ORI. def : Pat<(i32 imm:$imm), (ORI (LIS (HI16 imm:$imm)), (LO16 imm:$imm))>;
如果沒有任何單指令模式符合將立即數載入暫存器,則將使用此模式。此規則表示「匹配任意 i32 立即數,將其轉換為
ORI
(「或 16 位元立即數」)和LIS
(「載入 16 位元立即數,其中立即數左移 16 位元」)指令」。為了使其運作,使用LO16
/HI16
節點轉換來操作輸入立即數(在此情況下,採用立即數的高 16 位元或低 16 位元)。當使用「Pat」類別將模式映射到具有一個或多個複雜運算元(例如 X86 位址模式)的指令時,模式可以使用
ComplexPattern
指定整個運算元,或者可以單獨指定複雜運算元的組成部分。後者例如由 PowerPC 後端針對預先遞增指令完成def STWU : DForm_1<37, (outs ptr_rc:$ea_res), (ins GPRC:$rS, memri:$dst), "stwu $rS, $dst", LdStStoreUpd, []>, RegConstraint<"$dst.reg = $ea_res">, NoEncode<"$ea_res">; def : Pat<(pre_store GPRC:$rS, ptr_rc:$ptrreg, iaddroff:$ptroff), (STWU GPRC:$rS, iaddroff:$ptroff, ptr_rc:$ptrreg)>;
在這裡,
ptroff
和ptrreg
運算元對被匹配到STWU
指令中memri
類別的複雜運算元dst
。雖然系統確實自動執行了很多操作,但如果有些東西難以表達,它仍然允許您編寫自訂 C++ 程式碼來匹配特殊情況。
雖然該系統有很多優點,但目前仍有一些限制,主要原因是它仍在開發中,尚未完成
總體而言,沒有辦法定義或匹配定義多個值的 SelectionDAG 節點(例如
SMUL_LOHI
、LOAD
、CALL
等)。這是您目前仍然*必須*為您的指令選擇器編寫自訂 C++ 程式碼的最大原因。目前還沒有很好的方法來支援匹配複雜的位址模式。未來,我們將擴展模式片段,讓它們能夠定義多個值(例如 X86 位址模式 的四個運算元,目前使用自訂 C++ 程式碼匹配)。此外,我們將擴展片段,以便一個片段可以匹配多個不同的模式。
我們還沒有自動推斷
isStore
/isLoad
等標記。我們還沒有為 合法化器 自動生成支援的暫存器和操作集。
我們還沒有辦法綁定自訂的合法化節點。
儘管有這些限制,但指令選擇器產生器對於典型指令集中的大多數二元和邏輯運算仍然非常有用。如果您遇到任何問題或不知道如何做某事,請告訴 Chris!
SelectionDAG 排程和形成階段¶
排程階段從選擇階段獲取目標指令的 DAG 並分配順序。排程器可以根據機器的各種約束選擇順序(即最小化暫存器壓力的順序或嘗試覆蓋指令延遲)。一旦建立了順序,DAG 就會轉換為 列表 MachineInstrs 並且 SelectionDAG 被銷毀。
請注意,此階段在邏輯上與指令選擇階段是分開的,但在程式碼中與之緊密相連,因為它在 SelectionDAG 上操作。
SelectionDAG 的未來方向¶
可選的每次函數選擇。
從
.td
檔案自動產生整個選擇器。
基於 SSA 的機器碼優化¶
待撰寫
活動區間¶
活動區間是指變數處於「活動」狀態的範圍(區間)。它們被一些 暫存器分配器 pass 用於確定兩個或多個需要相同實體暫存器的虛擬暫存器是否在程式中的同一點處於活動狀態(即它們是否衝突)。當這種情況發生時,必須「溢出」一個虛擬暫存器。
活動變數分析¶
確定變數活動區間的第一步是計算指令後立即失效的暫存器集合(即指令計算值,但從未使用)和指令使用的暫存器集合,但在指令之後從未使用(即它們被終止)。活動變數資訊是針對函數中的每個「虛擬」暫存器和「可分配暫存器」的實體暫存器計算的。這是以一種非常有效的方式完成的,因為它使用 SSA 來稀疏地計算虛擬暫存器(採用 SSA 形式)的生命週期資訊,並且只需要在區塊內追蹤實體暫存器。在暫存器分配之前,LLVM 可以假設實體暫存器僅在單個基本區塊內處於活動狀態。這使得它可以進行單一的局部分析,以解析每個基本區塊內的實體暫存器生命週期。如果實體暫存器不可分配暫存器(例如堆疊指標或條件碼),則不會追蹤它。
實體暫存器可能在函數中處於活動狀態或非活動狀態。活動輸入值通常是暫存器中的參數。活動輸出值通常是暫存器中的返回值。活動輸入值會被標記,並在活動區間分析期間獲得一個虛擬的「定義」指令。如果函數的最後一個基本區塊是 return
,則它會被標記為使用函數中的所有活動輸出值。
PHI
節點需要特殊處理,因為從函數 CFG 的深度優先遍歷計算活動變數資訊並不能保證在使用 PHI
節點使用的虛擬暫存器之前定義它。當遇到 PHI
節點時,只處理定義,因為使用將在其他基本區塊中處理。
對於當前基本區塊的每個 PHI
節點,我們在當前基本區塊的末尾模擬一個賦值,並遍歷後繼基本區塊。如果後繼基本區塊有一個 PHI
節點,並且其中一個 PHI
節點的操作數來自當前基本區塊,則該變數在當前基本區塊及其所有先前基本區塊中都被標記為「活動」,直到遇到具有定義指令的基本區塊為止。
活動區間分析¶
我們現在擁有執行活動區間分析和構建活動區間本身所需的資訊。我們首先對基本區塊和機器指令進行編號。然後,我們處理「活動輸入」值。這些值位於實體暫存器中,因此假設實體暫存器在基本區塊結束時被終結。虛擬暫存器的活動區間是針對機器指令 [1, N]
的某個順序計算的。活動區間是一個區間 [i, j)
,其中 1 >= i >= j > N
,變數在該區間內處於活動狀態。
注意
更多內容即將推出…
暫存器分配¶
「暫存器分配問題」在於將可以使用無限數量虛擬暫存器的程式 Pv 對應到包含有限(可能很小)數量實體暫存器的程式 Pp。每個目標架構都有不同數量的實體暫存器。如果實體暫存器的數量不足以容納所有虛擬暫存器,則其中一些虛擬暫存器必須對應到記憶體中。這些虛擬變數稱為「溢出虛擬變數」。
LLVM 中暫存器的表示方式¶
在 LLVM 中,實體暫存器由整數表示,通常範圍從 1 到 1023。若要查看特定架構如何定義此編號,您可以閱讀該架構的 GenRegisterNames.inc
檔案。例如,通過檢查 lib/Target/X86/X86GenRegisterInfo.inc
,我們可以看到 32 位元暫存器 EAX
由 43 表示,而 MMX 暫存器 MM0
則對應到 65。
某些架構包含共享相同實體位置的暫存器。一個顯著的例子是 X86 平台。例如,在 X86 架構中,暫存器 EAX
、AX
和 AL
共享前八個位元。這些實體暫存器在 LLVM 中被標記為「別名」。給定特定架構,您可以通過檢查其 RegisterInfo.td
檔案來檢查哪些暫存器是別名。此外,MCRegAliasIterator
類別會列舉與某個暫存器別名的所有實體暫存器。
在 LLVM 中,實體暫存器被分組為「暫存器類別」。屬於同一暫存器類別的元素在功能上是等效的,並且可以互換使用。每個虛擬暫存器只能對應到特定類別的實體暫存器。例如,在 X86 架構中,某些虛擬變數只能分配給 8 位元暫存器。暫存器類別由 TargetRegisterClass
物件描述。若要瞭解虛擬暫存器是否與給定的實體暫存器相容,可以使用以下程式碼
bool RegMapping_Fer::compatible_class(MachineFunction &mf,
unsigned v_reg,
unsigned p_reg) {
assert(TargetRegisterInfo::isPhysicalRegister(p_reg) &&
"Target register must be physical");
const TargetRegisterClass *trc = mf.getRegInfo().getRegClass(v_reg);
return trc->contains(p_reg);
}
有時,主要是為了除錯,更改目標架構中可用的物理暫存器數量會很有用。 這必須在 TargetRegisterInfo.td
檔案中靜態完成。 只需 grep
搜尋 RegisterClass
,其最後一個參數是暫存器列表。 將其中一些註釋掉是一種避免使用它們的簡單方法。 更禮貌的方法是明確地從*分配順序*中排除某些暫存器。 請參閱 lib/Target/X86/X86RegisterInfo.td
中 GR8
暫存器類別的定義,以獲取此示例。
虛擬暫存器也由整數表示。 與物理暫存器相反,不同的虛擬暫存器永遠不會共享相同的編號。 物理暫存器在 TargetRegisterInfo.td
檔案中靜態定義,並且無法由應用程式開發人員建立,而虛擬暫存器則不然。 若要建立新的虛擬暫存器,請使用 MachineRegisterInfo::createVirtualRegister()
方法。 此方法將返回一個新的虛擬暫存器。 使用 IndexedMap<Foo, VirtReg2IndexFunctor>
來保存每個虛擬暫存器的訊息。 如果您需要枚舉所有虛擬暫存器,請使用函數 TargetRegisterInfo::index2VirtReg()
來查找虛擬暫存器編號。
for (unsigned i = 0, e = MRI->getNumVirtRegs(); i != e; ++i) {
unsigned VirtReg = TargetRegisterInfo::index2VirtReg(i);
stuff(VirtReg);
}
在暫存器分配之前,指令的操作數主要是虛擬暫存器,儘管也可以使用物理暫存器。 若要檢查給定的機器操作數是否為暫存器,請使用布林函數 MachineOperand::isRegister()
。 若要獲取暫存器的整數代碼,請使用 MachineOperand::getReg()
。 一條指令可以定義或使用一個暫存器。 例如,ADD reg:1026 := reg:1025 reg:1024
定義了暫存器 1024,並使用了暫存器 1025 和 1026。 給定一個暫存器操作數,方法 MachineOperand::isUse()
會告知該暫存器是否正在被指令使用。 方法 MachineOperand::isDef()
會告知該暫存器是否正在被定義。
我們將在暫存器分配之前將 LLVM 位元碼中存在的物理暫存器稱為*預先著色暫存器*。 預先著色暫存器用於許多不同的情況,例如,傳遞函數呼叫的參數以及儲存特定指令的結果。 預先著色暫存器有兩種類型:*隱式*定義的和*顯式*定義的。 顯式定義的暫存器是普通操作數,可以使用 MachineInstr::getOperand(int)::getReg()
訪問。 若要檢查哪些暫存器是由指令隱式定義的,請使用 TargetInstrInfo::get(opcode)::ImplicitDefs
,其中 opcode
是目標指令的操作碼。 顯式和隱式物理暫存器之間的一個重要區別是,後者是針對每條指令靜態定義的,而前者可能會根據正在編譯的程序而有所不同。 例如,表示函數呼叫的指令將始終隱式定義或使用同一組物理暫存器。 若要讀取指令隱式使用的暫存器,請使用 TargetInstrInfo::get(opcode)::ImplicitUses
。 預先著色暫存器對任何暫存器分配演算法都施加了約束。 暫存器分配器必須確保在虛擬暫存器仍然有效時,它們都不會被虛擬暫存器的值覆蓋。
將虛擬暫存器映射至實體暫存器¶
有兩種方式可以將虛擬暫存器映射至實體暫存器(或記憶體位置)。第一種方式,我們稱之為*直接映射*,是基於使用 TargetRegisterInfo
和 MachineOperand
類別的方法。第二種方式,我們稱之為*間接映射*,依賴於 VirtRegMap
類別來插入載入和儲存指令,以在記憶體中傳送和取得值。
直接映射為暫存器配置器的開發人員提供了更大的靈活性;然而,它更容易出錯,並且需要更多的實作工作。基本上,程式設計師必須指定在目標函數中應該插入載入和儲存指令的位置,以便在記憶體中取得和儲存值。要將實體暫存器分配給指定運算元中的虛擬暫存器,請使用 MachineOperand::setReg(p_reg)
。要插入儲存指令,請使用 TargetInstrInfo::storeRegToStackSlot(...)
,要插入載入指令,請使用 TargetInstrInfo::loadRegFromStackSlot
。
間接映射使應用程式開發人員免於插入載入和儲存指令的複雜性。要將虛擬暫存器映射至實體暫存器,請使用 VirtRegMap::assignVirt2Phys(vreg, preg)
。要將特定虛擬暫存器映射至記憶體,請使用 VirtRegMap::assignVirt2StackSlot(vreg)
。此方法將返回 vreg
值所在的堆疊位置。如果需要將另一個虛擬暫存器映射至相同的堆疊位置,請使用 VirtRegMap::assignVirt2StackSlot(vreg, stack_location)
。使用間接映射時需要注意的一點是,即使虛擬暫存器映射至記憶體,它仍然需要映射至實體暫存器。這個實體暫存器是虛擬暫存器在被儲存之前或重新載入之後應該存在的位置。
如果使用間接策略,在所有虛擬暫存器都被映射至實體暫存器或堆疊位置之後,需要使用溢出器物件在程式碼中放置載入和儲存指令。每個已映射至堆疊位置的虛擬機將在定義後儲存至記憶體,並在使用前載入。溢出器的實作會嘗試回收載入/儲存指令,避免不必要的指令。有關如何呼叫溢出器的範例,請參閱 lib/CodeGen/RegAllocLinearScan.cpp
中的 RegAllocLinearScan::runOnMachineFunction
。
處理雙地址指令¶
除了極少數例外(例如,函數呼叫),LLVM 機器碼指令都是三地址指令。也就是說,預期每個指令最多定義一個暫存器,最多使用兩個暫存器。然而,有些架構使用雙地址指令。在這種情況下,定義的暫存器也是使用的暫存器之一。例如,在 X86 中,像 ADD %EAX, %EBX
這樣的指令實際上等同於 %EAX = %EAX + %EBX
。
為了產生正確的程式碼,LLVM 必須將表示兩個地址指令的三地址指令轉換為真正的兩個地址指令。LLVM 為此目的提供了 TwoAddressInstructionPass
Pass。它必須在暫存器配置之前執行。執行後,產生的程式碼可能不再採用 SSA 格式。例如,當 %a = ADD %b %c
等指令轉換為以下兩個指令時,就會發生這種情況:
%a = MOVE %b
%a = ADD %a %c
請注意,在內部,第二個指令表示為 ADD %a[def/use] %c
。也就是說,暫存器運算元 %a
同時由指令使用和定義。
SSA 解構階段¶
暫存器配置過程中的一個重要轉換稱為「SSA 解構階段」。SSA 格式簡化了對程式控制流程圖執行的許多分析。然而,傳統的指令集沒有實現 PHI 指令。因此,為了產生可執行的程式碼,編譯器必須用保留其語義的其他指令替換 PHI 指令。
有許多方法可以安全地從目標程式碼中移除 PHI 指令。最傳統的 PHI 解構演算法使用複製指令替換 PHI 指令。這是 LLVM 採用的策略。SSA 解構演算法在 lib/CodeGen/PHIElimination.cpp
中實現。為了呼叫此 Pass,必須在暫存器配置器的程式碼中將識別碼 PHIEliminationID
標記為必需。
指令摺疊¶
「指令摺疊」是在暫存器配置過程中執行的優化,用於移除不必要的複製指令。例如,像這樣的指令序列
%EBX = LOAD %mem_address
%EAX = COPY %EBX
可以安全地替換為單個指令
%EAX = LOAD %mem_address
可以使用 TargetRegisterInfo::foldMemoryOperand(...)
方法摺疊指令。摺疊指令時必須小心;摺疊後的指令可能與原始指令有很大不同。有關其使用範例,請參閱 lib/CodeGen/LiveIntervalAnalysis.cpp
中的 LiveIntervals::addIntervalsForSpills
。
內建暫存器配置器¶
LLVM 基礎架構為應用程式開發人員提供了三種不同的暫存器配置器
「快速」—— 此暫存器配置器是偵錯組建的預設配置器。它在基本塊級別配置暫存器,嘗試將值保留在暫存器中並適當地重複使用暫存器。
「基本」—— 這是一種遞增的暫存器配置方法。活動範圍按啟發式驅動的順序一次分配給一個暫存器。由於程式碼可以在配置期間即時重寫,因此此架構允許開發有趣的配置器作為擴展。它本身不是生產暫存器配置器,但對於分類錯誤和作為性能基準而言,它是一種潛在有用的獨立模式。
「貪婪」——「預設配置器」。這是「基本」配置器的高度調整實現,它包含全域活動範圍拆分。此配置器努力將溢出程式碼的成本降至最低。
PBQP — 一種基於分區布林二次規劃 (PBQP) 的暫存器分配器。此分配器透過建構一個表示正在考慮的暫存器分配問題的 PBQP 問題、使用 PBQP 解法器解決此問題,以及將解映射回暫存器分配來運作。
在 llc
中使用的暫存器分配器類型可以使用命令列選項 -regalloc=...
來選擇。
$ llc -regalloc=linearscan file.bc -o ln.s
$ llc -regalloc=fast file.bc -o fa.s
$ llc -regalloc=pbqp file.bc -o pbqp.s
前言/結尾代碼插入¶
注意
待撰寫
精簡 Unwind 資訊¶
拋出異常需要從函數中進行 *Unwinding* 操作。關於如何對給定函數進行 Unwinding 的資訊傳統上以 DWARF Unwind(又稱框架)資訊表示。但該格式最初是為偵錯器的回溯而開發的,並且每個框架描述條目 (FDE) 每个函數需要約 20-30 位元組。還需要在執行時將函數中的地址映射到相應 FDE 的成本。另一種 Unwind 編碼稱為 *精簡 Unwind 資訊*,每个函數僅需 4 位元組。
精簡 Unwind 資訊編碼是一個 32 位元值,以特定於架構的方式編碼。它指定要恢復哪些暫存器以及從哪裡恢復,以及如何從函數中 Unwind。當連結器建立最終連結映像時,它將建立一個 __TEXT,__unwind_info
區段。此區段是執行時存取任何給定函數的 Unwind 資訊的一種小巧快速的途徑。如果我們為函數發出精簡 Unwind 資訊,則該精簡 Unwind 資訊將在 __TEXT,__unwind_info
區段中編碼。如果我們發出 DWARF Unwind 資訊,則 __TEXT,__unwind_info
區段將包含最終連結映像中 __TEXT,__eh_frame
區段中 FDE 的偏移量。
對於 X86,精簡 Unwind 資訊編碼有三種模式
- 具有框架指標(``EBP`` 或 ``RBP``)的函數
基於
EBP/RBP
的框架,其中EBP/RBP
在返回地址後立即被推入堆疊,然後將ESP/RSP
移動到EBP/RBP
。因此,要進行 Unwind,使用當前的EBP/RBP
值恢復ESP/RSP
,然後透過彈出堆疊來恢復EBP/RBP
,並透過將堆疊彈出到 PC 中一次來完成返回。所有需要恢復的非常態暫存器都必須保存在堆疊上的一個小範圍內,該範圍從EBP-4
到EBP-1020
(RBP-8
到RBP-1020
)。偏移量(在 32 位元模式下除以 4,在 64 位元模式下除以 8)在第 16-23 位元中編碼(遮罩:0x00FF0000
)。保存的暫存器在第 0-14 位元中編碼(遮罩:0x00007FFF
),作為下表中的五個 3 位元條目精簡數字
i386 暫存器
x86-64 暫存器
1
EBX
RBX
2
ECX
R12
3
EDX
R13
4
EDI
R14
5
ESI
R15
6
EBP
RBP
- 無框架且具有小的固定堆疊大小(``EBP`` 或 ``RBP`` 不做為框架指標使用)
若要返回,則將一個常數(在精簡展開編碼中編碼)添加到
ESP/RSP
。然後透過將堆疊彈出到 PC 中來完成返回。所有需要恢復的非易失性暫存器必須在返回地址之後立即保存在堆疊上。堆疊大小(在 32 位元模式下除以 4,在 64 位元模式下除以 8)以位元 16-23 編碼(遮罩:0x00FF0000
)。在 32 位元模式下,最大堆疊大小為 1024 位元組,在 64 位元模式下為 2048 位元組。已儲存的暫存器數量以位元 9-12 編碼(遮罩:0x00001C00
)。位元 0-9(遮罩:0x000003FF
)包含已儲存的暫存器及其順序。(如需編碼演算法,請參閱lib/Target/X86FrameLowering.cpp
中的encodeCompactUnwindRegistersWithoutFrame()
函數。)- 無框架且具有大的固定堆疊大小(``EBP`` 或 ``RBP`` 不做為框架指標使用)
這種情況類似於「無框架且具有小的固定堆疊大小」的情況,但堆疊大小太大而無法在精簡展開編碼中編碼。相反,它要求函數在其序言中包含「
subl $nnnnnn, %esp
」。精簡編碼包含函數中$nnnnnn
值的偏移量,位於位元 9-12(遮罩:0x00001C00
)。
延遲機器碼最佳化¶
注意
待撰寫
程式碼發射¶
程式碼生成的程式碼發射步驟負責從程式碼生成器抽象概念(如 MachineFunction、MachineInstr 等)降低到 MC 層使用的抽象概念(MCInst、MCStreamer 等)。這是透過結合幾個不同的類別來完成的:目標獨立的 AsmPrinter 類別、AsmPrinter 的目標特定子類別(例如 SparcAsmPrinter)和 TargetLoweringObjectFile 類別。
由於 MC 層在物件檔案的抽象層級運作,因此它沒有函數、全域變數等的觀念。相反,它考慮的是標籤、指令和指令。此時使用的一個關鍵類別是 MCStreamer 類別。這是一個以不同方式實現的抽象 API(例如,輸出 .s 檔案、輸出 ELF .o 檔案等),它實際上是一個「組譯器 API」。MCStreamer 對每個指令都有一個方法,例如 EmitLabel、EmitSymbolAttribute、switchSection 等,它們直接對應於組譯層級的指令。
如果您有興趣為目標實現程式碼生成器,則必須為您的目標實現三個重要事項
首先,您需要為您的目標建立 AsmPrinter 的子類別。此類別實現了將 MachineFunction 轉換為 MC 標籤構造的一般降低過程。AsmPrinter 基礎類別提供了一些有用的方法和常式,並且還允許您以一些重要方式覆蓋降低過程。如果您正在實現 ELF、COFF 或 MachO 目標,則應該可以免費獲得大部分的降低,因為 TargetLoweringObjectFile 類別實現了許多常見的邏輯。
第二,您需要為目標實作指令印表機。指令印表機會取得 MCInst 並將其轉譯為 raw_ostream 的文字格式。大多數指令會從 .td 檔案自動產生(當您在指令中指定類似「
add $dst, $src1, $src2
」的內容時),但您需要實作常式來列印運算元。第三,您需要實作將 MachineInstr 降級為 MCInst 的程式碼,這通常在「<target>MCInstLower.cpp」中實作。這個降級程序通常是目標特定的,並且負責將跳轉表項目、常數池索引、全域變數位址等適當地轉換為 MCLabel。這個轉譯層也負責將程式碼產生器使用的虛擬運算碼展開為其對應的實際機器指令。由此產生的 MCInst 會被送入指令印表機或編碼器。
最後,您可以選擇性地實作 MCCodeEmitter 的子類別,將 MCInst 降級為機器碼位元組和重定位資訊。如果您想支援直接產生 .o 檔案,或是想為目標實作組譯器,這一點就很重要。
發出函式堆疊大小資訊¶
當 TargetLoweringObjectFile::StackSizesSection
不為空,且 TargetOptions::EmitStackSizeSection
已設定 (-stack-size-section) 時,將會發出一個包含函式堆疊大小中繼資料的區段。該區段將包含函式符號值(指標大小)和堆疊大小(無符號 LEB128)的成對陣列。堆疊大小值僅包含在函式序言中配置的空間。具有動態堆疊配置的函式不包含在內。
VLIW 封包器¶
在超長指令字組 (VLIW) 架構中,編譯器負責將指令映射到架構上可用的功能單元。為此,編譯器會建立稱為「封包」或「叢集」的指令群組。LLVM 中的 VLIW 封包器是一種與目標無關的機制,用於啟用機器指令的封包化。
從指令映射到功能單元¶
VLIW 目標中的指令通常可以映射到多個功能單元。在封包化過程中,編譯器必須能夠判斷指令是否可以添加到封包中。這個決定可能很複雜,因為編譯器必須檢查指令到功能單元的所有可能映射。因此,為了降低編譯時間的複雜度,VLIW 封包器會解析目標的指令類別,並在編譯時產生表格。然後,提供的機器無關 API 可以查詢這些表格,以確定指令是否可以容納在封包中。
如何產生和使用封包化表格¶
封包器會從目標行程表中讀取指令類別,並建立確定性有限狀態機 (DFA) 來表示封包的狀態。 DFA 由三個主要元素組成:輸入、狀態和轉移。 生成的 DFA 的輸入集表示要新增至封包的指令。 狀態表示封包中指令可能消耗的功能單元。 在 DFA 中,從一種狀態到另一種狀態的轉移會在將指令新增至現有封包時發生。 如果功能單元與指令之間存在合法映射,則 DFA 會包含對應的轉移。 缺少轉移表示不存在合法映射,並且無法將指令新增至封包。
若要為 VLIW 目標產生表格,請將 *Target*GenDFAPacketizer.inc 作為目標新增至目標目錄中的 Makefile。 匯出的 API 提供三個函數:DFAPacketizer::clearResources()
、DFAPacketizer::reserveResources(MachineInstr *MI)
和 DFAPacketizer::canReserveResources(MachineInstr *MI)
。 這些函數允許目標封包器將指令新增至現有封包,以及檢查是否可以將指令新增至封包。 如需詳細資訊,請參閱 llvm/CodeGen/DFAPacketizer.h
。
實作原生組譯器¶
雖然您閱讀本文可能是因為您想編寫或維護編譯器後端,但 LLVM 也完全支援建置原生組譯器。 我們已盡力自動化從 .td 檔案產生組譯器(尤其是指令語法和編碼),這表示手動和重複的資料輸入大部分可以與編譯器進行分解和共用。
指令解析¶
注意
待撰寫
指令別名處理¶
指令解析完成後,它會進入 MatchInstructionImpl 函數。 MatchInstructionImpl 函數會執行別名處理,然後執行實際比對。
別名處理是將相同指令的不同詞彙形式規範化為一種表示形式的階段。 可以實作幾種不同類型的別名,它們按照處理順序列在下方(從最簡單/最弱到最複雜/最強大)。 一般來說,您會想要使用符合指令需求的第一個別名機制,因為它允許更簡潔的描述。
助記符別名¶
別名處理的第一階段是針對允許使用兩個不同助記符的指令類別進行簡單的指令助記符重新映射。 這個階段是從一個輸入助記符到一個輸出助記符的簡單且無條件的重新映射。 此種形式的別名無法查看運算元,因此重新映射必須適用於給定助記符的所有形式。 助記符別名的定義很簡單,例如 X86 具有
def : MnemonicAlias<"cbw", "cbtw">;
def : MnemonicAlias<"smovq", "movsq">;
def : MnemonicAlias<"fldcww", "fldcw">;
def : MnemonicAlias<"fucompi", "fucomip">;
def : MnemonicAlias<"ud2a", "ud2">;
… 以及許多其他內容。 使用 MnemonicAlias 定義,助記符會被簡單直接地重新映射。 雖然 MnemonicAlias 無法查看指令的任何方面(例如運算元),但它們可以透過 Requires 子句依賴全域模式(與配對器支援的模式相同)
def : MnemonicAlias<"pushf", "pushfq">, Requires<[In64BitMode]>;
def : MnemonicAlias<"pushf", "pushfl">, Requires<[In32BitMode]>;
在此範例中,助記符會根據目前的指令集映射到不同的助記符。
指令別名¶
別名處理最常見的階段發生在匹配過程中:它為匹配器提供新的形式以及要生成的特定指令。指令別名有兩個部分:要匹配的字符串和要生成的指令。例如
def : InstAlias<"movsx $src, $dst", (MOVSX16rr8W GR16:$dst, GR8 :$src)>;
def : InstAlias<"movsx $src, $dst", (MOVSX16rm8W GR16:$dst, i8mem:$src)>;
def : InstAlias<"movsx $src, $dst", (MOVSX32rr8 GR32:$dst, GR8 :$src)>;
def : InstAlias<"movsx $src, $dst", (MOVSX32rr16 GR32:$dst, GR16 :$src)>;
def : InstAlias<"movsx $src, $dst", (MOVSX64rr8 GR64:$dst, GR8 :$src)>;
def : InstAlias<"movsx $src, $dst", (MOVSX64rr16 GR64:$dst, GR16 :$src)>;
def : InstAlias<"movsx $src, $dst", (MOVSX64rr32 GR64:$dst, GR32 :$src)>;
這顯示了指令別名的一個強大範例,根據組譯程式中存在的運算元以多種不同方式匹配相同的助記符。指令別名的結果可以包括與目標指令不同順序的運算元,並且可以使用多次輸入,例如
def : InstAlias<"clrb $reg", (XOR8rr GR8 :$reg, GR8 :$reg)>;
def : InstAlias<"clrw $reg", (XOR16rr GR16:$reg, GR16:$reg)>;
def : InstAlias<"clrl $reg", (XOR32rr GR32:$reg, GR32:$reg)>;
def : InstAlias<"clrq $reg", (XOR64rr GR64:$reg, GR64:$reg)>;
此範例還顯示了綁定運算元只列出一次。在 X86 後端,XOR8rr 有兩個輸入 GR8 和一個輸出 GR8(其中一個輸入綁定到輸出)。InstAliases 採用扁平化的運算元清單,綁定運算元沒有重複項。指令別名的結果也可以使用立即數和固定物理寄存器,這些寄存器在結果中作為簡單的立即運算元添加,例如
// Fixed Immediate operand.
def : InstAlias<"aad", (AAD8i8 10)>;
// Fixed register operand.
def : InstAlias<"fcomi", (COM_FIr ST1)>;
// Simple alias.
def : InstAlias<"fcomi $reg", (COM_FIr RST:$reg)>;
指令別名也可以有 Requires 子句,使其成為子目標特定的。
如果後端支持,指令打印機可以自動發出別名,而不是被別名的內容。它通常會產生更好、更易讀的代碼。如果最好打印出被別名的內容,則將“0”作為第三個參數傳遞給 InstAlias 定義。
指令匹配¶
注意
待撰寫
目標特定實現注意事項¶
本文檔的這一部分說明特定於特定目標的代碼生成器的功能或設計決策。
尾調用優化¶
尾調用優化,被調用者重用調用者的堆棧,目前在 x86/x86-64、PowerPC、AArch64 和 WebAssembly 上受支持。如果滿足以下條件,則在 x86/x86-64、PowerPC 和 AArch64 上執行:
調用者和被調用者具有調用約定
fastcc
、cc 10
(GHC 調用約定)、cc 11
(HiPE 調用約定)、tailcc
或swifttailcc
。調用是尾調用 - 在尾部位置(ret 緊跟在調用之後,並且 ret 使用調用的值或為 void)。
選項
-tailcallopt
已啟用或調用約定為tailcc
。滿足特定於平台的約束。
x86/x86-64 約束
不使用可變參數列表。
在 x86-64 上,當生成 GOT/PIC 代碼時,僅支持模塊本地調用(可見性 = 隱藏或受保護)。
PowerPC 約束
不使用可變參數列表。
不使用 byval 參數。
在 ppc32/64 GOT/PIC 上,僅支持模塊本地調用(可見性 = 隱藏或受保護)。
WebAssembly 約束
不使用可變參數列表
已啟用“尾調用”目標屬性。
調用者和被調用者的返回類型必須匹配。除非被調用者為 void,否則調用者不能為 void。
AArch64 約束
不使用可變參數列表。
範例
調用方式為 llc -tailcallopt test.ll
。
declare fastcc i32 @tailcallee(i32 inreg %a1, i32 inreg %a2, i32 %a3, i32 %a4)
define fastcc i32 @tailcaller(i32 %in1, i32 %in2) {
%l1 = add i32 %in1, %in2
%tmp = tail call fastcc i32 @tailcallee(i32 inreg %in1, i32 inreg %in2, i32 %in1, i32 %l1)
ret i32 %tmp
}
-tailcallopt
的含義
為了在被呼叫方比呼叫方擁有更多參數的情況下支援尾端呼叫最佳化,會使用「被呼叫方彈出參數」的慣例。這目前會導致每個未經尾端呼叫最佳化的 fastcc
呼叫(因為不符合上述一或多個限制)後都必須重新調整堆疊。因此,在此類情況下,效能可能會變差。
同級呼叫最佳化¶
同級呼叫最佳化是尾端呼叫最佳化的一種受限形式。與前一節中描述的尾端呼叫最佳化不同,當未指定 -tailcallopt
選項時,它可以在任何尾端呼叫上自動執行。
當滿足以下限制時,目前會在 x86/x86-64 上執行同級呼叫最佳化
呼叫方和被呼叫方具有相同的呼叫慣例。它可以是
c
或fastcc
。調用是尾調用 - 在尾部位置(ret 緊跟在調用之後,並且 ret 使用調用的值或為 void)。
呼叫方和被呼叫方具有相符的返回類型,或者不使用被呼叫方的結果。
如果任何被呼叫方參數正在堆疊中傳遞,則它們必須在呼叫方自己的傳入參數堆疊中可用,並且框架偏移量必須相同。
範例
declare i32 @bar(i32, i32)
define i32 @foo(i32 %a, i32 %b, i32 %c) {
entry:
%0 = tail call i32 @bar(i32 %a, i32 %b)
ret i32 %0
}
X86 後端¶
X86 程式碼產生器位於 lib/Target/X86
目錄中。此程式碼產生器能夠針對各種 x86-32 和 x86-64 處理器,並且包含對 ISA 擴充功能(例如 MMX 和 SSE)的支援。
X86 目標三元組支援¶
以下是 X86 後端支援的已知目標三元組。這不是一個詳盡的清單,添加人們測試過的內容將會很有幫助。
i686-pc-linux-gnu — Linux
i386-unknown-freebsd5.3 — FreeBSD 5.3
i686-pc-cygwin — Win32 上的 Cygwin
i686-pc-mingw32 — Win32 上的 MingW
i386-pc-mingw32msvc — Linux 上的 MingW 交叉編譯器
i686-apple-darwin* — X86 上的 Apple Darwin
x86_64-unknown-linux-gnu — Linux
X86 呼叫慣例支援¶
後端已知以下特定於目標的呼叫慣例
x86_StdCall — 在 Microsoft Windows 平台上看到的 stdcall 呼叫慣例(CC ID = 64)。
x86_FastCall — 在 Microsoft Windows 平台上看到的 fastcall 呼叫慣例(CC ID = 65)。
x86_ThisCall — 類似於 X86_StdCall。在 ECX 中傳遞第一個參數,其他參數則透過堆疊傳遞。被呼叫方負責清理堆疊。根據預設,MSVC 會在其 ABI 中的方法使用此慣例(CC ID = 70)。
在 MachineInstrs 中表示 X86 位址模式¶
x86 具有非常靈活的記憶體存取方式。它能夠直接在整數指令(使用 ModR/M 位址)中形成以下表達式的記憶體位址
SegmentReg: Base + [1,2,4,8] * IndexReg + Disp32
為了表示這一點,LLVM 會為這種形式的每個記憶體運算元追蹤不少於 5 個運算元。這意味著「載入」形式的「mov
」按順序具有以下 MachineOperand
Index: 0 | 1 2 3 4 5
Meaning: DestReg, | BaseReg, Scale, IndexReg, Displacement Segment
OperandTy: VirtReg, | VirtReg, UnsImm, VirtReg, SignExtImm PhysReg
存放和其他所有指令都以相同的方式和順序處理四個記憶體運算元。如果未指定區段暫存器 (regno = 0),則不會產生區段覆寫。「Lea」運算沒有指定的區段暫存器,因此它們只有 4 個運算元用於其記憶體參考。
支援的 x86 位址空間¶
x86 有一項功能,可透過 x86 區段暫存器對不同的位址空間執行載入和儲存。指令上的區段覆寫前置位元組會導致指令的記憶體存取轉到指定的區段。LLVM 位址空間 0 是預設的位址空間,其中包含堆疊和程式中任何未限定的記憶體存取。位址空間 1-255 目前保留給使用者自訂的程式碼使用。GS 區段由位址空間 256 表示,FS 區段由位址空間 257 表示,而 SS 區段由位址空間 258 表示。其他 x86 區段尚未配置位址空間號碼。
雖然這些位址空間可能看起來類似於透過 thread_local
關鍵字實現的執行緒區域儲存區 (TLS),並且通常使用相同的底層硬體,但仍存在一些根本差異。
thread_local
關鍵字適用於全域變數,並指定應將它們配置在執行緒區域記憶體中。它不涉及型別限定詞,並且可以使用一般指標指向這些變數,並使用一般載入和儲存指令存取它們。thread_local
關鍵字在 LLVM IR 層級與目標無關(儘管 LLVM 尚未針對某些配置實作它)。
相反的,特殊位址空間適用於靜態型別。每個載入和儲存指令在其位址運算元型別中都有一個特定的位址空間,這決定了要存取哪個位址空間。LLVM 會忽略全域變數上的這些特殊位址空間限定詞,並且不提供直接在其中配置儲存空間的方法。在 LLVM IR 層級,這些特殊位址空間的行為部分取決於底層作業系統或執行階段環境,並且它們特定於 x86(LLVM 在某些情況下尚未正確處理它們)。
某些作業系統和執行階段環境會(或將來可能會)將 FS/GS 區段暫存器用於各種低階用途,因此在考慮使用它們時應格外小心。
指令命名¶
指令名稱由基本名稱、預設運算元大小和每個運算元的一個字元(可選特殊大小)組成。例如
ADD8rr -> add, 8-bit register, 8-bit register
IMUL16rmi -> imul, 16-bit register, 16-bit memory, 16-bit immediate
IMUL16rmi8 -> imul, 16-bit register, 16-bit memory, 8-bit immediate
MOVSX32rm16 -> movsx, 32-bit register, 16-bit memory
PowerPC 後端¶
PowerPC 程式碼產生器位於 lib/Target/PowerPC 目錄中。程式碼產生可以重定位到 PowerPC ISA 的多種變體或「子目標」,包括 ppc32、ppc64 和 altivec。
LLVM PowerPC ABI¶
LLVM 遵循 AIX PowerPC ABI,但有兩個例外。LLVM 使用 PC 相對(PIC)或靜態定址來存取全域值,因此不使用 TOC (r2)。其次,r31 被用作框架指標,以允許堆疊框架動態增長。LLVM 利用沒有 TOC 的優勢,在呼叫者框架的 PowerPC 連結區域中提供空間來儲存框架指標。PowerPC ABI 的其他詳細資訊可以在 PowerPC ABI 中找到。注意:此連結描述的是 32 位元 ABI。64 位元 ABI 類似,只是 GPR 的空間為 8 位元組寬(而不是 4 位元組),並且 r13 保留供系統使用。
框架佈局¶
在函式呼叫期間,PowerPC 框架的大小通常是固定的。由於框架大小固定,因此可以使用相對於堆疊指標的固定偏移量來存取框架中的所有參考。例外情況是當存在動態 alloca 或可變大小的陣列時,此時會使用基址指標 (r31) 作為堆疊指標的代理,並且堆疊指標可以自由增長或縮小。如果沒有將 -fomit-frame-pointer 旗標傳遞給 llvm-gcc,也會使用基址指標。堆疊指標始終與 16 位元組對齊,以便為 altivec 向量分配的空間將正確對齊。
呼叫框架的佈局如下(頂部為低記憶體地址)
連結區 |
參數區 |
動態區 |
區域變數區 |
已儲存暫存器區 |
先前框架 |
被呼叫者使用「連結區」在分配自己的框架之前儲存特殊暫存器。只有三個項目與 LLVM 相關。第一個項目是先前的堆疊指標 (sp),也稱為連結。這允許像 gdb 或例外處理常式這樣的探測工具快速掃描堆疊中的框架。函式結尾也可以使用連結從堆疊中彈出框架。連結區中的第三個項目用於從 lr 暫存器儲存返回地址。最後,如上所述,最後一個項目用於儲存先前的框架指標 (r31)。連結區中的項目是 GPR 的大小,因此連結區在 32 位元模式下為 24 位元組長,在 64 位元模式下為 48 位元組長。
32 位元連結區
0 | 已儲存 SP (r1) |
4 | 已儲存 CR |
8 | 已儲存 LR |
12 | 保留 |
16 | 保留 |
20 | 已儲存 FP (r31) |
64 位元連結區
0 | 已儲存 SP (r1) |
8 | 已儲存 CR |
16 | 已儲存 LR |
24 | 保留 |
32 | 保留 |
40 | 已儲存 FP (r31) |
「參數區」用於儲存要傳遞給被呼叫函式的參數。根據 PowerPC ABI,實際上前幾個參數是在暫存器中傳遞的,參數區中的空間未使用。但是,如果沒有足夠的暫存器,或者被呼叫者是 thunk 或可變參數函式,則可以將這些暫存器參數溢出到參數區中。因此,參數區必須足夠大,才能儲存呼叫者進行的最大呼叫序列的所有參數。大小還必須至少足以溢出暫存器 r3-r10。這允許對呼叫簽章一無所知的被呼叫者(例如 thunk 和可變參數函式)有足夠的空間來快取參數暫存器。因此,參數區至少為 32 位元組(在 64 位元模式下為 64 位元組)。還請注意,由於參數區是框架頂部的固定偏移量,因此被呼叫者可以使用相對於堆疊指標(或基址指標)的固定偏移量來存取其分割參數。
結合有關連結、參數區和對齊方式的信息。堆疊框架在 32 位元模式下至少為 64 位元組,在 64 位元模式下至少為 128 位元組。
「動態區」的大小最初為零。如果函式使用動態 alloca,則會向堆疊添加空間,連結區和參數區會移至堆疊頂部,並且新空間在連結區和參數區下方立即可用。移動連結區和參數區的成本很小,因為只需要複製連結值。通過將原始框架大小添加到基址指標,可以輕鬆獲取連結值。請注意,動態空間中的分配需要遵守 16 位元組對齊。
「區域變數區」是 llvm 編譯器為區域變數保留空間的地方。
「已儲存暫存器區」是 llvm 編譯器在進入被呼叫者時溢出被呼叫者儲存暫存器的地方。
序言/結尾¶
llvm 的序言和結尾與 PowerPC ABI 中描述的相同,但有以下例外。被呼叫者儲存的暫存器會在框架建立後被溢出。這使得 llvm 結尾/序言支援可以與其他目標通用。基址指標被呼叫者儲存的暫存器 r31 會儲存在連結區域的 TOC 位置中。這簡化了基址指標的空間配置,並使其在程式設計和偵錯期間易於定位。
動態配置¶
注意
TODO - 更多內容將陸續推出。
NVPTX 後端¶
lib/Target/NVPTX 下的 NVPTX 代碼產生器是 NVIDIA NVPTX 代碼產生器用於 LLVM 的開源版本。它由 NVIDIA 貢獻,是 CUDA 編譯器 (nvcc) 中使用的代碼產生器的移植版本。它的目標是 PTX 3.0/3.1 ISA,並且可以針對任何大於或等於 2.0(Fermi)的計算能力。
此目標具有產品級品質,並且應該與官方 NVIDIA 工具鏈完全相容。
代碼產生器選項
選項 | 說明 |
---|---|
sm_20 | 將著色器模型/計算能力設定為 2.0 |
sm_21 | 將著色器模型/計算能力設定為 2.1 |
sm_30 | 將著色器模型/計算能力設定為 3.0 |
sm_35 | 將著色器模型/計算能力設定為 3.5 |
ptx30 | 目標 PTX 3.0 |
ptx31 | 目標 PTX 3.1 |
擴展的 Berkeley 封包過濾器 (eBPF) 後端¶
擴展 BPF(或 eBPF)類似於用於過濾網路封包的原始(「經典」)BPF (cBPF)。bpf() 系統呼叫 執行與 eBPF 相關的一系列操作。對於 cBPF 和 eBPF 程式,Linux 核心會在載入程式之前對其進行靜態分析,以確保它們不會損害正在執行的系統。eBPF 是一種 64 位元 RISC 指令集,設計用於一對一映射到 64 位元 CPU。操作碼採用 8 位元編碼,並定義了 87 條指令。共有 10 個暫存器,按功能分組,如下所示。
R0 return value from in-kernel functions; exit value for eBPF program
R1 - R5 function call arguments to in-kernel functions
R6 - R9 callee-saved registers preserved by in-kernel functions
R10 stack frame pointer (read only)
指令編碼(算術和跳轉)¶
eBPF 正在重用經典版本中的大部分操作碼編碼,以簡化經典 BPF 到 eBPF 的轉換。對於算術和跳轉指令,8 位元的「代碼」欄位分為三個部分
+----------------+--------+--------------------+
| 4 bits | 1 bit | 3 bits |
| operation code | source | instruction class |
+----------------+--------+--------------------+
(MSB) (LSB)
三個 LSB 位元儲存指令類別,它是以下其中之一
BPF_LD 0x0
BPF_LDX 0x1
BPF_ST 0x2
BPF_STX 0x3
BPF_ALU 0x4
BPF_JMP 0x5
(unused) 0x6
BPF_ALU64 0x7
當 BPF_CLASS(code) == BPF_ALU 或 BPF_ALU64 或 BPF_JMP 時,第 4 個位元編碼來源運算元
BPF_X 0x1 use src_reg register as source operand
BPF_K 0x0 use 32 bit immediate as source operand
而四個 MSB 位元儲存操作碼
BPF_ADD 0x0 add
BPF_SUB 0x1 subtract
BPF_MUL 0x2 multiply
BPF_DIV 0x3 divide
BPF_OR 0x4 bitwise logical OR
BPF_AND 0x5 bitwise logical AND
BPF_LSH 0x6 left shift
BPF_RSH 0x7 right shift (zero extended)
BPF_NEG 0x8 arithmetic negation
BPF_MOD 0x9 modulo
BPF_XOR 0xa bitwise logical XOR
BPF_MOV 0xb move register to register
BPF_ARSH 0xc right shift (sign extended)
BPF_END 0xd endianness conversion
如果 BPF_CLASS(code) == BPF_JMP,則 BPF_OP(code) 是以下其中之一
BPF_JA 0x0 unconditional jump
BPF_JEQ 0x1 jump ==
BPF_JGT 0x2 jump >
BPF_JGE 0x3 jump >=
BPF_JSET 0x4 jump if (DST & SRC)
BPF_JNE 0x5 jump !=
BPF_JSGT 0x6 jump signed >
BPF_JSGE 0x7 jump signed >=
BPF_CALL 0x8 function call
BPF_EXIT 0x9 function return
指令編碼(載入、儲存)¶
對於載入和儲存指令,8 位元的「代碼」欄位分為
+--------+--------+-------------------+
| 3 bits | 2 bits | 3 bits |
| mode | size | instruction class |
+--------+--------+-------------------+
(MSB) (LSB)
大小修飾符是以下其中之一
BPF_W 0x0 word
BPF_H 0x1 half word
BPF_B 0x2 byte
BPF_DW 0x3 double word
模式修飾符是以下其中之一
BPF_IMM 0x0 immediate
BPF_ABS 0x1 used to access packet data
BPF_IND 0x2 used to access packet data
BPF_MEM 0x3 memory
(reserved) 0x4
(reserved) 0x5
BPF_XADD 0x6 exclusive add
封包資料存取 (BPF_ABS, BPF_IND)¶
兩條非泛型指令:(BPF_ABS | <size> | BPF_LD) 和 (BPF_IND | <size> | BPF_LD),用於存取封包資料。暫存器 R6 是一個隱式輸入,必須包含指向 sk_buff 的指標。暫存器 R0 是一個隱式輸出,其中包含從封包中提取的資料。暫存器 R1-R5 是暫存暫存器,並且不得用於在 BPF_ABS | BPF_LD 或 BPF_IND | BPF_LD 指令之間儲存資料。這些指令也具有隱式的程式退出條件。當 eBPF 程式嘗試存取超出封包邊界的資料時,直譯器將中止程式的執行。
- BPF_IND | BPF_W | BPF_LD 等效於
R0 = ntohl(*(u32 *) (((struct sk_buff *) R6)->data + src_reg + imm32))
eBPF 映射¶
eBPF 映射用於在核心與用戶空間之間共享數據。目前實作的類型有雜湊和數組,並可能擴展到支持布隆過濾器、基數樹等。映射由其類型、最大元素數、鍵大小和值大小(以字節為單位)定義。 eBPF 系統調用支持映射上的創建、更新、查找和刪除功能。
函數調用¶
函數調用參數使用最多五個寄存器(R1 - R5)傳遞。返回值在專用寄存器 (R0) 中傳遞。另外四個寄存器(R6 - R9)由被調用者保存,這些寄存器中的值在核心函數中保留。 R0 - R5 是核心函數中的臨時寄存器,因此如果需要跨函數調用,eBPF 程序必須在這些寄存器中存儲/恢復值。可以使用唯讀幀指針 R10 訪問堆棧。 eBPF 寄存器在 x86_64 和其他 64 位架構上與硬件寄存器一對一映射。例如,x86_64 內核 JIT 將它們映射為
R0 - rax
R1 - rdi
R2 - rsi
R3 - rdx
R4 - rcx
R5 - r8
R6 - rbx
R7 - r13
R8 - r14
R9 - r15
R10 - rbp
因為 x86_64 ABI 要求 rdi、rsi、rdx、rcx、r8、r9 用於參數傳遞,而 rbx、r12 - r15 由被調用者保存。
程序啟動¶
eBPF 程序接收單個參數並包含單個 eBPF 主例程;程序不包含 eBPF 函數。函數調用僅限於一組預定義的核心函數。程序的大小限制為 4K 指令:這確保了快速終止和有限數量的核心函數調用。在運行 eBPF 程序之前,驗證器會執行靜態分析以防止代碼中的循環,並確保有效的寄存器使用和操作數類型。
AMDGPU 後端¶
AMDGPU 代碼生成器位於 lib/Target/AMDGPU
目錄中。該代碼生成器能夠針對各種 AMD GPU 處理器。有關更多信息,請參閱AMDGPU 後端用戶指南。