编写 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 中定义的指令中。您还应该为 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 的与目标无关的代码生成器,您应该像所有当前机器后端一样:创建 LLVMTargetMachine 的子类。(要从头开始创建目标,请创建 TargetMachine 的子类。)

要让 LLVM 实际构建和链接您的目标,您需要使用 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy 运行 cmake。这将构建您的目标,而无需将其添加到所有目标列表中。

一旦您的目标稳定,您可以将其添加到主 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”后跟三个值:第一个指示长双精度数的大小,然后是 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 中定义。定义的值包括整数类型(例如 i16i32i1 用于布尔值)、浮点类型(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
    )>;

使用 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) {}
}

寄存器分配器将避免使用保留寄存器,并且在使用完所有易失寄存器之前不会使用被调用者保存的寄存器。这通常足够好,但在某些情况下,可能需要提供自定义分配顺序。

实现 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 指令的奔腾,此文件为 X86InstrSSE.td,对于带有 MMX 的奔腾,此文件为 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))]>;

第四个参数是输入源,它使用地址操作数 MEMrr,该操作数在 SparcInstrInfo.td 中较早定义。

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的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功能用于将指令彼此关联。当您有多种指令格式并且需要在指令选择后在它们之间切换时,它特别有用。此整个功能由关系模型驱动,这些关系模型可以在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 — 返回的目标块。对于计算结果为真的条件分支,TBB是目标。

  • MachineBasicBlock *&FBB — 对于计算结果为假的条件分支,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;
}

块可能以单个条件分支指令结束,如果条件计算结果为假,则该指令会贯穿到后继块。在这种情况下,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 中,过程管理器 (PM) 应该将每个指令选择过程添加到要运行的过程队列中。

LLVM 静态编译器 (llc) 是一个用于可视化 DAG 内容的优秀工具。要显示特定处理阶段之前或之后的 SelectionDAG,请使用 llc 的命令行选项,在 SelectionDAG 指令选择过程 中进行了描述。

要描述指令选择器的行为,应将 LLVM 代码降低到 SelectionDAG 的模式作为指令定义在 XXXInstrInfo.td 中的最后一个参数添加。例如,在 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 值的示例。

提升

对于给定类型没有原生支持的操作,可以将指定类型提升到受支持的更大类型。例如,SPARC 不支持布尔值(i1 类型)的带符号扩展加载,因此在 SparcISelLowering.cpp 中,下面的第三个参数 Promotei1 类型值更改为较大的类型后再加载。

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

扩展

对于没有原生支持的类型,可能需要进一步分解值,而不是提升。对于没有原生支持的操作,可以使用其他操作的组合来达到类似的效果。在 SPARC 中,浮点正弦和余弦三角函数操作通过扩展到其他操作来支持,如第三个参数 ExpandsetOperationAction 所示。

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

自定义

对于某些操作,简单的类型提升或操作扩展可能不够。在某些情况下,必须实现特殊的内在函数。

例如,常数值可能需要特殊处理,或者操作可能需要在堆栈中溢出和恢复寄存器并与寄存器分配器一起工作。

如下所示 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);
  ...
  }
}

最后,使用 FP 寄存器将浮点值转换为整数,实现 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);
}

调用约定

为了支持特定目标的调用约定,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]>>
]>;

CC_Sparc32SparcCallingConv.td 中的定义引入了 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 传递来生成汇编输出。为此,您需要实现一个打印机的代码,该打印机使用以下步骤将 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 方法会调用 runOnMachineFunction。针对不同目标的 runOnMachineFunction 实现方式有所不同,但通常会执行以下步骤来处理每个机器函数

  • 调用 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 case,首先发出的数据(通过 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 实现目标特定代码生成活动的 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 寄存器的处理器返回不同的回调函数。

回调函数最初保存并随后恢复被调用者寄存器值、传入参数以及帧和返回地址。回调函数需要对寄存器或堆栈进行低级访问,因此它通常使用汇编程序实现。