編寫一個LLVM後端

本文翻譯自 LLVM 官方的一篇教程:https://releases.llvm.org/10.0.0/docs/WritingAnLLVMBackend.html#instruction-scheduling
該文檔需要有一定的 LLVM 和 編譯原理的基礎。
LLVM目前的更新很活躍,請注意跟蹤項目最新變更

1 介紹

這篇文章描述瞭如何編寫一個用於將LLVM中間表示(IR)轉換成特定目標機器上的代碼或其他編程語言的編譯器後端的技術。作用於特定目標機器的代碼可以使彙編碼形式,也可以是用於JIT編譯器的二進制碼形式。

LLVM的後端特點是目標無關代碼生成器,它可以輸出多種不同類型目標CPU的代碼,包括X86、PowerPC、ARM以及SPARC等。後端也會被用來生成如SPU一類的元胞處理器(Cell processor)或者是一些GPU上的計算內核。

這篇文章專注在的路徑是在release版本LLVM源碼路徑下的llvm/lib/Target,尤其是專注於舉例如何編寫一個爲SPARC目標平臺的靜態編譯器(也就是發射彙編碼),因爲SPARC架構有非常標準的特性,比如RISC架構指令集以及常規的調用約定。

1.1 目標讀者

本文目標讀者是任何需要編寫LLVM後端來爲特定軟硬件目標生成代碼的人。

1.2 預先閱讀

以下資料必須提前閱讀了解:

  • LLVM Language Reference Manual:這是一個介紹LLVM彙編語言的參考手冊
  • The LLVM Target-Independent Code Generator:一個描述用於翻譯LLVM中間表示到特定目標機器代碼需要使用的類結構和算法的指南。需要特別注意代碼生成階段(pass)的內容:指令選擇,調度和隊列化(Formation),SSA級優化,寄存器分配,Prolog和Epilog代碼插入,後機器代碼優化,以及代碼發射。
  • TableGen:這個文檔描述了TableGen(tblgen)引用如何管理領域特定信息來支持LLVM代碼生成。TableGen從一個特殊的目標描述文件(.td後綴)中讀入輸入信息,然後生成c++代碼,用於代碼生成。
  • Writing an LLVM Pass:彙編輸出是一個FunctionPass,另外還有幾個SelectionDAG的處理步驟。

另外,爲了支持SPARC案例相關的信息,你需要有一份 The SPARC Architecture Manual, Version 8 來作爲參考。更多關於ARM架構指令集的信息,需要參考 ARM Architecture Reference Manual 。有關於GNU彙編器格式的說明,參考 Using As ,特別是彙編代碼輸出的部分, Using As 中包含了一個目標機器相關特性的清單。

1.3 基本步驟

編寫一個編譯器後端來將LLVM的IR轉換爲特定目標的代碼(如硬件機器或其他語言),需要以下步驟:

  • 創建一個TargetMachine的子類,用來描述你的目標機器的特性。拷貝已經存在的其他特定後端中的TargetMachine和頭文件;比如拷貝SparcTargetMachine.cpp和SparcTargetMachine.h,但是要修改文件名爲你自己的目標。類似的,也要把文件內容中的space都改成你的目標名稱。
  • 需要描述目標機器的寄存器集。依賴於目標相關的RegisterInfo.td文件作爲輸入,使用TableGen來生成有關寄存器定義、寄存器別名和寄存器類的代碼。你也可能編寫一些額外的代碼,通過實現繼承TargetRegisterInfo類的子類來表示有助於寄存器分配和寄存器間交互的信息。
  • 需要描述目標機器的指令集。依賴於目標相關的TargetInstrFormats.td文件和TargetInstrInfo.td文件作爲輸入,使用TableGen來生成有關目標的指令集信息。你也可能編寫一些額外的代碼,通過實現TargetInstrInfo類的子類來表示目標機器的一些機器指令。
  • 需要描述將LLVM IR從一個DAG描述的指令轉換成原生特定機器指令的選擇和轉換。依賴於目標相關的TargetInstrInfo.td文件作爲輸入,使用TableGen生成有關描述模式匹配和指令選擇的信息。編寫XXXISelDAGToDAG.cpp文件中代碼(XXX表示目標平臺名稱)來描述模式匹配和DAG到DAG的指令選擇。另外也要完成XXXISelLowering.cpp文件中代碼,來替代或移除一些SelectionDAG中不支持的操作和數據類型。
  • 需要爲彙編輸出模塊編寫代碼,從而可以將LLVM IR轉換爲與你目標機器平臺匹配的GAS格式的輸出。你應該會在目標相關的TargetInstrInfo.td中增加對指令彙編格式的約定。同事還需要完成繼承AsmPrinter類的之類,它被用來實現LLVM IR到彙編格式的轉換,另外還有個輔助的繼承類TargetAsmInfo的之類。
  • 可選部分,可以支持子目標平臺(subtarget)你可以編寫一個繼承自TargetSubtarget類的之類,通過命令行參數-mcpu=和-mattr=可以指定針對特定子目標平臺和部分特性的編譯選項。
  • 可選部分,增加一個JIT支持,創建一個機器碼輸出,你需要編寫一個繼承自TargetJITInfo類的子類,用來發射二進制機器碼到內存中。

在cpp和h文件中,首先需要爲這些方法佔位,然後再逐步實現它們。最初,你可能不知道這些類需要哪些私有成員,以及哪些子類需要被創建。

1.4 預備步驟

爲了創建你的編譯器後端,你需要創建和修改一些文件,這裏簡單討論了一下。但是真正的操作,你必須要參考 LLVM Target-Independent Code Generator 文檔中的描述來逐步進行。

首先,你應該在 lib/Target 目錄下創建一個你自己目標名稱的子目錄,用來存放所有的和你目標相關的文件。如果你的目標叫做Dummy,需要創建的目錄就是 lib/Target/Dummy。

在這個目錄下,需要創建一個CMakeLists.txt文件,你可以簡單的從其他的後端路徑下複製該文件,然後直接修改,至少需要將 LLVM_TARGET_DEFINITIONS 變量修改了。對應的library可以叫做LLVMDummy(你可以參考MIPS後端)。另一種方式是,你可以區分LLVMDummyCodeGen和LLVMDummyAsmPrinter這兩個爲不同的庫,後者需要實現在 lib/Target/Dummy下一級的子目錄中(你可以參考PowerPC後端)。

需要注意,這兩種命名方式是硬編碼在llvm-config中的。使用其他的命名方式會讓llvm-config無法正常工作,並在llc中產生很多的鏈接錯誤。

爲了讓你的目標真的做什麼事情,至少你需要實現TargetMachine的子類,這個實現是在 lib/Target/DummyTargetMachine.cpp中完成的,但任何在該目錄下的其他文件都應該能正常編譯和工作。爲了實現LLVM的目標無關的代碼生成工作,你需要實現所有當前機器平臺後端需要做的事情,實現一個繼承自LLVMTargetMachine的子類(如果是從零開始創建目標平臺,實現TargetMachine的子類)。

爲了能讓LLVM可以編譯和鏈接你的目標,你需要指定參數-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy來運行cmake,這將能夠在不必將目標添加在其他目標的列表前,就構建你的目標。

一旦你的目標後端穩定了,你可以將其增加在LLVM_ALL_TARGETS變量中,這個變量位於最外層的CMakeLists.txt中。

