編寫 LLVM 後端¶
簡介¶
本文档描述了编写编译器后端的技巧,这些后端将 LLVM 中间表示(IR)转换为指定机器或其他语言的代码。 针对特定机器的代码可以采用汇编代码或二进制代码(可用于 JIT 编译器)的形式。
LLVM 的後端具有獨立於目標的程式碼產生器,可以為多種目標 CPU 產生輸出,包括 X86、PowerPC、ARM 和 SPARC。 後端也可用於產生針對 Cell 處理器的 SPU 或 GPU 的程式碼,以支援運算核心的執行。
本文檔重點介紹在下載的 LLVM 版本中 llvm/lib/Target
的子目錄中找到的現有範例。 特別是,本文檔重點介紹為 SPARC 目標建立靜態編譯器(發射文字組語的編譯器)的範例,因為 SPARC 具有相當標準的特性,例如 RISC 指令集和直接的呼叫慣例。
目標讀者¶
本文檔的目標讀者是任何需要編寫 LLVM 後端,為特定硬體或軟體目標產生程式碼的人。
先備閱讀¶
在閱讀本文檔之前,必須先閱讀以下重要文檔
LLVM 語言參考手冊 — LLVM 組語語言的參考手冊。
LLVM 獨立於目標的程式碼產生器 — 指南,介紹將 LLVM 內部表示轉換為指定目標的機器碼的組件(類別和程式碼產生演算法)。 請特別注意程式碼產生階段的描述:指令選擇、排程與格式化、基於 SSA 的最佳化、暫存器分配、序言/後記程式碼插入、後期機器碼最佳化和程式碼發射。
TableGen 概述 — 本文檔描述 TableGen (
tblgen
) 應用程式,該應用程式管理特定領域的資訊以支援 LLVM 程式碼產生。 TableGen 處理來自目標描述檔 (.td
後綴) 的輸入,並產生可用於程式碼產生的 C++ 程式碼。編寫 LLVM Pass(舊版 PM 版本) — 組語列印器是一個
FunctionPass
,一些SelectionDAG
處理步驟也是如此。
若要遵循本文檔中的 SPARC 範例,請準備一份 The SPARC Architecture Manual, Version 8 以供參考。 關於 ARM 指令集的詳細資訊,請參閱 ARM Architecture Reference Manual。 關於 GNU 組譯器格式 (GAS
) 的更多資訊,請參閱 Using As,特別是對於組語列印器。 “Using As” 包含目標機器相關功能的列表。
基本步驟¶
若要為 LLVM 編寫編譯器後端,將 LLVM IR 轉換為指定目標(機器或其他語言)的程式碼,請遵循以下步驟
建立
TargetMachine
類別的子類別,用於描述目標機器的特性。 複製現有特定TargetMachine
類別和標頭檔的範例; 例如,從SparcTargetMachine.cpp
和SparcTargetMachine.h
開始,但變更目標的檔名。 同樣地,將引用 “Sparc
” 的程式碼變更為引用您的目標。描述目標的暫存器集合。 使用 TableGen 從目標特定的
RegisterInfo.td
輸入檔產生用於暫存器定義、暫存器別名和暫存器類別的程式碼。 您也應該為TargetRegisterInfo
類別的子類別編寫額外程式碼,該子類別表示用於暫存器分配的類別暫存器檔案資料,並描述暫存器之間的互動。描述目標的指令集。 使用 TableGen 從
TargetInstrFormats.td
和TargetInstrInfo.td
的目標特定版本產生用於目標特定指令的程式碼。 您應該為TargetInstrInfo
類別的子類別編寫額外程式碼,以表示目標機器支援的機器指令。描述從指令的定向非循環圖 (DAG) 表示到原生目標特定指令的 LLVM IR 的選擇和轉換。 使用 TableGen 產生程式碼,根據目標特定版本的
TargetInstrInfo.td
中的其他資訊來比對模式並選擇指令。 為XXXISelDAGToDAG.cpp
編寫程式碼,其中XXX
識別特定目標,以執行模式比對和 DAG 到 DAG 的指令選擇。 也在XXXISelLowering.cpp
中編寫程式碼,以取代或移除 SelectionDAG 中原生不支援的運算和資料類型。為組語列印器編寫程式碼,將 LLVM IR 轉換為目標機器的 GAS 格式。 您應該將組語字串新增至目標特定版本的
TargetInstrInfo.td
中定義的指令。 您也應該為AsmPrinter
的子類別編寫程式碼,該子類別執行 LLVM 到組語的轉換,以及TargetAsmInfo
的簡單子類別。選擇性地,新增對子目標(即,具有不同功能的變體)的支援。 您也應該為
TargetSubtarget
類別的子類別編寫程式碼,這可讓您使用-mcpu=
和-mattr=
命令列選項。選擇性地,新增 JIT 支援並建立機器碼發射器 (
TargetJITInfo
的子類別),用於將二進制程式碼直接發射到記憶體中。
在 .cpp
和 .h
檔案中,最初先建立這些方法的存根,然後稍後再實作它們。 最初,您可能不知道類別需要哪些私有成員,以及哪些組件需要被子類別化。
預備知識¶
若要實際建立編譯器後端,您需要建立和修改一些檔案。 此處討論的是絕對最小值。 但是若要實際使用 LLVM 獨立於目標的程式碼產生器,您必須執行 LLVM 獨立於目標的程式碼產生器 文檔中描述的步驟。
首先,您應該在 lib/Target
下建立一個子目錄,以保存與目標相關的所有檔案。 如果您的目標稱為 “Dummy”,請建立目錄 lib/Target/Dummy
。
在這個新目錄中,建立一個 CMakeLists.txt
。 最簡單的方法是複製另一個目標的 CMakeLists.txt
並修改它。 它至少應包含 LLVM_TARGET_DEFINITIONS
變數。 庫可以命名為 LLVMDummy
(例如,請參閱 MIPS 目標)。 或者,您可以將庫拆分為 LLVMDummyCodeGen
和 LLVMDummyAsmPrinter
,後者應在 lib/Target/Dummy
下方的子目錄中實作(例如,請參閱 PowerPC 目標)。
請注意,這兩種命名方案都硬編碼到 llvm-config
中。 使用任何其他命名方案都會混淆 llvm-config
,並在連結 llc
時產生許多(看似無關的)連結器錯誤。
若要讓您的目標實際執行某些操作,您需要實作 TargetMachine
的子類別。 此實作通常應位於檔案 lib/Target/DummyTargetMachine.cpp
中,但 lib/Target
目錄中的任何檔案都將被建置且應可運作。 若要使用 LLVM 獨立於目標的程式碼產生器,您應該執行目前所有機器後端都執行的操作:建立 CodeGenTargetMachineImpl
的子類別。 (若要從頭開始建立目標,請建立 TargetMachine
的子類別。)
若要讓 LLVM 實際建置和連結您的目標,您需要使用 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy
執行 cmake
。 這將建置您的目標,而無需將其新增至所有目標的列表中。
一旦您的目標穩定,您可以將其新增至主 CMakeLists.txt
中的 LLVM_ALL_TARGETS
變數。
目標機器¶
CodeGenTargetMachineImpl
設計為使用 LLVM 獨立於目標的程式碼產生器實作的目標的基底類別。 CodeGenTargetMachineImpl
類別應由實作各種虛擬方法的具體目標類別專門化。 CodeGenTargetMachineImpl
在 include/llvm/CodeGen/CodeGenTargetMachineImpl.h
中定義為 TargetMachine
的子類別。 TargetMachine
類別實作 (include/llvm/Target/TargetMachine.cpp
) 也處理眾多命令列選項。
若要建立 CodeGenTargetMachineImpl
的具體目標特定子類別,請先複製現有的 TargetMachine
類別和標頭。 您應該命名您建立的檔案,以反映您的特定目標。 例如,對於 SPARC 目標,將檔案命名為 SparcTargetMachine.h
和 SparcTargetMachine.cpp
。
對於目標機器 XXX
,XXXTargetMachine
的實作必須具有存取方法,以取得表示目標組件的物件。 這些方法命名為 get*Info
,旨在取得指令集 (getInstrInfo
)、暫存器集合 (getRegisterInfo
)、堆疊框架佈局 (getFrameInfo
) 和類似資訊。XXXTargetMachine
也必須實作 getDataLayout
方法,以存取具有目標特定資料特性的物件,例如資料類型大小和對齊要求。
例如,對於 SPARC 目標,標頭檔 SparcTargetMachine.h
宣告了幾個 get*Info
和 getDataLayout
方法的原型,這些方法僅傳回類別成員。
namespace llvm {
class Module;
class SparcTargetMachine : public CodeGenTargetMachineImpl {
const DataLayout DataLayout; // Calculates type size & alignment
SparcSubtarget Subtarget;
SparcInstrInfo InstrInfo;
TargetFrameInfo FrameInfo;
protected:
virtual const TargetAsmInfo *createTargetAsmInfo() const;
public:
SparcTargetMachine(const Module &M, const std::string &FS);
virtual const SparcInstrInfo *getInstrInfo() const {return &InstrInfo; }
virtual const TargetFrameInfo *getFrameInfo() const {return &FrameInfo; }
virtual const TargetSubtarget *getSubtargetImpl() const{return &Subtarget; }
virtual const TargetRegisterInfo *getRegisterInfo() const {
return &InstrInfo.getRegisterInfo();
}
virtual const DataLayout *getDataLayout() const { return &DataLayout; }
// Pass Pipeline Configuration
virtual bool addInstSelector(PassManagerBase &PM, bool Fast);
virtual bool addPreEmitPass(PassManagerBase &PM, bool Fast);
};
} // end namespace llvm
getInstrInfo()
getRegisterInfo()
getFrameInfo()
getDataLayout()
getSubtargetImpl()
對於某些目標,您還需要支援以下方法
getTargetLowering()
getJITInfo()
某些架構,例如 GPU,不支援跳轉到任意程式位置,而是使用遮罩執行來實作分支,並使用迴圈體周圍的特殊指令來實作迴圈。 為了避免引入此類硬體無法處理的不可簡化的控制流程的 CFG 修改,目標在初始化時必須呼叫 setRequiresStructuredCFG(true)。
此外,XXXTargetMachine
建構函式應指定一個 TargetDescription
字串,該字串決定目標機器的資料佈局,包括指標大小、對齊方式和位元組序等特性。 例如,SparcTargetMachine
的建構函式包含以下內容
SparcTargetMachine::SparcTargetMachine(const Module &M, const std::string &FS)
: DataLayout("E-p:32:32-f128:128:128"),
Subtarget(M, FS), InstrInfo(Subtarget),
FrameInfo(TargetFrameInfo::StackGrowsDown, 8, 0) {
}
連字符分隔 TargetDescription
字串的各個部分。
字串中的大寫 “
E
” 表示大端目標資料模型。 小寫 “e
” 表示小端。“
p:
” 後面跟著指標資訊:大小、ABI 對齊方式和偏好的對齊方式。 如果 “p:
” 後面僅跟著兩個數字,則第一個值是指標大小,第二個值是 ABI 和偏好的對齊方式。然後是數字類型對齊方式的字母:“
i
”、“f
”、“v
” 或 “a
”(分別對應於整數、浮點數、向量或聚合)。 “i
”、“v
” 或 “a
” 後面跟著 ABI 對齊方式和偏好的對齊方式。“f
” 後面跟著三個值:第一個表示 long double 的大小,然後是 ABI 對齊方式,然後是 ABI 偏好的對齊方式。
目標註冊¶
您還必須向 TargetRegistry
註冊您的目標,這是其他 LLVM 工具用來在執行時查找和使用您的目標的方式。TargetRegistry
可以直接使用,但對於大多數目標,都有輔助範本可以為您處理工作。
所有目標都應宣告一個全域 Target
物件,用於在註冊期間表示目標。 然後,在目標的 TargetInfo
庫中,目標應定義該物件並使用 RegisterTarget
範本來註冊目標。 例如,Sparc 註冊程式碼如下所示
Target llvm::getTheSparcTarget();
extern "C" void LLVMInitializeSparcTargetInfo() {
RegisterTarget<Triple::sparc, /*HasJIT=*/false>
X(getTheSparcTarget(), "sparc", "Sparc");
}
這允許 TargetRegistry
按名稱或目標三元組查找目標。 此外,大多數目標也會註冊額外功能,這些功能在單獨的庫中可用。 這些註冊步驟是分開的,因為某些客戶端可能希望僅連結到目標的某些部分 — 例如,JIT 程式碼產生器不需要使用組語列印器。 以下是註冊 Sparc 組語列印器的範例
extern "C" void LLVMInitializeSparcAsmPrinter() {
RegisterAsmPrinter<SparcAsmPrinter> X(getTheSparcTarget());
}
如需更多資訊,請參閱 “llvm/Target/TargetRegistry.h”。
暫存器集合與暫存器類別¶
您應該描述一個具體的目標特定類別,表示目標機器的暫存器檔案。 此類別稱為 XXXRegisterInfo
(其中 XXX
識別目標),並表示用於暫存器分配的類別暫存器檔案資料。 它也描述暫存器之間的互動。
您還需要定義暫存器類別,以對相關暫存器進行分類。 應為暫存器群組新增暫存器類別,這些暫存器對於某些指令都以相同方式處理。 典型的範例是整數、浮點數或向量暫存器的暫存器類別。 暫存器分配器允許指令使用指定暫存器類別中的任何暫存器,以類似方式執行指令。 暫存器類別將虛擬暫存器從這些集合分配給指令,而暫存器類別讓獨立於目標的暫存器分配器自動選擇實際的暫存器。
暫存器的大部分程式碼,包括暫存器定義、暫存器別名和暫存器類別,都是由 TableGen 從 XXXRegisterInfo.td
輸入檔產生,並放置在 XXXGenRegisterInfo.h.inc
和 XXXGenRegisterInfo.inc
輸出檔中。 XXXRegisterInfo
實作中的某些程式碼需要手動編碼。
定義暫存器¶
XXXRegisterInfo.td
檔案通常以目標機器的暫存器定義開始。Register
類別(在 Target.td
中指定)用於為每個暫存器定義一個物件。 指定的字串 n
成為暫存器的 Name
。 基本 Register
物件沒有任何子暫存器,並且不指定任何別名。
class Register<string n> {
string Namespace = "";
string AsmName = n;
string Name = n;
int SpillSize = 0;
int SpillAlignment = 0;
list<Register> Aliases = [];
list<Register> SubRegs = [];
list<int> DwarfNumbers = [];
}
例如,在 X86RegisterInfo.td
檔案中,有一些暫存器定義使用 Register
類別,例如
def AL : Register<"AL">, DwarfRegNum<[0, 0, 0]>;
這定義了暫存器 AL
,並為其分配了值(使用 DwarfRegNum
),這些值由 gcc
、gdb
或偵錯資訊寫入器用於識別暫存器。 對於暫存器 AL
,DwarfRegNum
採用 3 個值的陣列,表示 3 種不同的模式:第一個元素用於 X86-64,第二個用於 X86-32 上的異常處理 (EH),第三個是通用的。 -1 是一個特殊的 Dwarf 數字,表示 gcc 數字未定義,-2 表示暫存器數字在此模式下無效。
從先前描述的 X86RegisterInfo.td
檔案中的行,TableGen 在 X86GenRegisterInfo.inc
檔案中產生此程式碼
static const unsigned GR8[] = { X86::AL, ... };
const unsigned AL_AliasSet[] = { X86::AX, X86::EAX, X86::RAX, 0 };
const TargetRegisterDesc RegisterDescriptors[] = {
...
{ "AL", "AL", AL_AliasSet, Empty_SubRegsSet, Empty_SubRegsSet, AL_SuperRegsSet }, ...
從暫存器資訊檔案中,TableGen 為每個暫存器產生一個 TargetRegisterDesc
物件。TargetRegisterDesc
在 include/llvm/Target/TargetRegisterInfo.h
中定義,具有以下欄位
struct TargetRegisterDesc {
const char *AsmName; // Assembly language name for the register
const char *Name; // Printable name for the reg (for debugging)
const unsigned *AliasSet; // Register Alias Set
const unsigned *SubRegs; // Sub-register set
const unsigned *ImmSubRegs; // Immediate sub-register set
const unsigned *SuperRegs; // Super-register set
};
TableGen 使用整個目標描述檔 (.td
) 來決定暫存器的文字名稱(在 TargetRegisterDesc
的 AsmName
和 Name
欄位中)以及其他暫存器與已定義暫存器的關係(在其他 TargetRegisterDesc
欄位中)。 在此範例中,其他定義將暫存器 “AX
”、“EAX
” 和 “RAX
” 建立為彼此的別名,因此 TableGen 為此暫存器別名集合產生一個以 null 結尾的陣列 (AL_AliasSet
)。
Register
類別通常用作更複雜類別的基底類別。 在 Target.td
中,Register
類別是 RegisterWithSubRegs
類別的基底,該類別用於定義需要在 SubRegs
列表中指定子暫存器的暫存器,如下所示
class RegisterWithSubRegs<string n, list<Register> subregs> : Register<n> {
let SubRegs = subregs;
}
在 SparcRegisterInfo.td
中,為 SPARC 定義了其他暫存器類別:Register
子類別 SparcReg
,以及更進一步的子類別:Ri
、Rf
和 Rd
。 SPARC 暫存器由 5 位元 ID 號碼識別,這是這些子類別的共同特徵。 請注意使用 “let
” 運算式來覆寫最初在超類別中定義的值(例如 Rd
類別中的 SubRegs
欄位)。
class SparcReg<string n> : Register<n> {
field bits<5> Num;
let Namespace = "SP";
}
// Ri - 32-bit integer registers
class Ri<bits<5> num, string n> :
SparcReg<n> {
let Num = num;
}
// Rf - 32-bit floating-point registers
class Rf<bits<5> num, string n> :
SparcReg<n> {
let Num = num;
}
// Rd - Slots in the FP register file for 64-bit floating-point values.
class Rd<bits<5> num, string n, list<Register> subregs> : SparcReg<n> {
let Num = num;
let SubRegs = subregs;
}
在 SparcRegisterInfo.td
檔案中,有一些暫存器定義使用 Register
的這些子類別,例如
def G0 : Ri< 0, "G0">, DwarfRegNum<[0]>;
def G1 : Ri< 1, "G1">, DwarfRegNum<[1]>;
...
def F0 : Rf< 0, "F0">, DwarfRegNum<[32]>;
def F1 : Rf< 1, "F1">, DwarfRegNum<[33]>;
...
def D0 : Rd< 0, "F0", [F0, F1]>, DwarfRegNum<[32]>;
def D1 : Rd< 2, "F2", [F2, F3]>, DwarfRegNum<[34]>;
上面顯示的最後兩個暫存器(D0
和 D1
)是雙精度浮點暫存器,它們是單精度浮點子暫存器對的別名。 除了別名之外,已定義暫存器的子暫存器和超暫存器關係也位於暫存器的 TargetRegisterDesc
的欄位中。
定義暫存器類別¶
RegisterClass
類別(在 Target.td
中指定)用於定義一個物件,該物件表示一組相關的暫存器,並定義暫存器的預設分配順序。 使用 Target.td
的目標描述檔 XXXRegisterInfo.td
可以使用以下類別建構暫存器類別
class RegisterClass<string namespace,
list<ValueType> regTypes, int alignment, dag regList> {
string Namespace = namespace;
list<ValueType> RegTypes = regTypes;
int Size = 0; // spill size, in bits; zero lets tblgen pick the size
int Alignment = alignment;
// CopyCost is the cost of copying a value between two registers
// default value 1 means a single instruction
// A negative value means copying is extremely expensive or impossible
int CopyCost = 1;
dag MemberList = regList;
// for register classes that are subregisters of this class
list<RegisterClass> SubRegClassList = [];
code MethodProtos = [{}]; // to insert arbitrary code
code MethodBodies = [{}];
}
若要定義 RegisterClass
,請使用以下 4 個引數
定義的第一個引數是命名空間的名稱。
第二個參數是
ValueType
暫存器類型值的列表,這些值定義於include/llvm/CodeGen/ValueTypes.td
中。定義的值包含整數類型(例如i16
、i32
和用於布林值的i1
)、浮點數類型(f32
、f64
)和向量類型(例如,8 x i16
向量的v8i16
)。在RegisterClass
中的所有暫存器必須具有相同的ValueType
,但有些暫存器可能會以不同的配置儲存向量資料。例如,一個可以處理 128 位元向量的暫存器可能能夠處理 16 個 8 位元整數元素、8 個 16 位元整數、4 個 32 位元整數等等。RegisterClass
定義的第三個參數指定了暫存器在儲存或載入到記憶體時所需的對齊方式。最後一個參數
regList
指定了哪些暫存器在這個類別中。如果沒有指定替代的分配順序方法,則regList
也定義了暫存器分配器使用的分配順序。除了簡單地使用(add R0, R1, ...)
列出暫存器之外,還有更進階的集合運算子可用。有關更多資訊,請參閱include/llvm/Target/Target.td
。
在 SparcRegisterInfo.td
中,定義了三個 RegisterClass
物件:FPRegs
、DFPRegs
和 IntRegs
。對於所有三個暫存器類別,第一個參數使用字串 “SP
” 定義了命名空間。FPRegs
定義了一組 32 個單精度浮點暫存器(F0
到 F31
);DFPRegs
定義了一組 16 個雙精度暫存器(D0-D15
)。
// F0, F1, F2, ..., F31
def FPRegs : RegisterClass<"SP", [f32], 32, (sequence "F%u", 0, 31)>;
def DFPRegs : RegisterClass<"SP", [f64], 64,
(add D0, D1, D2, D3, D4, D5, D6, D7, D8,
D9, D10, D11, D12, D13, D14, D15)>;
def IntRegs : RegisterClass<"SP", [i32], 32,
(add L0, L1, L2, L3, L4, L5, L6, L7,
I0, I1, I2, I3, I4, I5,
O0, O1, O2, O3, O4, O5, O7,
G1,
// Non-allocatable regs:
G2, G3, G4,
O6, // stack ptr
I6, // frame ptr
I7, // return address
G0, // constant zero
G5, G6, G7 // reserved for kernel
)>;
將 SparcRegisterInfo.td
與 TableGen 搭配使用會產生數個輸出檔案,這些檔案旨在包含在你編寫的其他原始碼中。SparcRegisterInfo.td
產生 SparcGenRegisterInfo.h.inc
,應將其包含在你編寫的 SPARC 暫存器實作的標頭檔(SparcRegisterInfo.h
)中。在 SparcGenRegisterInfo.h.inc
中,定義了一個名為 SparcGenRegisterInfo
的新結構,它使用 TargetRegisterInfo
作為其基底。它也基於定義的暫存器類別指定了類型:DFPRegsClass
、FPRegsClass
和 IntRegsClass
。
SparcRegisterInfo.td
也產生 SparcGenRegisterInfo.inc
,它包含在 SPARC 暫存器實作 SparcRegisterInfo.cpp
的底部。以下程式碼僅顯示產生的整數暫存器和相關的暫存器類別。IntRegs
中暫存器的順序反映了目標描述檔中 IntRegs
的定義順序。
// IntRegs Register Class...
static const unsigned IntRegs[] = {
SP::L0, SP::L1, SP::L2, SP::L3, SP::L4, SP::L5,
SP::L6, SP::L7, SP::I0, SP::I1, SP::I2, SP::I3,
SP::I4, SP::I5, SP::O0, SP::O1, SP::O2, SP::O3,
SP::O4, SP::O5, SP::O7, SP::G1, SP::G2, SP::G3,
SP::G4, SP::O6, SP::I6, SP::I7, SP::G0, SP::G5,
SP::G6, SP::G7,
};
// IntRegsVTs Register Class Value Types...
static const MVT::ValueType IntRegsVTs[] = {
MVT::i32, MVT::Other
};
namespace SP { // Register class instances
DFPRegsClass DFPRegsRegClass;
FPRegsClass FPRegsRegClass;
IntRegsClass IntRegsRegClass;
...
// IntRegs Sub-register Classes...
static const TargetRegisterClass* const IntRegsSubRegClasses [] = {
NULL
};
...
// IntRegs Super-register Classes..
static const TargetRegisterClass* const IntRegsSuperRegClasses [] = {
NULL
};
...
// IntRegs Register Class sub-classes...
static const TargetRegisterClass* const IntRegsSubclasses [] = {
NULL
};
...
// IntRegs Register Class super-classes...
static const TargetRegisterClass* const IntRegsSuperclasses [] = {
NULL
};
IntRegsClass::IntRegsClass() : TargetRegisterClass(IntRegsRegClassID,
IntRegsVTs, IntRegsSubclasses, IntRegsSuperclasses, IntRegsSubRegClasses,
IntRegsSuperRegClasses, 4, 4, 1, IntRegs, IntRegs + 32) {}
}
暫存器分配器將避免使用保留的暫存器,並且在所有 Volatile 暫存器都已使用完畢之前,不會使用被呼叫者儲存的暫存器。這通常已足夠好,但在某些情況下,可能有必要提供自訂的分配順序。
實作 TargetRegisterInfo
的子類別¶
最後一個步驟是手動編寫 XXXRegisterInfo
的部分程式碼,它實作了 TargetRegisterInfo.h
中描述的介面(請參閱 TargetRegisterInfo 類別)。除非被覆寫,否則這些函式會傳回 0
、NULL
或 false
。以下是在 SparcRegisterInfo.cpp
中為 SPARC 實作覆寫的函式列表:
getCalleeSavedRegs
— 傳回被呼叫者儲存的暫存器列表,順序為所需的被呼叫者儲存堆疊框架偏移量。getReservedRegs
— 傳回一個位元集合,以實體暫存器編號索引,指示特定暫存器是否不可用。hasFP
— 傳回布林值,指示函式是否應具有專用的框架指標暫存器。eliminateCallFramePseudoInstr
— 如果使用了呼叫框架設定或銷毀虛擬指令,則可以呼叫此函式來消除它們。eliminateFrameIndex
— 從可能使用抽象框架索引的指令中消除抽象框架索引。emitPrologue
— 將前言程式碼插入到函式中。emitEpilogue
— 將後語程式碼插入到函式中。
指令集¶
在程式碼產生的早期階段,LLVM IR 程式碼會轉換為 SelectionDAG
,其節點是包含目標指令的 SDNode
類別的實例。SDNode
具有運算碼、運算元、類型需求和運算屬性。例如,運算是否可交換、運算是否從記憶體載入。各種運算節點類型在 include/llvm/CodeGen/SelectionDAGNodes.h
檔案中描述(ISD
命名空間中 NodeType
列舉的值)。
TableGen 使用以下目標描述 (.td
) 輸入檔案來產生大部分用於指令定義的程式碼
Target.td
— 定義Instruction
、Operand
、InstrInfo
和其他基本類別的地方。TargetSelectionDAG.td
— 由SelectionDAG
指令選擇產生器使用,包含SDTC*
類別(SelectionDAG 類型約束)、SelectionDAG
節點的定義(例如imm
、cond
、bb
、add
、fadd
、sub
)和模式支援(Pattern
、Pat
、PatFrag
、PatLeaf
、ComplexPattern
)。XXXInstrFormats.td
— 用於定義目標特定指令的模式。XXXInstrInfo.td
— 目標特定指令範本、條件碼和指令集中的指令的定義。對於架構修改,可以使用不同的檔案名稱。例如,對於具有 SSE 指令的 Pentium,此檔案為X86InstrSSE.td
,而對於具有 MMX 的 Pentium,此檔案為X86InstrMMX.td
。
還有一個目標特定的 XXX.td
檔案,其中 XXX
是目標的名稱。XXX.td
檔案包含其他 .td
輸入檔案,但其內容僅對子目標直接重要。
您應描述一個具體的目標特定類別 XXXInstrInfo
,它代表目標機器支援的機器指令。XXXInstrInfo
包含 XXXInstrDescriptor
物件的陣列,每個物件描述一個指令。指令描述符定義了
運算碼助記符
運算元的數量
隱含暫存器定義和使用的列表
目標獨立屬性(例如記憶體存取、是否可交換)
目標特定旗標
指令類別(定義於 Target.td
中)主要用作更複雜指令類別的基礎。
class Instruction {
string Namespace = "";
dag OutOperandList; // A dag containing the MI def operand list.
dag InOperandList; // A dag containing the MI use operand list.
string AsmString = ""; // The .s format to print the instruction with.
list<dag> Pattern; // Set to the DAG pattern for this instruction.
list<Register> Uses = [];
list<Register> Defs = [];
list<Predicate> Predicates = []; // predicates turned into isel match code
... remainder not shown for space ...
}
SelectionDAG
節點 (SDNode
) 應包含一個物件,該物件代表定義於 XXXInstrInfo.td
中的目標特定指令。指令物件應代表來自目標機器的架構手冊的指令(例如 SPARC 目標的 SPARC 架構手冊)。
架構手冊中的單個指令通常被建模為多個目標指令,具體取決於其運算元。例如,手冊可能會描述一個接受暫存器或立即運算元的加法指令。LLVM 目標可以使用兩個名為 ADDri
和 ADDrr
的指令來對此進行建模。
您應為每個指令類別定義一個類別,並將每個運算碼定義為該類別的子類別,並帶有適當的參數,例如運算碼和擴展運算碼的固定二進制編碼。您應將暫存器位元映射到指令中編碼它們的位元(對於 JIT)。此外,您應指定在使用自動組譯器列印時應如何列印指令。
正如 SPARC 架構手冊第 8 版中所述,指令有三種主要的 32 位元格式。格式 1 僅適用於 CALL
指令。格式 2 用於條件碼分支和 SETHI
(設定暫存器的高位元)指令。格式 3 用於其他指令。
這些格式中的每一種都在 SparcInstrFormat.td
中具有對應的類別。InstSP
是其他指令類別的基底類別。為更精確的格式指定了額外的基底類別:例如在 SparcInstrFormat.td
中,F2_1
用於 SETHI
,而 F2_2
用於分支。還有其他三個基底類別:F3_1
用於暫存器/暫存器運算,F3_2
用於暫存器/立即運算,而 F3_3
用於浮點運算。SparcInstrInfo.td
也為合成 SPARC 指令新增了基底類別 Pseudo
。
SparcInstrInfo.td
主要包含 SPARC 目標的運算元和指令定義。在 SparcInstrInfo.td
中,以下目標描述檔條目 LDrr
定義了從記憶體位址到暫存器的 Word(LD
SPARC 運算碼)的載入整數指令。第一個參數值 3 (11
2) 是此運算類別的運算值。第二個參數 (000000
2) 是 LD
/載入 Word 的特定運算值。第三個參數是輸出目的地,它是一個暫存器運算元,定義於 Register
目標描述檔 (IntRegs
) 中。
def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMrr $rs1, $rs2):$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRrr:$addr))]>;
第四個參數是輸入來源,它使用在 SparcInstrInfo.td
中較早定義的位址運算元 MEMrr
。
def MEMrr : Operand<i32> {
let PrintMethod = "printMemOperand";
let MIOperandInfo = (ops IntRegs, IntRegs);
}
第五個參數是一個字串,組譯器列印器會使用它,並且在實作組譯器列印器介面之前可以將其留空。第六個也是最後一個參數是用於在 LLVM 目標獨立程式碼產生器 中描述的 SelectionDAG 選擇階段期間匹配指令的模式。此參數在下一節 指令選擇器 中詳細介紹。
指令類別定義不會針對不同的運算元類型進行多載,因此暫存器、記憶體或立即值運算元需要單獨版本的指令。例如,要執行從立即運算元到暫存器的 Word 的載入整數指令,定義了以下指令類別
def LDri : F3_2 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMri $rs1, $simm13):$addr),
"ld [$addr], $dst",
[(set i32:$rd, (load ADDRri:$addr))]>;
為如此多相似的指令編寫這些定義可能涉及大量的剪下和貼上。在 .td
檔案中,multiclass
指令啟用建立範本,以便一次定義多個指令類別(使用 defm
指令)。例如,在 SparcInstrInfo.td
中,multiclass
模式 F3_12
被定義為每次調用 F3_12
時建立 2 個指令類別
multiclass F3_12 <string OpcStr, bits<6> Op3Val, SDNode OpNode> {
def rr : F3_1 <2, Op3Val,
(outs IntRegs:$rd), (ins IntRegs:$rs1, IntRegs:$rs1),
!strconcat(OpcStr, " $rs1, $rs2, $rd"),
[(set i32:$rd, (OpNode i32:$rs1, i32:$rs2))]>;
def ri : F3_2 <2, Op3Val,
(outs IntRegs:$rd), (ins IntRegs:$rs1, i32imm:$simm13),
!strconcat(OpcStr, " $rs1, $simm13, $rd"),
[(set i32:$rd, (OpNode i32:$rs1, simm13:$simm13))]>;
}
因此,當對 XOR
和 ADD
指令使用 defm
指令時,如下所示,它會建立四個指令物件:XORrr
、XORri
、ADDrr
和 ADDri
。
defm XOR : F3_12<"xor", 0b000011, xor>;
defm ADD : F3_12<"add", 0b000000, add>;
SparcInstrInfo.td
也包含分支指令引用的條件碼的定義。SparcInstrInfo.td
中的以下定義指示 SPARC 條件碼的位元位置。例如,第 10 位元代表整數的「大於」條件,而第 22 位元代表浮點數的「大於」條件。
def ICC_NE : ICC_VAL< 9>; // Not Equal
def ICC_E : ICC_VAL< 1>; // Equal
def ICC_G : ICC_VAL<10>; // Greater
...
def FCC_U : FCC_VAL<23>; // Unordered
def FCC_G : FCC_VAL<22>; // Greater
def FCC_UG : FCC_VAL<21>; // Unordered or Greater
...
(請注意,Sparc.h
也定義了與相同 SPARC 條件碼對應的列舉。必須注意確保 Sparc.h
中的值與 SparcInstrInfo.td
中的值對應。即,SPCC::ICC_NE = 9
,SPCC::FCC_U = 23
等等。)
指令運算元映射¶
程式碼產生器後端將指令運算元映射到指令中的欄位。每當指令編碼 Inst
中的位元被分配給沒有具體值的欄位時,預期 outs
或 ins
列表中的運算元具有匹配的名稱。然後,此運算元會填充該未定義的欄位。例如,Sparc 目標將 XNORrr
指令定義為具有三個運算元的 F3_1
格式指令:輸出 $rd
和輸入 $rs1
和 $rs2
。
def XNORrr : F3_1<2, 0b000111,
(outs IntRegs:$rd), (ins IntRegs:$rs1, IntRegs:$rs2),
"xnor $rs1, $rs2, $rd",
[(set i32:$rd, (not (xor i32:$rs1, i32:$rs2)))]>;
SparcInstrFormats.td
中的指令範本顯示 F3_1
的基底類別是 InstSP
。
class InstSP<dag outs, dag ins, string asmstr, list<dag> pattern> : Instruction {
field bits<32> Inst;
let Namespace = "SP";
bits<2> op;
let Inst{31-30} = op;
dag OutOperandList = outs;
dag InOperandList = ins;
let AsmString = asmstr;
let Pattern = pattern;
}
InstSP
定義了 op
欄位,並使用它來定義指令的第 30 和 31 位元,但沒有為其分配值。
class F3<dag outs, dag ins, string asmstr, list<dag> pattern>
: InstSP<outs, ins, asmstr, pattern> {
bits<5> rd;
bits<6> op3;
bits<5> rs1;
let op{1} = 1; // Op = 2 or 3
let Inst{29-25} = rd;
let Inst{24-19} = op3;
let Inst{18-14} = rs1;
}
F3
定義了 rd
、op3
和 rs1
欄位,並在指令中使用它們,並且再次沒有分配值。
class F3_1<bits<2> opVal, bits<6> op3val, dag outs, dag ins,
string asmstr, list<dag> pattern> : F3<outs, ins, asmstr, pattern> {
bits<8> asi = 0; // asi not currently used
bits<5> rs2;
let op = opVal;
let op3 = op3val;
let Inst{13} = 0; // i field = 0
let Inst{12-5} = asi; // address space identifier
let Inst{4-0} = rs2;
}
F3_1
為 op
和 op3
欄位分配了一個值,並定義了 rs2
欄位。因此,F3_1
格式的指令將需要 rd
、rs1
和 rs2
的定義,以便完全指定指令編碼。
然後,XNORrr
指令在其 OutOperandList 和 InOperandList 中提供這三個運算元,它們綁定到對應的欄位,從而完成指令編碼。
對於某些指令,單個運算元可能包含子運算元。如前所示,指令 LDrr
使用類型為 MEMrr
的輸入運算元。此運算元類型包含兩個暫存器子運算元,由 MIOperandInfo
值定義為 (ops IntRegs, IntRegs)
。
def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMrr $rs1, $rs2):$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRrr:$addr))]>;
由於此指令也是 F3_1
格式,因此它也將期望名為 rd
、rs1
和 rs2
的運算元。為了允許這樣做,複雜的運算元可以選擇性地為其每個子運算元命名。在此範例中,MEMrr
的第一個子運算元被命名為 $rs1
,第二個子運算元被命名為 $rs2
,並且整個運算元也被命名為 $addr
。
當特定指令未使用指令格式定義的所有運算元時,可以改為將常數值綁定到一個或所有運算元。例如,RDASR
指令僅採用單個暫存器運算元,因此我們將常數零分配給 rs2
let rs2 = 0 in
def RDASR : F3_1<2, 0b101000,
(outs IntRegs:$rd), (ins ASRRegs:$rs1),
"rd $rs1, $rd", []>;
指令運算元名稱映射¶
TableGen 還將產生一個名為 getNamedOperandIdx() 的函式,可用於根據運算元的 TableGen 名稱在 MachineInstr 中查找運算元的索引。在指令的 TableGen 定義中設定 UseNamedOperandTable 位元會將其所有運算元新增到列舉 llvm::XXX:OpName,並為其在 OperandMap 表格中新增一個條目,可以使用 getNamedOperandIdx() 查詢該條目
int DstIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::dst); // => 0
int BIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::b); // => 1
int CIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::c); // => 2
int DIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::d); // => -1
...
OpName 列舉中的條目直接從 TableGen 定義中取得,因此具有小寫名稱的運算元在列舉中將具有小寫條目。
若要在您的後端中包含 getNamedOperandIdx() 函式,您需要在 XXXInstrInfo.cpp 和 XXXInstrInfo.h 中定義一些前處理器巨集。例如
XXXInstrInfo.cpp
// For getNamedOperandIdx() function definition.
#define GET_INSTRINFO_NAMED_OPS
#include "XXXGenInstrInfo.inc"
XXXInstrInfo.h
// For OpName enum and getNamedOperandIdx declaration.
#define GET_INSTRINFO_OPERAND_ENUM
#include "XXXGenInstrInfo.inc"
指令運算元類型¶
TableGen 還將產生一個列舉,其中包含後端中定義的所有具名 Operand 類型,位於 llvm::XXX::OpTypes 命名空間中。一些常見的立即 Operand 類型(例如 i8、i32、i64、f32、f64)在 include/llvm/Target/Target.td
中為所有目標定義,並且在每個目標的 OpTypes 列舉中可用。此外,只有具名的 Operand 類型才會出現在列舉中:匿名類型會被忽略。例如,X86 後端定義了 brtarget
和 brtarget8
,它們都是 TableGen Operand
類別的實例,代表分支目標運算元
def brtarget : Operand<OtherVT>;
def brtarget8 : Operand<OtherVT>;
這會產生
namespace X86 {
namespace OpTypes {
enum OperandType {
...
brtarget,
brtarget8,
...
i32imm,
i64imm,
...
OPERAND_TYPE_LIST_END
} // End namespace OpTypes
} // End namespace X86
在典型的 TableGen 方式中,若要使用列舉,您需要定義一個前處理器巨集
#define GET_INSTRINFO_OPERAND_TYPES_ENUM // For OpTypes enum
#include "XXXGenInstrInfo.inc"
指令排程¶
可以使用 MCDesc::getSchedClass() 查詢指令行程。該值可以由 TableGen 在 XXXGenInstrInfo.inc 中產生的 llvm::XXX::Sched 命名空間中的列舉命名。排程類別的名稱與 XXXSchedule.td 中提供的名稱相同,外加一個預設的 NoItinerary 類別。
排程模型由 TableGen 的 SubtargetEmitter 使用 CodeGenSchedModels
類別產生。這與指定機器資源使用的行程方法不同。utils/schedcover.py
工具可用於確定哪些指令已被排程模型描述涵蓋,哪些沒有。第一步是使用以下指令建立輸出檔案。然後在輸出檔案上執行 schedcover.py
$ <src>/utils/schedcover.py <build>/lib/Target/AArch64/tblGenSubtarget.with
instruction, default, CortexA53Model, CortexA57Model, CycloneModel, ExynosM3Model, FalkorModel, KryoModel, ThunderX2T99Model, ThunderXT8XModel
ABSv16i8, WriteV, , , CyWriteV3, M3WriteNMISC1, FalkorWr_2VXVY_2cyc, KryoWrite_2cyc_XY_XY_150ln, ,
ABSv1i64, WriteV, , , CyWriteV3, M3WriteNMISC1, FalkorWr_1VXVY_2cyc, KryoWrite_2cyc_XY_noRSV_67ln, ,
...
若要捕獲從產生排程模型輸出的偵錯資訊,請變更到適當的目標目錄並使用以下命令:帶有 subtarget-emitter
偵錯選項的命令
$ <build>/bin/llvm-tblgen -debug-only=subtarget-emitter -gen-subtarget \
-I <src>/lib/Target/<target> -I <src>/include \
-I <src>/lib/Target <src>/lib/Target/<target>/<target>.td \
-o <build>/lib/Target/<target>/<target>GenSubtargetInfo.inc.tmp \
> tblGenSubtarget.dbg 2>&1
其中 <build>
是組建目錄,src
是來源目錄,而 <target>
是目標的名稱。為了再次檢查上述命令是否是需要的命令,可以使用以下命令從組建中捕獲確切的 TableGen 命令:
$ VERBOSE=1 make ...
並在輸出中搜尋 llvm-tblgen
命令。
指令關係映射¶
此 TableGen 功能用於將指令彼此關聯。當您有多種指令格式並且需要在指令選擇後在它們之間切換時,此功能特別有用。整個功能由關係模型驅動,關係模型可以使用 InstrMapping
類別作為基底在 XXXInstrInfo.td
檔案中定義。TableGen 解析所有模型,並使用指定的資訊產生指令關係映射。關係映射作為表格發射到 XXXGenInstrInfo.inc
檔案中,以及用於查詢它們的函式。有關如何使用此功能的詳細資訊,請參閱 如何使用指令映射。
實作 TargetInstrInfo
的子類別¶
最後一個步驟是手動編寫 XXXInstrInfo
的部分程式碼,它實作了 TargetInstrInfo.h
中描述的介面(請參閱 TargetInstrInfo 類別)。除非被覆寫,否則這些函式會傳回 0
或布林值,或者它們會斷言失敗。以下是在 SparcInstrInfo.cpp
中為 SPARC 實作覆寫的函式列表:
isLoadFromStackSlot
— 如果指定的機器指令是從堆疊槽直接載入,則傳回目的地暫存器的編號和堆疊槽的FrameIndex
。isStoreToStackSlot
— 如果指定的機器指令是直接儲存到堆疊槽,則傳回目的地暫存器的編號和堆疊槽的FrameIndex
。copyPhysReg
— 在一對實體暫存器之間複製值。storeRegToStackSlot
— 將暫存器值儲存到堆疊槽。loadRegFromStackSlot
— 從堆疊槽載入暫存器值。storeRegToAddr
— 將暫存器值儲存到記憶體。loadRegFromAddr
— 從記憶體載入暫存器值。foldMemoryOperand
— 嘗試將任何載入或儲存指令的指令與指定運算元的指令組合。
分支摺疊和 If 轉換¶
可以透過組合指令或消除永遠不會到達的指令來提高效能。XXXInstrInfo
中的 analyzeBranch
方法可以實作來檢查條件指令並移除不必要的指令。analyzeBranch
查看機器基本區塊 (MBB) 的結尾,以尋找改進的機會,例如分支摺疊和 If 轉換。BranchFolder
和 IfConverter
機器函式傳遞(請參閱 lib/CodeGen
目錄中的原始碼檔案 BranchFolding.cpp
和 IfConversion.cpp
)呼叫 analyzeBranch
以改進代表指令的控制流程圖。
可以檢查 analyzeBranch
的幾個實作(適用於 ARM、Alpha 和 X86)作為您自己的 analyzeBranch
實作的模型。由於 SPARC 沒有實作有用的 analyzeBranch
,因此以下顯示 ARM 目標實作。
analyzeBranch
傳回布林值並採用四個參數
MachineBasicBlock &MBB
— 要檢查的傳入區塊。MachineBasicBlock *&TBB
— 傳回的目的地區塊。對於評估為 true 的條件分支,TBB
是目的地。MachineBasicBlock *&FBB
— 對於評估為 false 的條件分支,FBB
作為目的地傳回。std::vector<MachineOperand> &Cond
— 用於評估條件分支的條件的運算元列表。
在最簡單的情況下,如果一個區塊在沒有分支的情況下結束,那麼它會直接執行到後續區塊。對於 TBB
或 FBB
皆未指定目標區塊,因此這兩個參數都會回傳 NULL
。analyzeBranch
的開頭(請參閱下方 ARM 目標的程式碼)顯示了函數參數以及最簡單情況的程式碼。
bool ARMInstrInfo::analyzeBranch(MachineBasicBlock &MBB,
MachineBasicBlock *&TBB,
MachineBasicBlock *&FBB,
std::vector<MachineOperand> &Cond) const
{
MachineBasicBlock::iterator I = MBB.end();
if (I == MBB.begin() || !isUnpredicatedTerminator(--I))
return false;
如果一個區塊以單一無條件分支指令結束,則 analyzeBranch
(如下所示)應在 TBB
參數中回傳該分支的目標。
if (LastOpc == ARM::B || LastOpc == ARM::tB) {
TBB = LastInst->getOperand(0).getMBB();
return false;
}
如果一個區塊以兩個無條件分支結束,則永遠不會執行到第二個分支。在這種情況下,如下所示,移除最後一個分支指令,並在 TBB
參數中回傳倒數第二個分支。
if ((SecondLastOpc == ARM::B || SecondLastOpc == ARM::tB) &&
(LastOpc == ARM::B || LastOpc == ARM::tB)) {
TBB = SecondLastInst->getOperand(0).getMBB();
I = LastInst;
I->eraseFromParent();
return false;
}
一個區塊可能以單一條件分支指令結束,如果條件評估為 false,則會直接執行到後續區塊。在這種情況下,analyzeBranch
(如下所示)應在 TBB
參數中回傳該條件分支的目標,並在 Cond
參數中回傳用於評估條件的運算元列表。
if (LastOpc == ARM::Bcc || LastOpc == ARM::tBcc) {
// Block ends with fall-through condbranch.
TBB = LastInst->getOperand(0).getMBB();
Cond.push_back(LastInst->getOperand(1));
Cond.push_back(LastInst->getOperand(2));
return false;
}
如果一個區塊以條件分支和隨後的無條件分支結束,則 analyzeBranch
(如下所示)應在 TBB
參數中回傳條件分支目標(假設它對應於條件評估的 “true
”),並在 FBB
中回傳無條件分支目標(對應於條件評估的 “false
”)。用於評估條件的運算元列表應在 Cond
參數中回傳。
unsigned SecondLastOpc = SecondLastInst->getOpcode();
if ((SecondLastOpc == ARM::Bcc && LastOpc == ARM::B) ||
(SecondLastOpc == ARM::tBcc && LastOpc == ARM::tB)) {
TBB = SecondLastInst->getOperand(0).getMBB();
Cond.push_back(SecondLastInst->getOperand(1));
Cond.push_back(SecondLastInst->getOperand(2));
FBB = LastInst->getOperand(0).getMBB();
return false;
}
對於最後兩種情況(以單一條件分支結束或以一個條件分支和一個無條件分支結束),在 Cond
參數中回傳的運算元可以傳遞給其他指令的方法,以建立新的分支或執行其他操作。analyzeBranch
的實作需要輔助方法 removeBranch
和 insertBranch
來管理後續操作。
analyzeBranch
在大多數情況下應回傳 false,表示成功。analyzeBranch
僅應在該方法對於該如何處理感到困惑時才回傳 true,例如,如果一個區塊有三個終止分支。analyzeBranch
如果遇到它無法處理的終止符(例如間接分支),則可能會回傳 true。
指令選擇器¶
LLVM 使用 SelectionDAG
來表示 LLVM IR 指令,而 SelectionDAG
的節點理想情況下代表原生目標指令。在程式碼產生期間,會執行指令選擇傳遞 (instruction selection pass) 以將非原生的 DAG 指令轉換為原生目標特定的指令。XXXISelDAGToDAG.cpp
中描述的傳遞用於比對模式並執行 DAG 到 DAG 的指令選擇。或者,可以定義一個傳遞(在 XXXBranchSelector.cpp
中)以對分支指令執行類似的 DAG 到 DAG 操作。稍後,XXXISelLowering.cpp
中的程式碼會替換或移除 SelectionDAG
中不原生支援(合法化)的操作和資料類型。
TableGen 使用以下目標描述輸入檔產生指令選擇的程式碼
XXXInstrInfo.td
— 包含目標特定指令集中指令的定義,產生XXXGenDAGISel.inc
,其包含在XXXISelDAGToDAG.cpp
中。XXXCallingConv.td
— 包含目標架構的呼叫和回傳值慣例,並且它產生XXXGenCallingConv.inc
,其包含在XXXISelLowering.cpp
中。
指令選擇傳遞的實作必須包含一個標頭,該標頭宣告 FunctionPass
類別或 FunctionPass
的子類別。在 XXXTargetMachine.cpp
中,Pass Manager (PM) 應將每個指令選擇傳遞新增到要執行的傳遞佇列中。
LLVM 靜態編譯器 (llc
) 是一個用於視覺化 DAG 內容的絕佳工具。若要在特定處理階段之前或之後顯示 SelectionDAG
,請使用 llc
的命令列選項,詳情請參閱 SelectionDAG 指令選擇程序。
為了描述指令選擇器的行為,您應該在 XXXInstrInfo.td
中指令定義的最後一個參數中新增用於將 LLVM 程式碼降低 (lowering) 為 SelectionDAG
的模式。例如,在 SparcInstrInfo.td
中,此條目定義了暫存器儲存操作,最後一個參數描述了具有儲存 DAG 運算子的模式。
def STrr : F3_1< 3, 0b000100, (outs), (ins MEMrr:$addr, IntRegs:$src),
"st $src, [$addr]", [(store i32:$src, ADDRrr:$addr)]>;
ADDRrr
是一種記憶體模式,也在 SparcInstrInfo.td
中定義
def ADDRrr : ComplexPattern<i32, 2, "SelectADDRrr", [], []>;
ADDRrr
的定義參考了 SelectADDRrr
,後者是在指令選擇器實作(例如 SparcISelDAGToDAG.cpp
)中定義的函數。
在 lib/Target/TargetSelectionDAG.td
中,儲存的 DAG 運算子定義如下
def store : PatFrag<(ops node:$val, node:$ptr),
(unindexedstore node:$val, node:$ptr)> {
let IsStore = true;
let IsTruncStore = false;
}
XXXInstrInfo.td
也產生(在 XXXGenDAGISel.inc
中)SelectCode
方法,該方法用於為指令呼叫適當的處理方法。在此範例中,SelectCode
為 ISD::STORE
運算碼呼叫 Select_ISD_STORE
。
SDNode *SelectCode(SDValue N) {
...
MVT::ValueType NVT = N.getNode()->getValueType(0);
switch (N.getOpcode()) {
case ISD::STORE: {
switch (NVT) {
default:
return Select_ISD_STORE(N);
break;
}
break;
}
...
STrr
的模式已比對,因此在 XXXGenDAGISel.inc
中的其他地方,為 Select_ISD_STORE
建立了 STrr
的程式碼。Emit_22
方法也在 XXXGenDAGISel.inc
中產生,以完成此指令的處理。
SDNode *Select_ISD_STORE(const SDValue &N) {
SDValue Chain = N.getOperand(0);
if (Predicate_store(N.getNode())) {
SDValue N1 = N.getOperand(1);
SDValue N2 = N.getOperand(2);
SDValue CPTmp0;
SDValue CPTmp1;
// Pattern: (st:void i32:i32:$src,
// ADDRrr:i32:$addr)<<P:Predicate_store>>
// Emits: (STrr:void ADDRrr:i32:$addr, IntRegs:i32:$src)
// Pattern complexity = 13 cost = 1 size = 0
if (SelectADDRrr(N, N2, CPTmp0, CPTmp1) &&
N1.getNode()->getValueType(0) == MVT::i32 &&
N2.getNode()->getValueType(0) == MVT::i32) {
return Emit_22(N, SP::STrr, CPTmp0, CPTmp1);
}
...
SelectionDAG 合法化階段¶
合法化階段將 DAG 轉換為使用目標原生支援的類型和操作。對於原生不支援的類型和操作,您需要將程式碼新增到目標特定的 XXXTargetLowering
實作中,以將不支援的類型和操作轉換為支援的類型和操作。
在 XXXTargetLowering
類別的建構子中,首先使用 addRegisterClass
方法來指定支援哪些類型以及哪些暫存器類別與它們相關聯。暫存器類別的程式碼是由 TableGen 從 XXXRegisterInfo.td
產生,並放置在 XXXGenRegisterInfo.h.inc
中。例如,SparcTargetLowering 類別的建構子實作(在 SparcISelLowering.cpp
中)以以下程式碼開頭
addRegisterClass(MVT::i32, SP::IntRegsRegisterClass);
addRegisterClass(MVT::f32, SP::FPRegsRegisterClass);
addRegisterClass(MVT::f64, SP::DFPRegsRegisterClass);
您應該檢查 ISD
命名空間中的節點類型(include/llvm/CodeGen/SelectionDAGNodes.h
),並確定目標原生支援哪些操作。對於不具有原生支援的操作,請將回呼新增到 XXXTargetLowering
類別的建構子中,以便指令選擇程序知道該怎麼做。TargetLowering
類別回呼方法(在 llvm/Target/TargetLowering.h
中宣告)為
setOperationAction
— 一般操作。setLoadExtAction
— 擴充載入。setTruncStoreAction
— 截斷儲存。setIndexedLoadAction
— 索引載入。setIndexedStoreAction
— 索引儲存。setConvertAction
— 類型轉換。setCondCodeAction
— 支援給定的條件碼。
注意:在較舊的版本中,使用 setLoadXAction
而不是 setLoadExtAction
。此外,在較舊的版本中,可能不支援 setCondCodeAction
。檢查您的版本以查看具體支援哪些方法。
這些回呼用於確定操作是否適用於指定的類型(或類型)。在所有情況下,第三個參數都是 LegalAction
類型列舉值:Promote
、Expand
、Custom
或 Legal
。SparcISelLowering.cpp
包含所有四個 LegalAction
值的範例。
提升 (Promote)¶
對於給定類型不具原生支援的操作,可以將指定的類型提升為更大的支援類型。例如,SPARC 不支援布林值 (i1
類型) 的符號擴充載入,因此在 SparcISelLowering.cpp
中,下方的第三個參數 Promote
會在載入之前將 i1
類型的值變更為更大的類型。
setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
展開 (Expand)¶
對於不具原生支援的類型,值可能需要進一步分解,而不是提升。對於不具原生支援的操作,可以使用其他操作的組合來達到類似的效果。在 SPARC 中,浮點正弦和餘弦三角函數操作透過展開為其他操作來支援,如 setOperationAction
的第三個參數 Expand
所示
setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);
自訂 (Custom)¶
對於某些操作,簡單的類型提升或操作展開可能不足。在某些情況下,必須實作特殊的內建函數。
例如,常數值可能需要特殊處理,或者操作可能需要在堆疊中溢出和還原暫存器,並與暫存器分配器一起使用。
如在下方的 SparcISelLowering.cpp
程式碼中所見,若要執行從浮點值到帶正負號整數的類型轉換,首先應使用 Custom
作為第三個參數呼叫 setOperationAction
setOperationAction(ISD::FP_TO_SINT, MVT::i32, Custom);
在 LowerOperation
方法中,對於每個 Custom
操作,應新增一個 case 陳述式以指示要呼叫哪個函數。在以下程式碼中,FP_TO_SINT
運算碼將呼叫 LowerFP_TO_SINT
方法
SDValue SparcTargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG) {
switch (Op.getOpcode()) {
case ISD::FP_TO_SINT: return LowerFP_TO_SINT(Op, DAG);
...
}
}
最後,實作 LowerFP_TO_SINT
方法,使用 FP 暫存器將浮點值轉換為整數。
static SDValue LowerFP_TO_SINT(SDValue Op, SelectionDAG &DAG) {
assert(Op.getValueType() == MVT::i32);
Op = DAG.getNode(SPISD::FTOI, MVT::f32, Op.getOperand(0));
return DAG.getNode(ISD::BITCAST, MVT::i32, Op);
}
合法 (Legal)¶
Legal
LegalizeAction
列舉值僅表示操作是原生支援的。Legal
代表預設條件,因此很少使用。在 SparcISelLowering.cpp
中,只有 SPARC v9 原生支援 CTPOP
(計算整數中設定位元的操作)的動作。以下程式碼為非 v9 SPARC 實作啟用 Expand
轉換技術。
setOperationAction(ISD::CTPOP, MVT::i32, Expand);
...
if (TM.getSubtarget<SparcSubtarget>().isV9())
setOperationAction(ISD::CTPOP, MVT::i32, Legal);
呼叫慣例¶
為了支援目標特定的呼叫慣例,XXXGenCallingConv.td
使用在 lib/Target/TargetCallingConv.td
中定義的介面(例如 CCIfType
和 CCAssignToReg
)。TableGen 可以採用目標描述符檔案 XXXGenCallingConv.td
並產生標頭檔 XXXGenCallingConv.inc
,後者通常包含在 XXXISelLowering.cpp
中。您可以使用 TargetCallingConv.td
中的介面來指定
參數配置的順序。
參數和回傳值放置的位置(亦即,在堆疊上或在暫存器中)。
可以使用哪些暫存器。
呼叫者或被呼叫者是否解除堆疊。
以下範例示範了 CCIfType
和 CCAssignToReg
介面的使用。如果 CCIfType
述詞為 true(亦即,如果目前的引數類型為 f32
或 f64
),則執行該動作。在這種情況下,CCAssignToReg
動作將引數值指派給第一個可用的暫存器:R0
或 R1
。
CCIfType<[f32,f64], CCAssignToReg<[R0, R1]>>
SparcCallingConv.td
包含目標特定回傳值呼叫慣例 (RetCC_Sparc32
) 和基本 32 位元 C 呼叫慣例 (CC_Sparc32
) 的定義。RetCC_Sparc32
的定義(如下所示)指示了哪些暫存器用於指定的純量回傳類型。單精度浮點數回傳到暫存器 F0
,雙精度浮點數回傳到暫存器 D0
。32 位元整數回傳到暫存器 I0
或 I1
。
def RetCC_Sparc32 : CallingConv<[
CCIfType<[i32], CCAssignToReg<[I0, I1]>>,
CCIfType<[f32], CCAssignToReg<[F0]>>,
CCIfType<[f64], CCAssignToReg<[D0]>>
]>;
SparcCallingConv.td
中 CC_Sparc32
的定義引入了 CCAssignToStack
,後者將值指派給具有指定大小和對齊方式的堆疊槽。在以下範例中,第一個參數 4 表示槽的大小,第二個參數也是 4,表示沿 4 位元組單位的堆疊對齊方式。(特殊情況:如果大小為零,則使用 ABI 大小;如果對齊方式為零,則使用 ABI 對齊方式。)
def CC_Sparc32 : CallingConv<[
// All arguments get passed in integer registers if there is space.
CCIfType<[i32, f32, f64], CCAssignToReg<[I0, I1, I2, I3, I4, I5]>>,
CCAssignToStack<4, 4>
]>;
CCDelegateTo
是另一個常用的介面,它嘗試尋找指定的子呼叫慣例,如果找到符合的,則會調用它。在以下範例(在 X86CallingConv.td
中)中,RetCC_X86_32_C
的定義以 CCDelegateTo
結尾。在將目前的值指派給暫存器 ST0
或 ST1
之後,會調用 RetCC_X86Common
。
def RetCC_X86_32_C : CallingConv<[
CCIfType<[f32], CCAssignToReg<[ST0, ST1]>>,
CCIfType<[f64], CCAssignToReg<[ST0, ST1]>>,
CCDelegateTo<RetCC_X86Common>
]>;
CCIfCC
是一個介面,它嘗試將給定的名稱與目前的呼叫慣例比對。如果該名稱識別出目前的呼叫慣例,則會調用指定的動作。在以下範例(在 X86CallingConv.td
中)中,如果正在使用 Fast
呼叫慣例,則調用 RetCC_X86_32_Fast
。如果正在使用 SSECall
呼叫慣例,則調用 RetCC_X86_32_SSE
。
def RetCC_X86_32 : CallingConv<[
CCIfCC<"CallingConv::Fast", CCDelegateTo<RetCC_X86_32_Fast>>,
CCIfCC<"CallingConv::X86_SSECall", CCDelegateTo<RetCC_X86_32_SSE>>,
CCDelegateTo<RetCC_X86_32_C>
]>;
CCAssignToRegAndStack
與 CCAssignToReg
相同,但也會在使用某些暫存器時配置堆疊槽。基本上,它的運作方式如下:CCIf<CCAssignToReg<regList>, CCAssignToStack<size, align>>
。
class CCAssignToRegAndStack<list<Register> regList, int size, int align>
: CCAssignToReg<regList> {
int Size = size;
int Align = align;
}
其他呼叫慣例介面包括
CCIf <predicate, action>
— 如果述詞符合,則套用該動作。CCIfInReg <action>
— 如果引數標記有 “inreg
” 屬性,則套用該動作。CCIfNest <action>
— 如果引數標記有 “nest
” 屬性,則套用該動作。CCIfNotVarArg <action>
— 如果目前的函數不接受可變數量的引數,則套用該動作。CCAssignToRegWithShadow <registerList, shadowList>
— 類似於CCAssignToReg
,但具有暫存器的陰影列表。CCPassByVal <size, align>
— 將值指派給具有最小指定大小和對齊方式的堆疊槽。CCPromoteToType <type>
— 將目前的值提升為指定的類型。CallingConv <[actions]>
— 定義每個支援的呼叫慣例。
組合語言印表機¶
在程式碼發射 (code emission) 階段,程式碼產生器可能會利用 LLVM 傳遞來產生組合語言輸出。若要執行此操作,您需要實作一個印表機的程式碼,該印表機將 LLVM IR 轉換為目標機器的 GAS 格式組合語言,使用以下步驟
為您的目標定義所有組合語言字串,將它們新增到
XXXInstrInfo.td
檔案中定義的指令。(請參閱 指令集。)TableGen 將產生一個輸出檔案 (XXXGenAsmWriter.inc
),其中包含XXXAsmPrinter
類別的printInstruction
方法的實作。撰寫
XXXTargetAsmInfo.h
,其中包含XXXTargetAsmInfo
類別(TargetAsmInfo
的子類別)的簡要宣告。撰寫
XXXTargetAsmInfo.cpp
,其中包含TargetAsmInfo
屬性的目標特定值,有時也包含方法的新的實作。撰寫
XXXAsmPrinter.cpp
,其中實作執行 LLVM 到組合語言轉換的AsmPrinter
類別。
XXXTargetAsmInfo.h
中的程式碼通常是 XXXTargetAsmInfo
類別的簡單宣告,用於 XXXTargetAsmInfo.cpp
中。同樣地,XXXTargetAsmInfo.cpp
通常有一些 XXXTargetAsmInfo
替換值的宣告,這些值會覆寫 TargetAsmInfo.cpp
中的預設值。例如,在 SparcTargetAsmInfo.cpp
中
SparcTargetAsmInfo::SparcTargetAsmInfo(const SparcTargetMachine &TM) {
Data16bitsDirective = "\t.half\t";
Data32bitsDirective = "\t.word\t";
Data64bitsDirective = 0; // .xword is only supported by V9.
ZeroDirective = "\t.skip\t";
CommentString = "!";
ConstantPoolSection = "\t.section \".rodata\",#alloc\n";
}
X86 組合語言印表機實作 (X86TargetAsmInfo
) 是一個範例,其中目標特定的 TargetAsmInfo
類別使用了覆寫的方法:ExpandInlineAsm
。
在 XXXAsmPrinter.cpp
中撰寫了 AsmPrinter
的目標特定實作,它實作了將 LLVM 轉換為可列印組合語言的 AsmPrinter
類別。該實作必須包含以下標頭,這些標頭具有 AsmPrinter
和 MachineFunctionPass
類別的宣告。MachineFunctionPass
是 FunctionPass
的子類別。
#include "llvm/CodeGen/AsmPrinter.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
作為 FunctionPass
,AsmPrinter
首先呼叫 doInitialization
來設定 AsmPrinter
。在 SparcAsmPrinter
中,實例化了一個 Mangler
物件來處理變數名稱。
在 XXXAsmPrinter.cpp
中,必須為 XXXAsmPrinter
實作 runOnMachineFunction
方法(在 MachineFunctionPass
中宣告)。在 MachineFunctionPass
中,runOnFunction
方法調用 runOnMachineFunction
。runOnMachineFunction
的目標特定實作有所不同,但通常會執行以下操作來處理每個機器函數
呼叫
SetupMachineFunction
以執行初始化。呼叫
EmitConstantPool
以列印(到輸出流)已溢出到記憶體的常數。呼叫
EmitJumpTableInfo
以列印目前函數使用的跳躍表。列印目前函數的標籤。
列印函數的程式碼,包括基本區塊標籤和指令的組合語言(使用
printInstruction
)
XXXAsmPrinter
實作也必須包含由 TableGen 產生的程式碼,該程式碼輸出在 XXXGenAsmWriter.inc
檔案中。XXXGenAsmWriter.inc
中的程式碼包含 printInstruction
方法的實作,該方法可能會呼叫這些方法
printOperand
printMemOperand
printCCOperand
(用於條件陳述式)printDataDirective
printDeclare
printImplicitDef
printInlineAsm
AsmPrinter.cpp
中的 printDeclare
、printImplicitDef
、printInlineAsm
和 printLabel
的實作通常足以列印組合語言,而不需要被覆寫。
printOperand
方法是使用針對運算元類型(暫存器、立即值、基本區塊、外部符號、全域位址、常數池索引或跳躍表索引)的長 switch
/case
陳述式實作的。對於具有記憶體位址運算元的指令,應實作 printMemOperand
方法以產生正確的輸出。同樣地,應使用 printCCOperand
來列印條件運算元。
應在 XXXAsmPrinter
中覆寫 doFinalization
,並且應呼叫它以關閉組合語言印表機。在 doFinalization
期間,全域變數和常數會列印到輸出。
子目標支援¶
子目標支援用於告知程式碼產生程序給定晶片組的指令集變體。例如,提供的 LLVM SPARC 實作涵蓋了 SPARC 微處理器架構的三個主要版本:Version 8(V8,它是 32 位元架構)、Version 9(V9,64 位元架構)和 UltraSPARC 架構。V8 有 16 個雙精度浮點暫存器,也可以用作 32 個單精度或 8 個四精度暫存器。V8 也純粹是大端序。V9 有 32 個雙精度浮點暫存器,也可以用作 16 個四精度暫存器,但不能用作單精度暫存器。UltraSPARC 架構結合了 V9 和 UltraSPARC Visual Instruction Set 擴充功能。
如果需要子目標支援,您應該為您的架構實作目標特定的 XXXSubtarget
類別。此類別應處理命令列選項 -mcpu=
和 -mattr=
。
TableGen 使用 `Target.td
` 和 `Sparc.td
` 檔案中的定義,來產生 `SparcGenSubtarget.inc
` 中的程式碼。在 `Target.td
` 中(如下所示),定義了 `SubtargetFeature
` 介面。`SubtargetFeature
` 介面的前 4 個字串參數依序為:功能名稱、由該功能設定的 XXXSubtarget 欄位、XXXSubtarget 欄位的值,以及功能的描述。(第五個參數是一個列表,列出隱含存在的功能,其預設值為空陣列。)
如果欄位的值是字串 "true" 或 "false",則該欄位會被視為布林值 (bool),而且應該只有一個 SubtargetFeature 參考它。否則,它會被視為整數。整數值可以是列舉常數的名稱。如果多個功能使用相同的整數欄位,該欄位將會被設定為所有啟用且共用該欄位的功能之最大值。
class SubtargetFeature<string n, string f, string v, string d,
list<SubtargetFeature> i = []> {
string Name = n;
string FieldName = f;
string Value = v;
string Desc = d;
list<SubtargetFeature> Implies = i;
}
在 `Sparc.td
` 檔案中,`SubtargetFeature
` 被用來定義以下功能。
def FeatureV9 : SubtargetFeature<"v9", "IsV9", "true",
"Enable SPARC-V9 instructions">;
def FeatureV8Deprecated : SubtargetFeature<"deprecated-v8",
"UseV8DeprecatedInsts", "true",
"Enable deprecated V8 instructions in V9 mode">;
def FeatureVIS : SubtargetFeature<"vis", "IsVIS", "true",
"Enable UltraSPARC Visual Instruction Set extensions">;
在 `Sparc.td
` 檔案的其他地方,定義了 `Proc
` 類別,然後用它來定義特定的 SPARC 處理器子類型,這些子類型可能具有先前描述的功能。
class Proc<string Name, list<SubtargetFeature> Features>
: Processor<Name, NoItineraries, Features>;
def : Proc<"generic", []>;
def : Proc<"v8", []>;
def : Proc<"supersparc", []>;
def : Proc<"sparclite", []>;
def : Proc<"f934", []>;
def : Proc<"hypersparc", []>;
def : Proc<"sparclite86x", []>;
def : Proc<"sparclet", []>;
def : Proc<"tsc701", []>;
def : Proc<"v9", [FeatureV9]>;
def : Proc<"ultrasparc", [FeatureV9, FeatureV8Deprecated]>;
def : Proc<"ultrasparc3", [FeatureV9, FeatureV8Deprecated]>;
def : Proc<"ultrasparc3-vis", [FeatureV9, FeatureV8Deprecated, FeatureVIS]>;
從 `Target.td
` 和 `Sparc.td
` 檔案中,產生的 `SparcGenSubtarget.inc
` 檔案指定了列舉值 (enum values) 以識別功能、常數陣列以表示 CPU 功能和 CPU 子類型,以及 `ParseSubtargetFeatures
` 方法,該方法解析功能字串,以設定指定的子目標選項。產生的 `SparcGenSubtarget.inc
` 檔案應該包含在 `SparcSubtarget.cpp
` 中。`XXXSubtarget
` 方法的目標特定實作應遵循以下虛擬碼:
XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
// Set the default features
// Determine default and user specified characteristics of the CPU
// Call ParseSubtargetFeatures(FS, CPU) to parse the features string
// Perform any additional operations
}
JIT 支援¶
目標機器的實作可選擇性地包含即時 (JIT) 程式碼產生器,該產生器將機器碼和輔助結構發射為二進制輸出,可以直接寫入記憶體。要做到這一點,請執行以下步驟來實作 JIT 程式碼產生:
編寫一個 `
XXXCodeEmitter.cpp
` 檔案,其中包含一個機器函數 pass,用於將目標機器指令轉換為可重定位的機器碼。編寫一個 `
XXXJITInfo.cpp
` 檔案,該檔案實作 JIT 介面,用於目標特定的程式碼產生活動,例如發射機器碼和樁 (stubs)。修改 `
XXXTargetMachine
`,使其透過其 `getJITInfo
` 方法提供 `TargetJITInfo
` 物件。
有幾種不同的方法可以編寫 JIT 支援程式碼。例如,TableGen 和目標描述符檔案可以用於建立 JIT 程式碼產生器,但不是強制性的。對於 Alpha 和 PowerPC 目標機器,TableGen 用於產生 `XXXGenCodeEmitter.inc
`,其中包含機器指令的二進制編碼和用於存取這些程式碼的 `getBinaryCodeForInstr
` 方法。其他 JIT 實作則沒有。
`XXXJITInfo.cpp
` 和 `XXXCodeEmitter.cpp
` 都必須包含 `llvm/CodeGen/MachineCodeEmitter.h
` 標頭檔,該檔案定義了 `MachineCodeEmitter
` 類別,其中包含用於多個回呼函數的程式碼,這些函數將資料(以位元組、字組、字串等形式)寫入輸出流。
機器碼發射器¶
在 `XXXCodeEmitter.cpp
` 中,`Emitter
` 類別的目標特定實作被實作為函數 pass(`MachineFunctionPass
` 的子類別)。`runOnMachineFunction
` 的目標特定實作(由 `MachineFunctionPass
` 中的 `runOnFunction
` 調用)遍歷 `MachineBasicBlock
`,並調用 `emitInstruction
` 來處理每個指令並發射二進制碼。`emitInstruction
` 主要使用在 `XXXInstrInfo.h
` 中定義的指令類型上的 case 語句來實作。例如,在 `X86CodeEmitter.cpp
` 中,`emitInstruction
` 方法是圍繞以下 `switch
`/`case
` 語句構建的:
switch (Desc->TSFlags & X86::FormMask) {
case X86II::Pseudo: // for not yet implemented instructions
... // or pseudo-instructions
break;
case X86II::RawFrm: // for instructions with a fixed opcode value
...
break;
case X86II::AddRegFrm: // for instructions that have one register operand
... // added to their opcode
break;
case X86II::MRMDestReg:// for instructions that use the Mod/RM byte
... // to specify a destination (register)
break;
case X86II::MRMDestMem:// for instructions that use the Mod/RM byte
... // to specify a destination (memory)
break;
case X86II::MRMSrcReg: // for instructions that use the Mod/RM byte
... // to specify a source (register)
break;
case X86II::MRMSrcMem: // for instructions that use the Mod/RM byte
... // to specify a source (memory)
break;
case X86II::MRM0r: case X86II::MRM1r: // for instructions that operate on
case X86II::MRM2r: case X86II::MRM3r: // a REGISTER r/m operand and
case X86II::MRM4r: case X86II::MRM5r: // use the Mod/RM byte and a field
case X86II::MRM6r: case X86II::MRM7r: // to hold extended opcode data
...
break;
case X86II::MRM0m: case X86II::MRM1m: // for instructions that operate on
case X86II::MRM2m: case X86II::MRM3m: // a MEMORY r/m operand and
case X86II::MRM4m: case X86II::MRM5m: // use the Mod/RM byte and a field
case X86II::MRM6m: case X86II::MRM7m: // to hold extended opcode data
...
break;
case X86II::MRMInitReg: // for instructions whose source and
... // destination are the same register
break;
}
這些 case 語句的實作通常首先發射操作碼 (opcode),然後獲取運算元 (operand)。然後,根據運算元,可能會調用輔助方法來處理運算元。例如,在 `X86CodeEmitter.cpp
` 中,對於 `X86II::AddRegFrm
` case,第一個發射的資料(由 `emitByte
` 發射)是添加到暫存器運算元的操作碼。然後,提取代表機器運算元的物件 `MO1
`。輔助方法,例如 `isImmediate
`、`isGlobalAddress
`、`isExternalSymbol
`、`isConstantPoolIndex
` 和 `isJumpTableIndex
`,決定了運算元類型。(`X86CodeEmitter.cpp
` 也有私有方法,例如 `emitConstant
`、`emitGlobalAddress
`、`emitExternalSymbolAddress
`、`emitConstPoolAddress
` 和 `emitJumpTableAddress
`,它們將資料發射到輸出流中。)
case X86II::AddRegFrm:
MCE.emitByte(BaseOpcode + getX86RegNum(MI.getOperand(CurOp++).getReg()));
if (CurOp != NumOps) {
const MachineOperand &MO1 = MI.getOperand(CurOp++);
unsigned Size = X86InstrInfo::sizeOfImm(Desc);
if (MO1.isImmediate())
emitConstant(MO1.getImm(), Size);
else {
unsigned rt = Is64BitMode ? X86::reloc_pcrel_word
: (IsPIC ? X86::reloc_picrel_word : X86::reloc_absolute_word);
if (Opcode == X86::MOV64ri)
rt = X86::reloc_absolute_dword; // FIXME: add X86II flag?
if (MO1.isGlobalAddress()) {
bool NeedStub = isa<Function>(MO1.getGlobal());
bool isLazy = gvNeedsLazyPtr(MO1.getGlobal());
emitGlobalAddress(MO1.getGlobal(), rt, MO1.getOffset(), 0,
NeedStub, isLazy);
} else if (MO1.isExternalSymbol())
emitExternalSymbolAddress(MO1.getSymbolName(), rt);
else if (MO1.isConstantPoolIndex())
emitConstPoolAddress(MO1.getIndex(), rt);
else if (MO1.isJumpTableIndex())
emitJumpTableAddress(MO1.getIndex(), rt);
}
}
break;
在先前的範例中,`XXXCodeEmitter.cpp
` 使用變數 `rt
`,它是一個 `RelocationType
` 列舉,可用於重定位地址(例如,具有 PIC 基底偏移量的全域地址)。該目標的 `RelocationType
` 列舉在簡短的目標特定 `XXXRelocations.h
` 檔案中定義。`RelocationType
` 由 `XXXJITInfo.cpp
` 中定義的 `relocate
` 方法使用,以重寫引用的全域符號的地址。
例如,`X86Relocations.h
` 為 X86 地址指定了以下重定位類型。在所有四種情況下,重定位的值都會被添加到記憶體中已有的值。對於 `reloc_pcrel_word
` 和 `reloc_picrel_word
`,還有一個額外的初始調整。
enum RelocationType {
reloc_pcrel_word = 0, // add reloc value after adjusting for the PC loc
reloc_picrel_word = 1, // add reloc value after adjusting for the PIC base
reloc_absolute_word = 2, // absolute relocation; no additional adjustment
reloc_absolute_dword = 3 // absolute relocation; no additional adjustment
};
目標 JIT 資訊¶
`XXXJITInfo.cpp
` 實作 JIT 介面,用於目標特定的程式碼產生活動,例如發射機器碼和樁。至少,目標特定版本的 `XXXJITInfo
` 實作以下內容:
`
getLazyResolverFunction
` — 初始化 JIT,為目標提供一個用於編譯的函數。`
emitFunctionStub
` — 返回一個原生函數,該函數具有用於回呼函數的指定地址。`
relocate
` — 根據重定位類型,更改引用的全域變數的地址。回呼函數是函數樁的包裝器,當真實目標最初未知時使用。
`getLazyResolverFunction
` 通常很容易實作。它使傳入的參數成為全域 `JITCompilerFunction
`,並返回將用作函數包裝器的回呼函數。對於 Alpha 目標(在 `AlphaJITInfo.cpp
` 中),`getLazyResolverFunction
` 的實作很簡單:
TargetJITInfo::LazyResolverFn AlphaJITInfo::getLazyResolverFunction(
JITCompilerFn F) {
JITCompilerFunction = F;
return AlphaCompilationCallback;
}
對於 X86 目標,`getLazyResolverFunction
` 的實作稍微複雜一些,因為它為具有 SSE 指令和 XMM 暫存器的處理器返回不同的回呼函數。
回呼函數最初儲存,然後還原被調用者儲存器值、傳入的參數以及框架和返回地址。回呼函數需要對暫存器或堆疊進行底層存取,因此它通常使用組合語言 (assembler) 實作。