撰寫 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 範例,請準備一份 SPARC 架構手冊,版本 8 作為參考。有關 ARM 指令集的詳細資訊,請參閱 ARM 架構參考手冊。如需更多關於 GNU 組合語言格式 (GAS) 的資訊,請參閱 使用 As,特別是關於組合語言印表機的部分。「使用 As」包含一個與目標機器相關的功能列表。

基本步驟

若要為 LLVM 編寫一個編譯器後端,將 LLVM IR 轉換為指定目標(機器或其他語言)的程式碼,請遵循以下步驟

  • 建立 TargetMachine 類別的子類別,描述目標機器的特性。複製特定 TargetMachine 類別和標頭檔的現有範例;例如,從 SparcTargetMachine.cppSparcTargetMachine.h 開始,但請將檔名更改為您的目標。同樣地,將參照「Sparc」的程式碼更改為參照您的目標。

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

  • 描述目標的指令集。使用 TableGen 從目標特定的 TargetInstrFormats.tdTargetInstrInfo.td 版本產生目標特定指令的程式碼。您應該為 TargetInstrInfo 類別的子類別編寫額外的程式碼,以表示目標機器支援的機器指令。

  • 描述 LLVM IR 從指令的有向無環圖 (DAG) 表示形式到原生目標特定指令的選擇和轉換。使用 TableGen 產生程式碼,根據目標特定版本 TargetInstrInfo.td 中的額外資訊匹配模式並選擇指令。編寫 XXXISelDAGToDAG.cpp 的程式碼,其中 XXX 標識特定的目標,以執行模式匹配和 DAG 到 DAG 的指令選擇。還要在 XXXISelLowering.cpp 中編寫程式碼,以替換或移除 SelectionDAG 中原生不支援的操作和資料類型。

  • 編寫一個組譯列印器的程式碼,將 LLVM IR 轉換為目標機器的 GAS 格式。您應該將組譯字串添加到目標特定版本的 TargetInstrInfo.td 中定義的指令中。您還應該為執行 LLVM 到組譯轉換的 AsmPrinter 子類別和 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 的目標獨立程式碼產生器,您應該執行所有當前機器後端所做的事情:建立 LLVMTargetMachine 的子類別。(要從頭開始建立目標,請建立 TargetMachine 的子類別。)

若要讓 LLVM 實際建置並連結您的目標,您需要執行 cmake 並使用 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy 參數。這將會建置您的目標,而無需將其添加到所有目標列表中。

一旦您的目標穩定,您可以將其添加到主 CMakeLists.txt 中的 LLVM_ALL_TARGETS 變數。

目標機器

LLVMTargetMachine 被設計為使用 LLVM 目標無關程式碼產生器實作的目標的基類別。具體的目標類別應該特化 LLVMTargetMachine 類別,並實作各種虛擬方法。LLVMTargetMachineinclude/llvm/Target/TargetMachine.h 中被定義為 TargetMachine 的子類別。TargetMachine 類別的實作 (TargetMachine.cpp) 也會處理許多命令列選項。

若要建立 LLVMTargetMachine 的具體目標特定子類別,請先複製現有的 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 LLVMTargetMachine {
  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,並為其分配了由 gccgdb 或除錯資訊寫入器用於識別暫存器的值(使用 DwarfRegNum)。對於暫存器 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)和向量類型(例如,v8i16 表示 8 x i16 向量)。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
    )>;

使用 TableGen 處理 SparcRegisterInfo.td 會產生多個輸出文件,這些文件旨在包含在您撰寫的其他原始程式碼中。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) {}
}

暫存器分配器會避免使用保留的暫存器,並且被呼叫端儲存的暫存器在所有可變動的暫存器都被使用之前都不會被使用。這通常已經足夠好了,但在某些情況下,可能需要提供自訂的分配順序。

實作 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* 類別(選擇 DAG 類型約束)、SelectionDAG 節點的定義(例如 immcondbbaddfaddsub)和模式支援(PatternPatPatFragPatLeafComplexPattern)。

  • XXXInstrFormats.td — 目標特定指令定義的模式。

  • XXXInstrInfo.td — 指令範本、條件碼和指令集指令的目標特定定義。對於架構修改,可以使用不同的檔案名稱。例如,對於具有 SSE 指令的 Pentium,此檔案為 X86InstrSSE.td,而對於具有 MMX 的 Pentium,此檔案為 X86InstrMMX.td

還有一個目標特定的 XXX.td 檔案,其中 XXX 是目標的名稱。XXX.td 檔案包含其他 .td 輸入檔案,但其內容僅對子目標直接重要。