2 目標機器

LLVMTargetMachine被設計作爲一個基類來完成LLVM目標無關代碼生成任務。這個類需要被繼承並實現其定義的虛函數。LLVMTargetMachine是TargetMachine的一個子類,其在 include/llvm/Target/TargetMachine.h 中被實現,同時TargetMachine類還處理大量和命令行參數有關的內容(TargetMachine.cpp)。

爲了定義一個繼承自LLVMTargetMachine類的目標平臺特定的子類,首先需要複製一個已經存在的TargetMachine的源文件和頭文件,然後你應該將其命名爲與你特定後端相關的名字,比如對於SPARC平臺來說,將其命名爲 SparcTargetMachine.h和SparcTargetMachine.cpp文件。

對於一個目標機器XXX,實現的XXXTargetMachine必須通過一些方法來訪問到對應後端的各種組件。這些方法被命名爲 get*Info,比如能夠獲得指令集信息的getInstrInfo,獲得寄存器集信息的getRegisterInfo,獲得幀信息的getFrameInfo等。XXXTargetMachine還需要實現getDataLayout方法,用來訪問對應這個目標特殊的數據特性,比如數據類型的空間佔用和對齊要求。

舉個例子,對於SPARC目標來說,頭文件SparcTargetMachine.h 中聲明瞭很多get*Info和getDataLayout方法的原型,這些方法返回的是對應的類成員對象。

namespace llvm {

class Module;

class SparcTargetMachine : public LLVMTargetMachine {
	const DataLayout DataLayout;
	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; }
  static unsigned getModuleMatchQuality(const Module &M);
  
  // 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字符串,用來決定該目標平臺的data layout,包括指針的佔用內存大小、對齊以及大小端信息。比如,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) {
}

連接符-區分了data layout字符串的不同部分:

  • 大寫的E表示這是大端目標數據模型,而小寫的e則表示是小端模型;
  • p:以及後續的幾個值,表示指針的信息,包括佔用空間大小,ABI的對齊要求和優先對齊。如果後邊只有2個數字,則第一個數字是指針的佔用空間大小,第二個數字同時表示兩種對齊情況。
  • 然後後邊的一個字符,可能是fiva等,分別表示浮點數、整數、向量和整體,然後後邊的數據格式意義與指針類型是基本一致的。

3 目標註冊

你還需要將你的目標通過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”

4 寄存器集合寄存器類別

(譯註:本節及後文將原文中Register Set譯爲寄存器集合,將Register Class譯爲寄存器類別。)

你應該接下來描述表示目標機器的寄存器文件的簡單類結構。這個類被稱爲 XXXRegisterInfo,這個類中的寄存器文件數據會被用來做寄存器分配等工作,它同事也描述了寄存器之間的關係。

你也需要定義一些寄存器類別來分類相關的寄存器。一個寄存器類別中的寄存器應該對於一些指令有着相同的行爲。典型的例子是包括所有整形寄存器的類別、浮點型寄存器的類別和向量寄存器的類別。寄存器分配允許一個指令使用某個特定寄存器類別中的寄存器來完成相同的指令功能。寄存器類別會給這些指令分配虛擬寄存器,同時也會在寄存器分配階段分配真實的寄存器。

大多數寄存器相關的代碼,比如寄存器定義、別名以及類別,都是在TableGen的 XXXRegisterInfo.td文件中完成的,這個文件會生成 XXXGenRegisterInfo.h.inc 和 XXXGenRegisterInfo.inc。另外一些代碼需要手動在 XXXRegisterInfo 結構中實現。

4.1 定義一個寄存器

在 XXXRegisterInfo.td文件中,習慣性先定義目標機器的寄存器。Register類(在Target.td中定義)用來爲每個寄存器定義對象,參見下邊代碼。其中的參數 n 是指寄存器的名字。基本 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 並且指定了 Dwarf 中寄存器編號,這個編號會被如 gcc,gdb 或其他調試信息工具來識別寄存器。對於 AL 寄存器 來說,DwarfRegNum 使用了一個由 3 個值組成的數組,用來表示 3 種不同的模式:第一個元素是針對 X86-64,第二個元素是用於 X86-32 中的異常處理(exception handling),第三個元素是通用值。如果指定 -1 則表示 gcc 的值未定義,如果指定 -2 則表示寄存器值是非法的。

對於之前的 td 文件中的描述,TableGen 工具會在 X86RegisterInfo.inc 中生成如下的 c++ 代碼:

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 對象。這個對象在 include/llvm/Target/TargetRegisterInfo.h 中定義,它的結構如下:

struct TargetRegisterDesc {
  const char *AsmName;              // Assembly language name for the register
  const char *Name;                 // Printable name for the reg (for debugging)
  const unsigned *AliasSet;         // Register Alias Set
  const unsigned *SubRegs;          // Sub-register set
  const unsigned *ImmSubRegs;       // Immediate sub-register set
  const unsigned *SuperRegs;        // Super-register set
}

TableGen 使用 td 文件來決定寄存器名稱(AsmName 和 Name 部分)和與其他寄存器的關係。在這個例子中,還定義了寄存器 AX,EAX 和 RAX 並互相作爲別名,所以 TableGen 生成了一個以 null 結尾的數組(AL_AliasSet)來保存寄存器別名集合。

Register 類也會作爲更加複雜的寄存器類的基類,在 Target.td 文件中,Register 類作爲 RegisterWithSubRegs 類的基類,後者被用來定義需要指定特殊子寄存器的寄存器,定義如下:

class RegisterWithSubRegs<string n, list<Register> subregs> : Register<n> {
  let SubRegs = subregs;
}

在 SparcRegisterInfo.td 文件中,還有SPARC 特殊使用的寄存器類,如 SparcReg,它以 Register 作爲基類,並衍生出更多子類,如 Ri,Rf 和 Rd。SPARC 寄存器由 5 個 ID 數字來識別,這個在不同的子類中是相同的,他們使用 let 表達式來覆蓋在父類中初始化時定義的值。

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 文件中,使用這些子類來完成寄存器定義,比如:

def G0 : Ri< 0, "G0">, DwarfRegNum<[0]>;
def G1 : Ri< 1, "G1">, DwarfRegNum<[1]>;
...
def F0 : Rf< 0, "F0">, DwarfRegNum<[32]>;
def F1 : Rf< 1, "F1">, DwarfRegNum<[33]>;
...
def D0 : Rd< 0, "F0", [F0, F1]>, DwarfRegNum<[32]>;
def D1 : Rd< 2, "F2", [F2, F3]>, DwarfRegNum<[34]>;
...

最後兩個寄存器(D0 和 D1)是雙精度的浮點寄存器,他們由兩個單精度浮點子寄存器組成。除別名之外,子寄存器和父寄存器的關係也存在於寄存器的 TargetRegisterDesc 字段中。

4.2 定義一個寄存器類別

(譯註,原文 Register Class 想表達的是寄存器的集合,這裏譯作寄存器類別,去 C++中的類做區別)

寄存器類別的類 RegisterClass(在 Target.td 中定義)被用來定義一個表示一組相關寄存器的集合的對象,同時用來定義寄存器的默認分配順序。目標描述文件 XXXRegisterInfo.td 使用 Target.td 來構造寄存器類別,該類的定義如下:

class RegisterClass<string namespace, list<ValueType> regTypes, int alignment, dag regList> {
  string Namespace = namespace;
  list<ValueType> RegTypes = regTypes;
  int size = 0;  // 位爲單位的溢出長度,設爲 0,由 tblgen 工具設定
  int Alignment = alignment;
  
  // CopyCost 是在兩個寄存器間複製值的成本
  // 默認是 1,表示用 1 條指令完成
  // 設定爲負數表示複製值非常困難或無法實現
  int CopyCost = 1;
  dag MemberList = regList;
  
  // 這個類別的子類別
  list<RegisterClass> SubRegClassList = [];
  
  code MethodProtos = [{}];  // 任意代碼
  code MethodBodies = [{}];
}

定義一個 RegisterClass 的對象,需要給定 4 個參數:

  • 第一個參數是命名空間;
  • 第二個參數是一個寄存器類型值的列表,這些類型在 include/llvm/CodeGen/ValueTypes.td 中定義。已定義的類型包括整數類型(i16, i32, 用於布爾型的i1等),浮點類型(f32, f64),向量類型(比如 v8i16 表示 8 * i16 的向量)。同一個寄存器類型中的所有的寄存器必須有相同的 ValueType,但是一些寄存器可能在不同的配置下存儲不同類型的向量數據。比如,一個能夠存放 128 位數據的向量寄存器,既可以保存 16 個 8 位的整形元素,也可以保存 8 個 16 位的整形或 4 個 32 位的整形元素(譯註:所以這個參數用列表來指定不同的可能的類型)。
  • 第三個參數是這個 RegisterClass 對象特定的對齊長度,當它們做 store 和 load 操作時,這個參數會被用到。
  • 第四個參數,指定了這個類別中有哪些寄存器。如果沒有指定可選的分配順序,則這個參數中的順序還同時表示寄存器分配時的順序。簡單的例子如(add R0, R1, ...),更加複雜的一些例子可查看 include/llvm/Target/Target.td。

在 SparcRegisterInfo.td 文件中,定義了三個寄存器類別的類對象,分別是 FPRegs,DFPRegs,IntRegs。這三個寄存器類別對象的第一個參數(命名空間)指定爲“SP”。FPRegs 定義了一組保存 32 位單精度浮點數的寄存器集合(F0 到 F31);DFPRegs 定義了一組保存 16 位雙精度浮點數寄存器集合(D0-D15)。實現代碼如下:

// F0, F1, F2, ..., F31
def FPRegs : RegisterClass<"SP", [f32], 32, (sequence "F%u", 0, 31)>;

def DFPRegs : RegisterClass<"SP", [f64], 64, 
                            (add D0, D1, D2, D3, D4, D5, D6, D7, D8, 
                                 D9, D10, D11, D12, D13, D14, D15)>;

def IntRegs : RegisterClass<"SP", [i32], 32, 
                            (add L0, L1, L2, L3, L4, L5, L6, L7, 
                                 I0, I1, I2, I3, I4, I5, 
                                 O0, O1, O2, O3, O4, O5, O7, 
                                 G1,
                                 // 不分配的寄存器:
                                 G2, G3, G4,
                                 O6, // 棧指針
                                 I6, // 幀指針
                                 I7, // 返回地址
                                 G0, // 常數 0
                                 G5, G6, G7 // 內核保留
                             )>;

將 SparcRegisterInfo.td 作爲 TableGen 的輸入,會生成多個輸出文件,這些文件可以在你的代碼中被調用。SparcRegisterInfo.td 首先生成 SparcGenRegisterInfo.h.inc,這個文件可以包含(included)到你的 SPARC 寄存器實現的頭文件(SparcRegisterInfo.h)中。在SparcGenRegisterInfo.h.inc 文件中,定義了一個新的結構,SparcGenRegisterInfo,它使用 TargetRegisterInfo 作爲基類,同樣的,會根據 td 文件中的指定區分類型:DFPRegsClass,FPRegsClass 和 IntRegsClass。

另外,SparcRegisterInfo.td 還會輸出 SparcGenRegisterInfo.inc,可以包含到(included)SparcRegisterInfo.cpp 最下邊,後者是寄存器的實現代碼文件。下邊代碼展示了生成的整數寄存器的內容和對應的類。IntRegs 的寄存器順序和 td 文件中的定義時保持一致。

// 整數寄存器類別
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 寄存器類別類型
static const MVT::ValueType IntRegsVTs[] = {
  MVT::i32, MVT::Other
};

namespace SP {    // 寄存器類別的實例
  DFPRegsClass    DFPRegsRegClass;
  FPRegsClass     FPRegsRegClass;
  IntRegsClass    IntRegsRegClass;
  ...

  static const TargetRegisterClass* const IntRegsSubRegClasses [] = {};

  static const TargetRegisterClass* const IntRegsSuperRegClasses [] = {};
  
  ...
    
  IntRegsClass::IntRegsClass() : TargetRegisterClass(IntRegsRegClassID,
                                                     IntRegsVTs, IntRegsSubclasses,
                                                     IntRegsSuperclasses, IntRegsSubRegClasses,
                                                     IntRegsSuperRegClasses, 4, 4, 1,
                                                     IntRegs, IntRegs + 32) {}
}

寄存器分配會避免使用保留寄存器,被調用函數保存的寄存器在所有可分配寄存器都被使用完之前不會被使用。大多數情況下這都是正常的,但在一些特殊情況下,可能需要手動指定分配順序。

4.3 實現一個 TargetRegisterInfo 的子類

寄存器的這一部分,最後一步是手動編寫 XXXRegisterInfo 的代碼,這一部分會實現 TargetRegisterInfo.h 中描述的接口。這些函數如果沒有被重寫(overridden),會返回 0,NULL 或 false。以下列出了一部分 SPARC 後端在 SparcRegisterInfo.cpp 中重寫的函數接口:

  • getCalleeSavedRegs:該函數返回一個被調用函數保存寄存器的列表,預期被用於調用棧幀偏移。
  • getReservedRegs:返回物理寄存器編號的序號列表,表示那些被保留的寄存器。
  • hasFP:返回一個布爾型,表示函數具有專用棧幀寄存器。
  • eliminateCallFramePseudoInstr:如果調用幀需要設置或銷燬僞指令,這個函數會清除它們。
  • eliminateFrameIndex:從使用抽象幀索引的指令中清除它們。
  • emitPrologue:插入 prologue 代碼。
  • emitEpilogure:插入 epilogure 代碼。

(譯註:這些函數的具體功能可參見代碼)

5 指令集

在代碼生成的早期階段,LLVM IR 格式代碼被轉換爲 SelectionDAG 格式,其中的節點SDNode 包含有目標平臺的指令信息。一個 SDNode 具有一個操作碼,還有操作數,類型要求和屬性,這些屬性比如有描述這個節點是可交換的(commutative),或者描述這個節點是一個 load 操作。不同的操作節點類型在 include/llvm/CodeGen/SelectionDAGNodes.h 中描述(NodeType 類型的枚舉屬於 ISD 命名空間)。

