編寫 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.cppSparcTargetMachine.h 開始,但變更目標的檔名。 同樣地,將引用 “Sparc” 的程式碼變更為引用您的目標。

  • 描述目標的暫存器集合。 使用 TableGen 從目標特定的 RegisterInfo.td 輸入檔產生用於暫存器定義、暫存器別名和暫存器類別的程式碼。 您也應該為 TargetRegisterInfo 類別的子類別編寫額外程式碼,該子類別表示用於暫存器分配的類別暫存器檔案資料,並描述暫存器之間的互動。

  • 描述目標的指令集。 使用 TableGen 從 TargetInstrFormats.tdTargetInstrInfo.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 目標)。 或者,您可以將庫拆分為 LLVMDummyCodeGenLLVMDummyAsmPrinter,後者應在 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 類別應由實作各種虛擬方法的具體目標類別專門化。 CodeGenTargetMachineImplinclude/llvm/CodeGen/CodeGenTargetMachineImpl.h 中定義為 TargetMachine 的子類別。 TargetMachine 類別實作 (include/llvm/Target/TargetMachine.cpp) 也處理眾多命令列選項。

若要建立 CodeGenTargetMachineImpl 的具體目標特定子類別,請先複製現有的 TargetMachine 類別和標頭。 您應該命名您建立的檔案,以反映您的特定目標。 例如,對於 SPARC 目標,將檔案命名為 SparcTargetMachine.hSparcTargetMachine.cpp

對於目標機器 XXXXXXTargetMachine 的實作必須具有存取方法,以取得表示目標組件的物件。 這些方法命名為 get*Info,旨在取得指令集 (getInstrInfo)、暫存器集合 (getRegisterInfo)、堆疊框架佈局 (getFrameInfo) 和類似資訊。XXXTargetMachine 也必須實作 getDataLayout 方法,以存取具有目標特定資料特性的物件,例如資料類型大小和對齊要求。

例如,對於 SPARC 目標,標頭檔 SparcTargetMachine.h 宣告了幾個 get*InfogetDataLayout 方法的原型,這些方法僅傳回類別成員。

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.incXXXGenRegisterInfo.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),這些值由 gccgdb 或偵錯資訊寫入器用於識別暫存器。 對於暫存器 ALDwarfRegNum 採用 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 物件。TargetRegisterDescinclude/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) 來決定暫存器的文字名稱(在 TargetRegisterDescAsmNameName 欄位中)以及其他暫存器與已定義暫存器的關係(在其他 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,以及更進一步的子類別:RiRfRd。 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]>;

上面顯示的最後兩個暫存器(D0D1)是雙精度浮點暫存器,它們是單精度浮點子暫存器對的別名。 除了別名之外,已定義暫存器的子暫存器和超暫存器關係也位於暫存器的 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 中。定義的值包含整數類型(例如 i16i32 和用於布林值的 i1)、浮點數類型(f32f64)和向量類型(例如,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 物件:FPRegsDFPRegsIntRegs。對於所有三個暫存器類別,第一個參數使用字串 “SP” 定義了命名空間。FPRegs 定義了一組 32 個單精度浮點暫存器(F0F31);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 作為其基底。它也基於定義的暫存器類別指定了類型:DFPRegsClassFPRegsClassIntRegsClass

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 類別)。除非被覆寫,否則這些函式會傳回 0NULLfalse。以下是在 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 — 定義 InstructionOperandInstrInfo 和其他基本類別的地方。

  • TargetSelectionDAG.td — 由 SelectionDAG 指令選擇產生器使用,包含 SDTC* 類別(SelectionDAG 類型約束)、SelectionDAG 節點的定義(例如 immcondbbaddfaddsub)和模式支援(PatternPatPatFragPatLeafComplexPattern)。

  • 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 目標可以使用兩個名為 ADDriADDrr 的指令來對此進行建模。

您應為每個指令類別定義一個類別,並將每個運算碼定義為該類別的子類別,並帶有適當的參數,例如運算碼和擴展運算碼的固定二進制編碼。您應將暫存器位元映射到指令中編碼它們的位元(對於 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 (112) 是此運算類別的運算值。第二個參數 (0000002) 是 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))]>;
}

因此,當對 XORADD 指令使用 defm 指令時,如下所示,它會建立四個指令物件:XORrrXORriADDrrADDri

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 = 9SPCC::FCC_U = 23 等等。)

指令運算元映射

程式碼產生器後端將指令運算元映射到指令中的欄位。每當指令編碼 Inst 中的位元被分配給沒有具體值的欄位時,預期 outsins 列表中的運算元具有匹配的名稱。然後,此運算元會填充該未定義的欄位。例如,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 定義了 rdop3rs1 欄位,並在指令中使用它們,並且再次沒有分配值。

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_1opop3 欄位分配了一個值,並定義了 rs2 欄位。因此,F3_1 格式的指令將需要 rdrs1rs2 的定義,以便完全指定指令編碼。