您應該描述一個具體的目標特定類別 XXXInstrInfo,它表示目標機器支援的機器指令。XXXInstrInfo 包含一個 XXXInstrDescriptor 物件陣列,每個物件都描述一個指令。指令描述符定義:

  • 操作碼助記符

  • 運算元數量

  • 隱式暫存器定義和使用的清單

  • 目標無關的屬性(例如記憶體存取、是否可交換)

  • 目標特定的旗標

Instruction 類別(在 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 定義了從記憶體位址載入整數指令到暫存器中的一個字(LD SPARC 操作碼)。第一個參數,值 3(112),是此操作類別的操作值。第二個參數(0000002)是 LD/載入字的特定操作值。第三個參數是輸出目的地,它是一個暫存器操作數,在 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 選擇階段期間用於匹配指令的模式。下一節 指令選擇器 中將詳細介紹此參數。

指令類別定義不會針對不同的操作數類型過載,因此需要針對暫存器、記憶體或立即值操作數使用不同版本的指令。例如,要執行從立即操作數到暫存器的字載入整數指令,請定義以下指令類別

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))]>;
}

因此,當 defm 指令用於 XORADD 指令時,如下所示,它會創建四個指令對象: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

#define GET_INSTRINFO_NAMED_OPS // For getNamedOperandIdx() function
#include "XXXGenInstrInfo.inc"

XXXInstrInfo.h

#define GET_INSTRINFO_OPERAND_ENUM // For OpName enum
#include "XXXGenInstrInfo.inc"

namespace XXX {
  int16_t getNamedOperandIdx(uint16_t Opcode, uint16_t NamedIndex);
} // End namespace XXX

指令操作數類型

TableGen 也會產生一個列舉,其中包含後端定義的所有具名運算元類型,位於 llvm::XXX::OpTypes 命名空間中。一些常見的立即運算元類型(例如 i8、i32、i64、f32、f64)是為 include/llvm/Target/Target.td 中的所有目標定義的,並且在每個目標的 OpTypes 列舉中可用。此外,只有具名運算元類型會出現在列舉中:匿名類型會被忽略。例如,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 使用 CodeGenSchedModels 類別由 SubtargetEmitter 產生的。這與指定機器資源使用的行程方法不同。工具 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 功能用於關聯指令。當您有多個指令格式並且需要在指令選擇後在它們之間切換時,它特別有用。此整個功能由關聯模型驅動,這些模型可以在 XXXInstrInfo.td 檔案中根據目標特定的指令集定義。關聯模型使用 InstrMapping 類別作為基底來定義。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 的節點理想情況下代表原生目標指令。在程式碼生成過程中,會執行指令選擇遍歷,將非原生的 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 程式碼降低到 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 類型列舉值: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 謂詞為真(也就是說,如果目前的參數類型為 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]> - 定義支援的每個呼叫慣例。

組譯器

在程式碼發射階段,程式碼產生器可以使用 LLVM pass 來產生組譯輸出。為此,您需要為印表機實現程式碼,該印表機會將 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

AsmPrinter 的目標設備特定實現寫入 XXXAsmPrinter.cpp 中,它實現了將 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.cppprintDeclareprintImplicitDefprintInlineAsmprintLabel 的實現通常足以列印組合語言,不需要覆寫。

printOperand 方法是使用一個長的 switch/case 語句來實現的,用於處理不同類型的運算元:暫存器、立即數、基本區塊、外部符號、全域位址、常數池索引或跳轉表索引。 對於具有記憶體位址運算元的指令,應該實現 printMemOperand 方法來產生適當的輸出。 同樣地,應該使用 printCCOperand 來列印條件運算元。

應該在 XXXAsmPrinter 中覆寫 doFinalization,並呼叫它來關閉組合語言印表機。 在 doFinalization 期間,全域變數和常數會輸出到輸出。

子目標支援

子目標支援用於將特定晶片組的指令集變更告知程式碼生成過程。 例如,提供的 LLVM SPARC 實現涵蓋了 SPARC 微處理器架構的三個主要版本:版本 8(V8,32 位元架構)、版本 9(V9,64 位元架構)和 UltraSPARC 架構。 V8 擁有 16 個雙精度浮點暫存器,這些暫存器也可以用作 32 個單精度或 8 個四精度暫存器。 V8 也完全是大端序。 V9 擁有 32 個雙精度浮點暫存器,這些暫存器也可以用作 16 個四精度暫存器,但不能用作單精度暫存器。 UltraSPARC 架構結合了 V9 和 UltraSPARC 視覺指令集擴充功能。