TableGen 使用以下列出的 td 文件來生成指令定義的代碼:

  • Target.td:這裏定義了主要的基本類型,比如 Instruction, Operand, InstrInfo 等;
  • TargetSelectionDAG.td:被 SelectionDAG 指令選擇生成器使用,包含有一些 SDTC 開頭的類(這些類是 selectionDAG 類型約束),以及定義 SelectionDAG 節點(比如 imm, cond, bb, add, fadd, sub 等),還有 pattern 的基礎類支持(比如 Pattern, Pat, PatFrag, PatLeaf, ComplexPattern 等)。
  • XXXInstrFormats.td:目標平臺相關的指令 pattern 定義。
  • XXXInstrInfo.td:目標平臺相關的指令模板、條件編碼、指令實現等。根據具體的架構區別,這個文件會有不同的命名,比如對於帶 SSE 指令的 Pentium 架構,這個文件被命名爲 X86InstrSSE.td,對於帶 MMX 指令的 Pentium 架構,這個文件爲 X86InstrMMX.td。(譯註:對於不那麼複雜的架構,可以不修改名稱)。

另外還有和平臺相關的 XXX.td 文件,該文件包含了其他的各種 td 文件,但其內容與子目標直接相關。

你應該完成一個精確的特定平臺下的 XXXInstrInfo 的類(譯註:這裏存疑,這個類默認應該是 TableGen 生成的),用來表示目標機器支持的指令。XXXInstrInfo 中包含有一個 XXXInstrDescriptor 的對象數組,每個對象描述一個指令。這個對象中包含有:

  • 操作編碼的標記名稱
  • 操作數的數量
  • 隱式使用的寄存器和定義的寄存器的列表
  • 目標無關的屬性(如內存操作,是否可替換等)
  • 目標相關的標記

Instruction 類(在 Target.td 中定義)經常會被先繼承爲更復雜的 Instruction 子類,其定義如下:

class Instruction {
  string Namespace = "";
  dag OutOperandList;    // 包含有 MI def 操作數列表的 dag 結構
  dag InOperandList;     // 包含有 MI use 操作數列表的 dag 結構
  string AsmString = ""; // 彙編文件中的指令表示
  list<dag> Pattern;     // 這條指令的 dag patter
  list<Register> Uses = [];
  list<Register> Defs = [];
  list<Predicate> Predicates = [];  // 指令選擇中的謂詞部分
  ...
}

SDNode中包含有平臺相關的指令的描述對象,這些指令的定義在 XXXInstrInfo.td 中定義。硬件架構手冊中有關於指令對象描述信息的說明(比如對於 SPARC 平臺的是 SPARC Architecture Manual)。

架構手冊中一條簡單的指令,可能會依賴於操作數的差異,被擴展爲多條指令。比如,手冊中描述了一條 add 指令,因爲 add 指令可能的操作數是寄存器或者立即數,所以在 LLVM 後端平臺中會有兩個指令,分別是 ADDri 和 ADDrr。

你應該爲沒個指令類別定義 class,然後爲每個不同的操作碼定義子類,同時指定合適的參數(比如固定的編碼部分和可變的部分)。另外還需要指定指令中寄存器佔用的位是哪些,這些也會被編碼,還有指令在輸出彙編格式時如何被打印。

在 SPARC Architecture Manual, Version 8 中,描述了架構主要有三種 32 位格式的指令,第一種格式是 CALL 指令,第二種格式是分支、條件指令以及 SETHI 指令,第三種格式是其他普通指令。

每一類指令格式都有一個對應的類,在 SparcInstrFormat.td 中定義。InstSP 是其他指令類的基類。其他的基類都是某種特殊格式下的結構:比如 F2_1 被用於 SETHI 指令,F2_2 被用於分支指令。另外還有三個基類:F3_1 被用於寄存器與寄存器的操作,F3_2 被用於寄存器與立即數的操作,F3_3 被用於浮點操作。SparcInstrInfo.td 中同樣爲合成指令(synthetic instructions)增加了基類(Pseudo)。

SParcInstrInfo.td 中主要由這些指令和操作數的定義組成。舉一個例子,下邊代碼中,描述了 LDrr 這個指令,它是一個從通過寄存器指定訪問的內存中 load 一個 32 位數據到寄存器的指令。

def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$dst), (ins MEMrr:$addr),
                 "ld [$addr], $dst",
                 [(set i32:$dst, (load ADDRrr:$addr))]>;

第一個參數,3(0b11),表示這個指令所在分類的操作碼;第二個指令,0b000000,表示這個指令特殊的操作碼。第三個參數,(outs IntRegs:$dst),是輸出位置,在這裏是一個寄存器操作數,IntRegs 的定義在寄存器的 td 中;第四個參數,(ins MEMrr:$addr),是一個地址操作數,MEMrr 在 SparcInstrInfo.td 中靠前的位置定義:

def MEMrr : Operand<i32> {
  let PrintMethod = "printMemOperand";
  let MIOperandInfo = (ops IntRegs, IntRegs);
}

第五個參數是一個字符串,它表示彙編輸出的樣式,也可以暫時留空,讓彙編輸出器(addembly printer)接口來實現。第六個參數,也是最後一個參數,是一個 pattern,這個參數用來在 SelectionDAG 指令選擇階段做指令匹配。參考:The LLVM Target-Independent Code Generator,這個參數在下一部分指令選擇時再介紹。

指令類不會根據不同類型的操作數來重載,所以需要根據操作數爲不同的寄存器、內存或立即數類型來分別定義指令類。比如,再針對從立即數指定訪問的內存中 load 一個 32 位數據到寄存器的指令,LDri ,定義如下:

def LDri : F3_2 <3, 0b000000, (outs IntRegs:$dst), (ins MEMri:$addr),
                 "ld [$addr], $dst",
                 [(set i32:$dst, (load ADDRri:$addr))]>;

但是,如果反覆的寫這些相似的指令,會有大量重複的冗餘代碼。在 td 文件中,可以通過 multiclass 關鍵字來同時一次性定義多個指令類(再通過 defm 來同時定義這些指令類的指令)。比如,在 SparcInstrInfo.td 中,F3_12 是個 multiclass,它內部定義了兩個指令類:

multiclass F3_12 <string OpcStr, bits<6> Op3Val, SDNode OpNode> {
  def rr : F3_1 <2, Op3Val,
                 (outs IntRegs:$dst), (ins IntRegs:$b, IntRegs:$c),
                 !strconcat(OpcStr, " $b, $c, $dst"),
                 [(set i32:$dst, (OpNode i32:$b, i32:$c))]>;
  def ri : F3_2 <2, Op3Val,
                 (outs IntRegs:$dst), (ins IntRegs:$b, i32imm:$c),
                 !strconcat(OpcStr, " $b, $c, $dst"),
                 [(set i32:$dst, (OpNode i32:$b, simm13:$c))]>;
}

這樣,我們就可以使用 defm 關鍵字來同時定義多個指令類,比如 XOR 和 ADD 指令,比如下邊代碼,定義了 4 條指令,分別是 XORrr, XORri, ADDrr, ADDri:

defm XOR : F3_12<"xor", 0b000011, xor>;
defm ADD : F3_12<"add", 0b000000, add>;

SparcInstrInfo.td 文件同樣定義了條件碼(condition codes),條件碼會在分支指令中作爲跳轉依據。以下代碼定義了 SPARC 中使用的條件碼位信息,比如,第 10 位表示整形數大於比較狀態,第 22 位表示浮點數大於比較狀態:

def ICC_NE : ICC_VAL< 9>; // 整形不等於
def ICC_E  : ICC_VAL< 1>; // 整形等於
def ICC_G  : ICC_VAL<10>; // 整形大於
...
def FCC_U  : FCC_VAL<23>; // 浮點型未排序
def FCC_G  : FCC_VAL<22>; // 浮點型大於
def FCC_UG : FCC_VAL<21>; // 浮點型未排序或大於

注:Sparc.h 中也定義了一些條件碼相關的枚舉類型,要確保其與這裏的類型保持一致,比如 SPCC::ICC_NE = 9, SPCC::FCC_U = 23。

5.1 指令操作數映射

代碼生成器後端會映射指令操作數到指令的編碼域(field)中。操作數按照定義的順序分配到指令中未綁定的編碼域,而當他們被分配值時會被綁定。比如說,在 Sparc 後端中,定義 XNORrr 指令,它具有 3 個操作數:

def XNORrr : F3_1<2, 0b000111,
                  (outs IntRegs:$dst), (ins IntRegs:$b, IntRegs:$c),
                  "xnor $b, $c, $dst",
                  [(set i32:$dst, (not (xor i32:$b, i32:$c)))]>;

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 字段沒有綁定。

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;
  let Inst{29-25} = rd;    // 目的操作數
  let Inst{24-19} = op3;
  let Inst{18-14} = rs1;   // 第一個源操作數
}

F3 中對 op 域做了綁定,並且定義了 rd, op3 和 rs1 域。F3 類型格式的指令會綁定 rd, op3 和 rs1。

class F3_1<bits<2> opVal, bits<6> op3val, dag outs, dag ins,
           string asmstr, list<dag> pattern>
    : F3<outs, ins, asmstr, pattern> {
  bits<8> asi = 0;
  bits<5> rs2;
  let op = opVal;
  let op3 = op3val;
  let Inst{13} = 0;
  let Inst{12-5} = asi;
  let Inst{4-0} = rs2;    // 第二個源操作數
}

F3_1中綁定了 op3,並定義了 rs2 域。F3_1類型格式的指令會綁定 rd, rs1, rs2。也就是前邊 XNORrr 定義時綁定的$dst$b,和$c操作數,分別對應 rd, rs1, rs2。

5.1.1 指令操作數命名映射

TableGen 還會生成一個 getNamedOperandIdx() 的函數,這個函數用來在 MachineInstr 中,以操作數的 TableGen 名字作爲輸入,查找對應序號。在一個指令的 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
#include "XXXGenInstrInfo.inc"

XXXInstrInfo.h:

#define GET_INSTRINFO_OPERAND_ENUM
#include "XXXGenInstrInfo.inc"

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

5.1.2 指令操作數類型

TableGen 還會生成一個枚舉結構,其中包括了所有後端定義的已命名操作數類型,所在的命名空間是 llvm::XXX::OpTypes。一些通用的立即數類型(比如 i8, i32, i64, f32, f64)被定義在統一的 include/llvm/Target/Target.td 文件中,對每個後端的OpTypes 枚舉都可用。同時,枚舉中僅僅包含已命名的操作數類型,匿名類型會被忽略。比如,X86 後端中定義了 brtarget 和 brtarget8,這兩個操作數類型是 Operand 類的實例化對象:

def brtarget : Operand<OtherVT>;
def brtarget8 : Operand<OtherVT>;

那麼,在枚舉結構中是:

namespace X86 {
namespace OpTypes {
enum OperandType {
  ...
  brtarget,
  brtarget8,
  ...
  i32imm,
  i64imm,
  ...
  OPERAND_TYPE_LIST_END
}
}
}

爲了能使用這個枚舉結構,你需要定義一個預處理宏:

#define GET_INSTRINFO_OPERAND_TYPES_ENUM
#include "XXXGenInstrInfo.inc"

5.2 指令調度

可以通過 MCDesc::getSchedClass() 方法來查看指令行程(itinerary)。對應的值在 TableGen 生成的 XXXGenInstrInfo.inc 中定義爲枚舉結構,所在的命名空間是 llvm::XXX::Sched。調度類的名字等同於 XXXSchedule.td 中的定義。

調度模型由 TableGen 中的 SubtargetEmitter 使用 CodeGenSchedModels 類來生成。這與機器平臺資源使用的行程方法不同。utils/schedcover.py 這個工具可以用來決定哪些指令可以被調度模型所涉及(covered)。第一步是使用以下指令來生成輸出文件,然後調用 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, ,
...

爲了獲取生成調度模型的調試輸出信息,使用以下命令(指定 target 路徑並設定 subtarget-emitter debug 選項):

$ <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 命令,通過在以下命令的輸出中搜索 llvm-tblgen 的輸出內容,從而再次檢查以上命令:

$ VERBOSE=1 make ...

5.3 指令相關映射

這個 TableGen 特性被用於相關的指令間。當你有多個指令格式需要在指令選擇之後互相轉換,那麼可以使用這個特性。這個特性受 XXXInstrInfo.td 文件中目標相關指令的相關模型(relation models)的驅動。相關模型使用 InstrMapping 類作爲基類。TableGen 解析所有的相關模型,並使用確定的信息生成指令的相關映射。相關映射和能夠調用它們的函數一起被髮射到 XXXGenInstrInfo.td 文件中。更多關於這個特性的信息,請參考:How To Use Instruction Mappings

5.4 實現一個 TargetInstrInfo 的子類

最後一步是在 XXXInstrInfo 中硬編碼一些代碼,來實現 TargetInstrInfo.h 中的接口描述。這些函數除非被重寫,否則均返回 0 或布爾類型,又或者直接 assert。以下是被 SPARC 後端重寫實現的函數的列表,定義在 SparcInstrInfo.cpp 中:

  • isLoadFromStackSlot:如果某個指令是從棧槽中直接 load,則返回目的寄存器的值和棧槽的棧幀下標。
  • isStoreToStackSlot:如果某個指令是直接 store 入棧槽,則返回目的寄存器的值和棧槽的棧幀下標。
  • copyPhysReg:在一對物理寄存器之間複製值。
  • storeRegToStackSlot:將一個寄存器值 store 入棧槽。
  • loadRegFromStackSlot:從棧槽中 load 值到一個寄存器。
  • storeRegToAddr:將一個寄存器值 store 入內存。
  • loadRegFromAddr:從內存中 load 值到一個寄存器。
  • foldMemoryOperand:嘗試合併特殊操作數的一些 load 和 store 指令。

5.5 分支摺疊和 If 約定

將指令合併或者消除不可達指令可以提高程序性能。在 XXXInstrInfo 中的 AnalyzeBranch 方法可以實現測試分支條件指令,並移除無效指令。它從一個機器基本塊(MBB, machine basic block)的末尾開始查找可能的性能提升方式,比如分支摺疊和 If 約定。BranchFolder 和 IfConverter 方法(位於 lib/CodeGen 路徑下的 BranchFolding.cpp 和 IfConversion.cpp 文件中)調用 AnalyzeBranch 方法來優化表示這些指令的控制流圖結構。

可以使用 AnalyzeBranch 的多種實現(ARM, Alpha, X86)來作爲你自己的 AnalyzeBranch 實現。SPARC 沒有實現一個能用的 AnalyzeBranch,所以使用 ARM 的實現,描述如下。