然後,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 格式,因此它也將期望名為 rdrs1rs2 的運算元。為了允許這樣做,複雜的運算元可以選擇性地為其每個子運算元命名。在此範例中,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 後端定義了 brtargetbrtarget8,它們都是 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 轉換。BranchFolderIfConverter 機器函式傳遞(請參閱 lib/CodeGen 目錄中的原始碼檔案 BranchFolding.cppIfConversion.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 — 用於評估條件分支的條件的運算元列表。

在最簡單的情況下,如果一個區塊在沒有分支的情況下結束,那麼它會直接執行到後續區塊。對於 TBBFBB 皆未指定目標區塊,因此這兩個參數都會回傳 NULLanalyzeBranch 的開頭(請參閱下方 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 的實作需要輔助方法 removeBranchinsertBranch 來管理後續操作。

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 方法,該方法用於為指令呼叫適當的處理方法。在此範例中,SelectCodeISD::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 類型列舉值:PromoteExpandCustomLegalSparcISelLowering.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);
}

呼叫慣例

為了支援目標特定的呼叫慣例,XXXGenCallingConv.td 使用在 lib/Target/TargetCallingConv.td 中定義的介面(例如 CCIfTypeCCAssignToReg)。TableGen 可以採用目標描述符檔案 XXXGenCallingConv.td 並產生標頭檔 XXXGenCallingConv.inc,後者通常包含在 XXXISelLowering.cpp 中。您可以使用 TargetCallingConv.td 中的介面來指定

  • 參數配置的順序。

  • 參數和回傳值放置的位置(亦即,在堆疊上或在暫存器中)。

  • 可以使用哪些暫存器。

  • 呼叫者或被呼叫者是否解除堆疊。

以下範例示範了 CCIfTypeCCAssignToReg 介面的使用。如果 CCIfType 述詞為 true(亦即,如果目前的引數類型為 f32f64),則執行該動作。在這種情況下,CCAssignToReg 動作將引數值指派給第一個可用的暫存器:R0R1

CCIfType<[f32,f64], CCAssignToReg<[R0, R1]>>

SparcCallingConv.td 包含目標特定回傳值呼叫慣例 (RetCC_Sparc32) 和基本 32 位元 C 呼叫慣例 (CC_Sparc32) 的定義。RetCC_Sparc32 的定義(如下所示)指示了哪些暫存器用於指定的純量回傳類型。單精度浮點數回傳到暫存器 F0,雙精度浮點數回傳到暫存器 D0。32 位元整數回傳到暫存器 I0I1

def RetCC_Sparc32 : CallingConv<[
  CCIfType<[i32], CCAssignToReg<[I0, I1]>>,
  CCIfType<[f32], CCAssignToReg<[F0]>>,
  CCIfType<[f64], CCAssignToReg<[D0]>>
]>;

SparcCallingConv.tdCC_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 結尾。在將目前的值指派給暫存器 ST0ST1 之後,會調用 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>
]>;

CCAssignToRegAndStackCCAssignToReg 相同,但也會在使用某些暫存器時配置堆疊槽。基本上,它的運作方式如下: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 類別。該實作必須包含以下標頭,這些標頭具有 AsmPrinterMachineFunctionPass 類別的宣告。MachineFunctionPassFunctionPass 的子類別。

#include "llvm/CodeGen/AsmPrinter.h"
#include "llvm/CodeGen/MachineFunctionPass.h"

作為 FunctionPassAsmPrinter 首先呼叫 doInitialization 來設定 AsmPrinter。在 SparcAsmPrinter 中,實例化了一個 Mangler 物件來處理變數名稱。

XXXAsmPrinter.cpp 中,必須為 XXXAsmPrinter 實作 runOnMachineFunction 方法(在 MachineFunctionPass 中宣告)。在 MachineFunctionPass 中,runOnFunction 方法調用 runOnMachineFunctionrunOnMachineFunction 的目標特定實作有所不同,但通常會執行以下操作來處理每個機器函數

  • 呼叫 SetupMachineFunction 以執行初始化。

  • 呼叫 EmitConstantPool 以列印(到輸出流)已溢出到記憶體的常數。

  • 呼叫 EmitJumpTableInfo 以列印目前函數使用的跳躍表。

  • 列印目前函數的標籤。

  • 列印函數的程式碼,包括基本區塊標籤和指令的組合語言(使用 printInstruction

XXXAsmPrinter 實作也必須包含由 TableGen 產生的程式碼,該程式碼輸出在 XXXGenAsmWriter.inc 檔案中。XXXGenAsmWriter.inc 中的程式碼包含 printInstruction 方法的實作,該方法可能會呼叫這些方法

  • printOperand

  • printMemOperand

  • printCCOperand(用於條件陳述式)

  • printDataDirective

  • printDeclare

  • printImplicitDef

  • printInlineAsm

AsmPrinter.cpp 中的 printDeclareprintImplicitDefprintInlineAsmprintLabel 的實作通常足以列印組合語言,而不需要被覆寫。

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) 實作。