如果需要子目標支援,則應該為您的架構實現特定於目標的 XXXSubtarget 類別。 該類別應該處理命令列選項 -mcpu=-mattr=

TableGen 使用 Target.tdSparc.td 文件中的定義來產生 SparcGenSubtarget.inc 中的程式碼。在 Target.td 中(如下所示),定義了 SubtargetFeature 介面。SubtargetFeature 介面的前 4 個字串參數分別是功能名稱、由功能設置的 XXXSubtarget 欄位、XXXSubtarget 欄位的值以及功能的描述。(第五個參數是其存在隱含的功能列表,其預設值為空陣列。)

如果欄位的值是字串「true」或「false」,則假設該欄位是一個布林值,並且只有一個 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.tdSparc.td 文件,產生的 SparcGenSubtarget.inc 指定了用於識別功能的列舉值、用於表示 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 文件,其中包含一個機器函數傳遞,用於將目標機器指令轉換為可重定位的機器碼。

  • 編寫一個 XXXJITInfo.cpp 文件,為目標特定的程式碼產生活動(例如發出機器碼和存根)實現 JIT 介面。

  • 修改 XXXTargetMachine,使其通過其 getJITInfo 方法提供一個 TargetJITInfo 對象。

編寫 JIT 支援程式碼的方法有很多種。例如,TableGen 和目標描述符文件可用於創建 JIT 程式碼產生器,但不是必需的。對於 Alpha 和 PowerPC 目標機器,TableGen 用於產生 XXXGenCodeEmitter.inc,其中包含機器指令的二進制編碼以及用於訪問這些程式碼的 getBinaryCodeForInstr 方法。其他 JIT 實現則沒有。

XXXJITInfo.cppXXXCodeEmitter.cpp 都必須包含 llvm/CodeGen/MachineCodeEmitter.h 標頭檔,該檔案定義了 MachineCodeEmitter 類別,其中包含用於將資料(以位元組、字組、字串等形式)寫入輸出串流的多個回調函式程式碼。

機器碼發射器

XXXCodeEmitter.cpp 中,Emitter 類別的目標特定實現為函式傳遞(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 語句的實現通常會先發射操作碼,然後取得運算元。然後,根據運算元的不同,可能會呼叫輔助方法來處理運算元。例如,在 X86CodeEmitter.cpp 中,對於 X86II::AddRegFrm 情況,發射的第一個資料(通過 emitByte)是添加到寄存器運算元的操作碼。然後提取表示機器運算元的物件 MO1。輔助方法(如 isImmediateisGlobalAddressisExternalSymbolisConstantPoolIndexisJumpTableIndex)確定運算元類型。(X86CodeEmitter.cpp 還具有私有方法,如 emitConstantemitGlobalAddressemitExternalSymbolAddressemitConstPoolAddressemitJumpTableAddress,用於將資料發射到輸出串流中。)

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 檔案中定義。RelocationTypeXXXJITInfo.cpp 中定義的 relocate 方法使用,用於重寫引用的全局符號的地址。

例如,X86Relocations.h 為 X86 位址指定了下列重定位類型。在所有四種情況下,重定位的值都會被加到記憶體中已有的值。對於 reloc_pcrel_wordreloc_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 实现了針對目標平台程式碼生成活動(如發出機器碼和 stub)的 JIT 介面。至少,目標平台特定的 XXXJITInfo 版本會實作以下內容

  • getLazyResolverFunction — 初始化 JIT,為目標平台提供一個用於編譯的函式。

  • emitFunctionStub — 為回調函式返回一個具有指定位址的原生函式。

  • relocate — 根據重定位類型更改被參考的全域變數的位址。

  • 回調函式是函式 stub 的包裝器,當實際目標最初未知時使用。

getLazyResolverFunction 通常很容易實現。它將傳入的參數作為全域變數 JITCompilerFunction,並返回將用作函式包裝器的回調函式。對於 Alpha 目標平台(在 AlphaJITInfo.cpp 中),getLazyResolverFunction 的實現很簡單

TargetJITInfo::LazyResolverFn AlphaJITInfo::getLazyResolverFunction(
                                            JITCompilerFn F) {
  JITCompilerFunction = F;
  return AlphaCompilationCallback;
}

對於 X86 目標平台,getLazyResolverFunction 的實現稍微複雜一些,因為它為具有 SSE 指令和 XMM 暫存器的處理器返回不同的回調函式。

回調函式會先儲存,然後再恢復被呼叫者暫存器值、傳入參數、以及框架和返回位址。回調函式需要對暫存器或堆疊進行低階存取,因此它通常使用組合語言來實現。