AnalyzeBranch 返回一個布爾類型值,並有 4 個參數:

  • MachineBasicBlock &MBB:輸入要檢查的 MBB 塊;
  • MachineBasicBlock *&TBB:將會返回的 MBB 塊,對於一個條件分支,TBB 返回爲真的塊;
  • MachineBasicBlock *&FBB:和 TBB 類似,FBB 返回爲假的塊;
  • std::vector &Cond:評估條件分支中條件的操作數列表;

在以下簡單的例子中,如果一個塊到結束時沒有分支,它就會傳遞到下一個塊,TBB 和 FBB 不會被特殊指定,均返回 NULL。AnalyzeBranch(ARM 版本)的開頭如下:

bool ARMInstrInfo::AnalyzeBranch(MachineBasicBlock &MBB,
                                 MachineBasicBlock *&TBB,
                                 MachineBasicBlock *&FBB,
                                 std::vector<MachineOperand> &Cond) const
{
  MachineBasicBlock::iterator I = MBB.end();
  if (I == MBB.begin() || !isUnpredicatedTerminator(--I))
    return false;

如果一個塊的結尾是一個非條件分支指令,那麼 AnalyzeBranch 會返回這個分支跳轉的塊到 TBB 中:

if (LastOpc == ARM::B || LastOpc == ARM::tB) {
  TBB = LastInst->getOperand(0).getMBB();
  return false;
}

如果一個塊的結尾有兩個非條件分支指令,那麼第二個分支就是不可達的。這種情況下,會移除第二個分支指令,並將跳轉的塊返回到 TBB 中:

if ((SecondLastOpc == ARM::B || SecondLastOpc == ARM::tB) &&
    (LastOpc == ARM::B || LastOpc == ARM::tB)) {
  TBB = SecondLastInst->getOperand(0).getMBB();
  I = LastInst;
  I->eraseFromParent();
  return false;
}

如果一個塊的結尾有一條條件分支指令且條件值爲 false,則會被傳遞到下一個後繼塊。AnalyzeBranch 將分支跳轉的塊範湖到 TBB,並且將操作數返回到 Cond。(譯註:這裏有點怪怪的,沒太看懂)

if (LastOpc == ARM::Bcc || LastOpc == ARM::tBcc) {
  TBB = LastInst->getOperand(0).getMBB();
  Cond.push_back(LastInst->getOperand(1));
  Cond.push_back(LastInst->getOperand(2));
  return false;
}

如果一個塊的結尾包含一個條件分支和一個確定的非條件分支指令,AnalyzeBranch 會將條件分支條件爲 true 的跳轉塊放到 TBB 中返回,將非條件分支跳轉的塊(也就是條件分支爲 false 時會跳轉的塊)放到 FBB 中返回。條件分支的操作數放到 Cond 參數返回。

unsigned SecondLastOpc = SecondLastInst->getOpcode();

if ((SecondLastOpc == ARM::Bcc && LastOpc == ARM::B) ||
    (SecondLastOpc == ARM::tBcc && LastOpc == ARM::tB)) {
  TBB = SecondLastInst->getOperand(0).getMBB();
  Cond.push_back(SecondLastInst->getOperand(1));
  Cond.push_back(SecondLastInst->getOperand(2));
  FBB = LastInst->getOperand(0).getMBB();
  return false;
}

對於最後兩種情況(結束是一條條件分支或一條條件分支和一條非條件分支),Cond 返回的操作數可以傳遞給其他指令的方法,來創建新的分支或其他操作。AnalyzeBranch 需要輔助函數 RemoveBranch 和 InstrBranch 來處理一些操作。

AnalyzeBranch 中,當處理正確時,返回 false(譯註:LLVM 中很多函數都是這樣的),僅僅當遇到無法處理的情況時,比如塊結束時有 3 個終止符或遇到無法處理的終止符時 ,會返回 true。

6 指令選擇

LLVM 使用 SelectionDAG 來表示 LLVM IR 指令,故而 SelectionDAG 的節點應當用來表示原生的目標指令。在代碼生成時,指令選擇階段將非目標相關的 DAG 節點描述的指令轉換爲目標相關的原生指令。這一部分代碼在 XXXISelDAGToDAG.cpp 中描述,通過模式匹配來完成從 DAG 到 DAG 的指令選擇工作。另外可選的是,可以定義個階段(pass)來實現分支指令的類似 DAG 到 DAG 的指令選擇工作(在 XXXBranchSelector.cpp 中描述)。之後,由 XXXISelLowering.cpp 中的代碼來替換或刪除不支持的操作和數據類型(也就是合法化)。

TableGen 爲指令選擇生成的代碼主要在以下的 td 文件中描述:

  • XXXInstrInfo.td:包括目標相關指令的定義,會被生成 XXXGenDAGISel.inc,而這個 inc 被 XXXISELDAGToDAG.cpp 使用;
  • XXXCallingConv.td:包括目標架構支持的調用和返回值約定描述,會被生成 XXXGenCallingConv.inc,這個 inc 被 XXXISelLowering.cpp 使用;

(譯註:以上列表中的描述實際上不是必然的,你可以在任意 C++ 源代碼中引用這些 inc 文件)

指令選擇階段的實現必須包括一個頭文件,聲明 FunctionPass 類或者其子類。在 XXXTargetMachine.cpp 中,需要使用 PassManager 將當前的指令選擇加入當階段序列(queue of passes)中。

LLVM 的靜態編譯器(llc)是一個可視化 DAG 內容非常棒的工具。使用 llc 加上特殊的命令行參數,就可以顯示在不同階段 SelectionDAG 的狀態。具體描述參考:SelectionDAG Instruction Selection Process

爲了描述指令選擇器的行爲,你需要爲 lowering LLVM 代碼 到 SelectionDAG 而指定 pattern。pattern 放在 XXXInstrInfo.td 文件中指令定義的最後一個參數中(譯註:不是絕對的,參見具體後端)。比如,在 SparcInstrInfo.td 中,定義寄存器 store 操作的指令,最後一個參數描述了 pattern。

def STrr : F3_1<3, 0b000100, (outs), (ins MEMrr:$addr, IntRegs:$src),
                "st $src, [$addr]", [(store i32:$src, ADDRrr:$addr)]>;

其中 ADDRrr 是一個 memory mode,也在這個文件中定義:

def ADDRrr : ComplexPattern<i32, 2, "SelectADDRrr", [], []>;

ADDRrr 的定義依賴自 SelectADDRrr,後者是一個定義在指令選擇器中的函數(比如定義在 SparcISelDAGToDAG.cpp 中)。

在 lib/Target/TargetSelectionDAG.td 文件(注,最新的工程中,這個文件位於 include/llvm/Target/TargetSelectionDAG.td,包括下邊的定義,也已更新,這裏不做修改)中,DAG 的操作符 store 定義如下:

def store : PatFrag<(ops node:$val, node:$ptr),
                    (st node:$val, node:$ptr), [{
                      if (StoreSDNode *ST = dyn_cast<StoreSDNode>(N))
                        return !ST->isTruncatingStore() &&
                               ST->getAddressingMode() == ISD::UNINDEXED;
                      return false;
                    }]>;

XXXInstrInfo.td 也生成 SelectCode 方法 (位於 XXXGenDAGISel.inc),這個方法被用來在爲指令匹配合適的處理方法時被調用。在上邊的例子中,SelectionCode 調用 Select_ISD_STORE 來處理 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 的 pattern 被匹配後,會在 XXXGenDAGISel.inc 中爲 Select_ISD_STORE 創建處理 STrr 的代碼,該文件內也會生成 Emit_22 方法,來配合完成選擇過程:

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

6.1 SelectionDAG合法化階段

合法化階段用於將DAG轉換爲使用目標本身支持的類型和操作。你需要在 XXXTargetLowering 中增加實現代碼,從而將原生不支持的類型和操作轉換爲支持的對應類型和操作。

XXXTargetLowering 類的構造函數中,首先使用了 addRegisterClass 方法來指定那些類型是支持的、那些寄存器類與之相配合。寄存器類代碼的設計是 TableGen 使用 XXXRegisterInfo.td 文件生成的,生成文件是 XXXGenRegisterInfo.h.inc。比如,SparcTargetLowering 類的構造函數開頭代碼爲:

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:Load 擴展操作
  • setTruncStoreAction:截斷 Store 操作
  • setIndexedLoadAction:序列 Load (Indexed Load)
  • setIndexedStoreAction:序列 Store (Indexed Store)
  • setConvertAction:類型轉換操作
  • setCondCodeAction:條件碼相關

注:舊一些的版本中,使用 setLoadXAction 代替 setLoadExtAction,並且 setCondCodeAction 可能不支持。請自己檢查自己的 LLVM 版本是否支持這些操作。

這些回調函數用來決定一個操作在特定類型下如何做,所有的 case 中,第三個參數是一個 LegalAction 類型,這是一個枚舉,包括枚舉值:Promote, Expand, Custom, Legal。在SparcISelLowering.cpp 中包含了全部的情況。

6.1.1 Promote

可以將一個原生不支持的類型 Promote 到一個更大的但支持的類型。比如,SPARC 不支持符號擴展的布爾型(i1 type),所以在 SparcISelLowering.cpp 中,以下代碼,指定在 load 之前,使用 Promote 將 i1 type 轉爲更大的類型。

setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);

6.1.2 Expand

可以將一個原生不支持的類型 Expand,而不是 Promote,使用其他操作的組合來實現功能。比如,SPARC 中浮點的正弦和餘弦運算可以通過展開成多個指令完成,代碼如下:

setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);

6.1.3 Custom

對於一些操作,簡單的類型 Promote 或類型 Expand 可能不適用。這是,就需要實現一些特殊的 intrinsic 函數。

比如,如果一個常數需要做特殊處理,或者一個操作需要 spill 和 restore 寄存器到棧,需要寄存器分配器協助。

在 SparcISelLowering.cpp 中,如下代碼,展示了一個從浮點到有符號整型的類型轉換,首先調用setOperationAction 並指定 Custom 值:

setOperationAction(ISD::FP_TO_SINT, MVT::i32, Custom);

然後,在LowerOperation 方法中,對於每個 Custom操作,對應一個條件 case。代碼如下,會調用到 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 需要被實現,實現代碼大致如下:

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

6.1.4 Legal

Legal 這個枚舉值只是表示一種操作是原生支持的,是默認的情況,所以其很少被使用。在 SparcISelLowering.cpp 中,CTPOP 這個操作(計算一個整型值中位爲1的數量)只在 SPARC V9 中原生支持。接下來的代碼表示在非 V9 的其他 SPARC 平臺上通過 Expand 來實現這個操作:

setOperationAction(ISD::CTPOP, MVT::i32, Expand);
...
if (TM.getSubtarget<SparcSubtarget>().isV9())
    setOperationAction(ISD::CTPOP, MVT::i32, Legal);   // 如果是 V9 那麼就原生支持

6.2 調用約定

爲了能夠支持目標相關的調用約定,XXXCallingConv.td (譯註,原文爲XXXGenCallingConv.td應有誤,已修正)文件中調用了一些在 lib/Target/TargetCallingConv.td (更新位於 include/llvm/CodeGen 中)文件內定義的接口,比如 CCIfType 和 CCAssignToReg。TableGen 使用 XXXCallingConv.td 來生成 XXXGenCallingConv.inc 文件,這個文件被 XXXISelLowering.cpp 文件所包含與使用。在 TargetCallingConv.td 中有一些接口功能有:

  • 參數分配的順序
  • 參數和返回值的存放位置(棧或寄存器)
  • 哪些寄存器用來分配
  • 調用方還是被調用方展開棧。

以下的例子展示了使用 CCIfType 和 CCAssignToReg 接口。如果 CCIfType 值爲 true (這表示當前的參數是 f32 或 f64),進而會觸發動作。當前的處理情況是使用 CCAssignToReg 將參數的值分配給靠前的未分配的寄存器,比如 R0 或 R1。

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

在 SparcCallingConv.td 文件中包含有目標相關的返回值調用約定的描述(RetCC_Sparc32)以及一個通用的C標準調用約定的描述(CC_Sparc32)。前者的定義如下,它指示了哪些寄存器被用作特殊的標量返回類型。包括單精度浮點型將返回值到寄存器 F0,雙精度浮點型會返回值到寄存器 D0,而32位整型會返回到寄存器 I0 或 I1。

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

而 CCIfCC 這個接口嘗試將給定的名稱與當前調用約定做匹配,如果名稱匹配當前的調用約定,這調用指定的操作。下邊是 X86的例子 (位於 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>
]>;

其他的一些類似的接口還有:

  • CCIf <predicate, action>:如果匹配 predicate,執行動作。
  • CCIfInReg < action>:如果參數被標記爲 inreg 屬性,執行動作。
  • CCIfNest < action>:如果參數被標記爲 nest 屬性,執行動作。
  • CCIfNotVarArg < action>:如果當前函數沒有可變長參數表,執行動作。
  • CCAssignToRegWithShadow <registerList, shadowList>:和 CCAssignToReg 類似,但是還包括一個寄存器的 shadow 列表。
  • CCPassByVal <size, align>:以指定最小的 size 和 align 向棧槽賦值。
  • CallingConv <[action]>:爲每一個支持的調用約定做定義。

(譯註:以上這些接口我都沒遇到過,所以不好做解釋)

7 彙編輸出

在 code emission 階段,代碼生成器可能需要使用一個 LLVM 的 pass 來處理彙編輸出工作(彙編的輸出是可選的)。你需要實現 assemble printer 的代碼,將 LLVM IR 轉換成 GAS 的彙編格式,主要步驟有:

  • 定義你的目標平臺支持的所有彙編語法字符串,將他們與 XXXInstrInfo.td 中的指令定義相對應。TableGen 將會爲你生成這些映射 (XXXGenAsmWriter.inc 文件),實現了一個 位於 XXXAsmPrinter 類的 printInstruction 成員函數。
  • 編寫 XXXTargetAsmInfo.h 文件,包含了 XXXTargetAsmInfo 類的最基本聲明信息,這個類是 TargetAsmInfo 的子類。
  • 編寫 XXXTargetAsmInfo.cpp 文件,包含了目標相關的 TargetAsmInfo 的屬性信息和一些方法的實現。
  • 編寫 XXXAsmPrinter.cpp 文件,實現 AsmPrinter 類,完成轉換的主要代碼。

在 XXXTargetAsmInfo.h 文件對 XXXTargetAsmInfo.cpp 通常不怎麼重要,並且其實 XXXTargetAsmInfo.cpp 中也只有一小部分聲明來覆蓋父類 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)中,覆蓋了默認的 ExpandInlineAsm 方法。

目標相關的 AsmPrinter 的實現位於 XXXAsmPrinter.cpp 中,定義了 AsmPrinter 類來完成轉換工作。必須包含下列頭文件:

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

MachineFunctionPass 是 FunctionPass 的子類,AsmPrinter 被註冊爲一個 MachineFunctionPass,它會首先調用 doInitialization 來設置 AsmPrinter。在 SparcAsmPrinter 中,一個 Mangler 對象被實例化來處理變量名。

在 XXXAsmPrinter.cpp 中,必須實現 runOnMachineFunction 方法(在 MachineFunctionPass 中聲明),在MachineFunctionPass 方法中,runOnFunction 方法調用了 runOnMachineFunction。目標相關的信息主要在 runOnMachineFunction 中有差別,通常會調用這些函數:

  • 調用 SetupMachineFunction 來初始化。
  • 調用 EmitConstantPool 來輸出 spill 到內存的常量。
  • 調用 EmitJumpTableInfo 來輸出當前函數的跳轉表。
  • 輸出當前函數的標籤(Label)。
  • 輸出當前函數的代碼內容,包括基本塊的標籤和裏邊的彙編指令(使用 printInstruction 實現)。

在 XXXAsmPrinter 中還需要使用 XXXGenAsmWriter.inc 中的函數,後者中包含了 printInstruction 的實現,該函數會調用以下函數:

  • printOperand
  • printMemOperand
  • printCCOperand
  • printDataDirective
  • printDeclare
  • printImplilcitDef
  • printInlineAsm

這些函數的實現在 AsmPrinter.cpp 中,通常來說都是可以直接適用的,不需要子類 XXXAsmPrinter 覆蓋。

printOperand 方法中有個非常長的 switch/case 結構來匹配不同類型的操作數:寄存器、立即數、基本塊、外部符號、全局地址、常量池索引、跳轉表索引。而對於包含內存操作數的指令,會使用 printMemOperand 方法來生成合適的彙編輸出形式。同理,像 printCCOperand 函數會輸出合理的條件碼操作數。

doFinalization 函數會在 XXXAsmPrinter 中覆蓋定義,將會在 assembly printer 完成工作後被調用。在 doFinalization 內,全局符號和常量將會被輸出。

8 子目標平臺支持

Subtarget(以下翻譯成 子目標平臺)被用於在對某種給定的芯片型號所對應的指令集進行代碼生成使用。比如,LLVM 的 SPARC 實現中包括有 3 種主要的 SPARC 微處理器架構版本:Version 8 (V8,32 位架構),Version 9 (V9,64 位架構)以及 UltraSPARC 架構。V8 有 16 位雙精度浮點寄存器,也可以被用作 32 位的單精度浮點或 8 位的四精度浮點寄存器。V8 是大端架構。V9 有 32 位雙精度浮點寄存器,也可以被用作 16 位的四精度浮點寄存器,但是不能被用作單精度浮點寄存器。UltraSPARC 架構在 V9 的指令集上做了擴展。

如果需要子目標平臺,你應該實現一個目標相關的 XXXSubtarget 的類,通過命令行參數 -mcpu= 和 -mattr= 可以訪問到這個類的功能。

TableGen 使用 Target.td 和 Sparc.td 中的內容來生成 SparcGenSubtarget.inc 文件。在 Target.td 中,定義了 SubtargetFeature 接口,如下邊代碼。前 4 個 string 參數是特徵名稱、屬性集、屬性的值和特性的描述,第五個參數是一個隱含屬性的列表,默認是空。

class SubtargetFeature<string n, string a, string v, string d, 
                       list<SubtargetFeature> i = []> {
  string Name = n;
  string Attribute = a;
  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",
                                           "V8DeprecatedInsts", "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]>;

通過這兩個 td 文件生成的 SparcGenSubtarget.inc 中,通過枚舉值來識別特性,用常數數組表示 CPU 特性和子平臺類型。ParseSubtargetFeatures 方法用來解析指定子平臺的特徵字符串。

這個 inc 文件應該被 SparcSubtarget.td 中包含,目標相關的一些實現放在 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
}

9 JIT支持

JIT (Just-In-Time,翻譯爲即時編譯)的代碼生成是目標平臺可選的一個功能,它可以發射二進制形式的機器碼和其他輔助信息並寫入內存。JIT 的實現主要有以下幾個步驟:

  • 編寫 XXXCodeEmitter.cpp 文件來包含一個 machine function 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 的類,其中實現了很多用於將數據寫到輸出流的回調函數。

9.1 機器碼輸出

在 XXXCodeEmitter.cpp 文件中,目標相關的部分在 Emitter 類中被實現爲一個 function pass (是 MachineFunctionPass 的子類)。目標相關的 runOnMachineFunction 實現(由 MachineFunctionPass 中的 runOnFunction 調用)迭代 MachineBasicBlock 並調用 emitInstruction 來處理每一條指令,併發射二進制碼。emitInstruction 的實現非常大,主要結構是一個 switch-case,用來選擇指令類型,這些指令類型在 XXXInstrInfo.h 中定義。比如,在 X86CodeEmitter.cpp 中,emitInstruction 方法的實現部分如下:

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 的具體實現經常是先發射操作碼的編碼,再發射操作數的編碼,根據操作數類型的不同,可以調用 helper 方法來處理不同的操作數。比如,在 X86CodeEmitter.cpp 中,X86II::AddRegFrm case 中,通過 emitBytes 發出的第一個數據是添加寄存器操作數的操作碼。然後發射操作數,MO1。helper 方法,比如 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 文件中定義,並被用於 relocate 方法(在 XXXJITInfo.cpp 中定義)中來重寫全局符號的引用(也就是寫入重定位信息)。

比如,X86Relocations.h 中指定了一下的重定位類型。這 4 中類型都會將重定位值添加到內存中。對於 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
}

9.2 目標平臺JIT信息

XXXJITInfo.cpp 文件中實現了目標平臺相關的 JIT 指令生成功能,比如發射機器碼和樁。一個最小的實現需要以下幾部分:

  • getLazyResolverFunction:初始化 JIT,給定目標一個用於編譯的函數;
  • emitFunctionStub:會返回一個指定回調函數的地址;
  • relocate:改變全局引用的地址,需要 relocation type 的參與;
  • 函數樁的包裝(wrapper)回調函數,在最初還不知道目標時使用;

getLazyResolverFunction 需要被繼承實現。它將輸入參數放入全局的 JITCompilerFunction 並返回一個回調函數,該回調函數會被用作一個函數包裝。 對於 Alpha 目標平臺(在 AlphaJITInfo.cpp 中),getLazyResolverFunction 的實現如下:

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

對於 X86 架構,getLazyResolverFunction 實現的更爲複雜一些,因爲它會返回一個更加複雜的回調函數,比如會包含 SSE 指令 和 XMM 寄存器。

回調函數初始化保存和之後還原被調用者寄存器值、輸入參數以及棧和返回地址。回調函數需要底層的功能來訪問寄存器或棧,所以它通常和彙編器一起實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章