编写 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
) 的更多信息,请参阅 Using As,特别是对于汇编打印机。 “Using As” 包含目标机器相关功能的列表。
基本步骤¶
要为 LLVM 编写编译器后端,将 LLVM IR 转换为指定目标(机器或其他语言)的代码,请按照以下步骤操作
创建
TargetMachine
类的子类,描述目标机器的特性。 复制特定TargetMachine
类和头文件的现有示例; 例如,从SparcTargetMachine.cpp
和SparcTargetMachine.h
开始,但更改文件的名称以适应您的目标。 同样,更改引用 “Sparc
” 的代码以引用您的目标。描述目标的寄存器集。 使用 TableGen 从特定于目标的
RegisterInfo.td
输入文件生成寄存器定义、寄存器别名和寄存器类的代码。 您还应该为TargetRegisterInfo
类的子类编写额外的代码,该子类表示用于寄存器分配的类寄存器文件数据,并描述寄存器之间的交互。描述目标的指令集。 使用 TableGen 从特定于目标的
TargetInstrFormats.td
和TargetInstrInfo.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 目标)。 或者,您可以将库拆分为 LLVMDummyCodeGen
和 LLVMDummyAsmPrinter
,后者应在 lib/Target/Dummy
下的子目录中实现 (例如,请参阅 PowerPC 目标)。
请注意,这两种命名方案硬编码到 llvm-config
中。 使用任何其他命名方案都会混淆 llvm-config
,并在链接 llc
时产生大量(看似无关的)链接器错误。
为了使您的目标实际执行某些操作,您需要实现 TargetMachine
的子类。 此实现通常应位于文件 lib/Target/DummyTargetMachine.cpp
中,但将构建 lib/Target
目录中的任何文件,并且应该可以工作。 要使用 LLVM 的目标无关代码生成器,您应该执行当前所有机器后端的操作:创建 CodeGenTargetMachineImpl
的子类。 (要从头开始创建目标,请创建 TargetMachine
的子类。)
要使 LLVM 实际构建和链接您的目标,您需要使用 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy
运行 cmake
。 这将构建您的目标,而无需将其添加到所有目标的列表中。
一旦您的目标稳定,您可以将其添加到主 CMakeLists.txt
中找到的 LLVM_ALL_TARGETS
变量。
目标机器¶
CodeGenTargetMachineImpl
设计为使用 LLVM 目标无关代码生成器实现的目标的基类。 CodeGenTargetMachineImpl
类应由具体的子类进行专门化,该子类实现各种虚方法。 CodeGenTargetMachineImpl
在 include/llvm/CodeGen/CodeGenTargetMachineImpl.h
中定义为 TargetMachine
的子类。 TargetMachine
类实现 (include/llvm/Target/TargetMachine.cpp
) 也处理许多命令行选项。
要创建 CodeGenTargetMachineImpl
的具体目标特定子类,请从复制现有的 TargetMachine
类和头文件开始。 您应该命名您创建的文件,以反映您的特定目标。 例如,对于 SPARC 目标,将文件命名为 SparcTargetMachine.h
和 SparcTargetMachine.cpp
。
对于目标机器 XXX
,XXXTargetMachine
的实现必须具有访问方法来获取表示目标组件的对象。 这些方法命名为 get*Info
,旨在获取指令集 (getInstrInfo
)、寄存器集 (getRegisterInfo
)、堆栈帧布局 (getFrameInfo
) 和类似信息。 XXXTargetMachine
还必须实现 getDataLayout
方法,以访问具有特定于目标的数据特征的对象,例如数据类型大小和对齐要求。
例如,对于 SPARC 目标,头文件 SparcTargetMachine.h
声明了几个 get*Info
和 getDataLayout
方法的原型,这些方法只是返回类成员。
namespace llvm {
class Module;
class SparcTargetMachine : public CodeGenTargetMachineImpl {
const DataLayout DataLayout; // Calculates type size & alignment
SparcSubtarget Subtarget;
SparcInstrInfo InstrInfo;
TargetFrameInfo FrameInfo;
protected:
virtual const TargetAsmInfo *createTargetAsmInfo() const;
public:
SparcTargetMachine(const Module &M, const std::string &FS);
virtual const SparcInstrInfo *getInstrInfo() const {return &InstrInfo; }
virtual const TargetFrameInfo *getFrameInfo() const {return &FrameInfo; }
virtual const TargetSubtarget *getSubtargetImpl() const{return &Subtarget; }
virtual const TargetRegisterInfo *getRegisterInfo() const {
return &InstrInfo.getRegisterInfo();
}
virtual const DataLayout *getDataLayout() const { return &DataLayout; }
// Pass Pipeline Configuration
virtual bool addInstSelector(PassManagerBase &PM, bool Fast);
virtual bool addPreEmitPass(PassManagerBase &PM, bool Fast);
};
} // end namespace llvm
getInstrInfo()
getRegisterInfo()
getFrameInfo()
getDataLayout()
getSubtargetImpl()
对于某些目标,您还需要支持以下方法
getTargetLowering()
getJITInfo()
某些架构(例如 GPU)不支持跳转到任意程序位置,而是使用掩码执行实现分支,并使用循环体周围的特殊指令实现循环。 为了避免引入此类硬件无法处理的不可约控制流的 CFG 修改,目标在初始化时必须调用 setRequiresStructuredCFG(true)。
此外,XXXTargetMachine
构造函数应指定 TargetDescription
字符串,该字符串确定目标机器的数据布局,包括指针大小、对齐方式和字节序等特征。 例如,SparcTargetMachine
的构造函数包含以下内容
SparcTargetMachine::SparcTargetMachine(const Module &M, const std::string &FS)
: DataLayout("E-p:32:32-f128:128:128"),
Subtarget(M, FS), InstrInfo(Subtarget),
FrameInfo(TargetFrameInfo::StackGrowsDown, 8, 0) {
}
连字符分隔 TargetDescription
字符串的各个部分。
字符串中的大写 “
E
” 表示大端目标数据模型。 小写 “e
” 表示小端。“
p:
” 后跟指针信息:大小、ABI 对齐和首选对齐。 如果 “p:
” 后仅跟两个数字,则第一个值是指针大小,第二个值是 ABI 和首选对齐。然后是数字类型对齐的字母:“
i
”、“f
”、“v
” 或 “a
”(分别对应于整数、浮点、向量或聚合)。 “i
”、“v
” 或 “a
” 后跟 ABI 对齐和首选对齐。 “f
” 后跟三个值:第一个值表示 long double 的大小,然后是 ABI 对齐,然后是 ABI 首选对齐。
目标注册¶
您还必须向 TargetRegistry
注册您的目标,这是其他 LLVM 工具用于在运行时查找和使用您的目标的方式。 可以直接使用 TargetRegistry
,但对于大多数目标,都有辅助模板可以为您处理工作。
所有目标都应声明一个全局 Target
对象,该对象用于在注册期间表示目标。 然后,在目标的 TargetInfo
库中,目标应定义该对象并使用 RegisterTarget
模板注册目标。 例如,Sparc 注册代码如下所示
Target llvm::getTheSparcTarget();
extern "C" void LLVMInitializeSparcTargetInfo() {
RegisterTarget<Triple::sparc, /*HasJIT=*/false>
X(getTheSparcTarget(), "sparc", "Sparc");
}
这允许 TargetRegistry
按名称或目标三元组查找目标。 此外,大多数目标还将注册单独库中提供的其他功能。 这些注册步骤是分开的,因为某些客户端可能希望仅链接到目标的某些部分 — 例如,JIT 代码生成器不需要使用汇编打印机。 以下是注册 Sparc 汇编打印机的示例
extern "C" void LLVMInitializeSparcAsmPrinter() {
RegisterAsmPrinter<SparcAsmPrinter> X(getTheSparcTarget());
}
有关更多信息,请参阅 “llvm/Target/TargetRegistry.h”。
寄存器集和寄存器类¶
您应该描述一个具体的特定于目标的类,该类表示目标机器的寄存器文件。 此类称为 XXXRegisterInfo
(其中 XXX
标识目标),表示用于寄存器分配的类寄存器文件数据。 它还描述了寄存器之间的交互。
您还需要定义寄存器类以对相关寄存器进行分类。 对于某些指令以相同方式处理的所有寄存器组,应添加寄存器类。 典型的示例是整数、浮点或向量寄存器的寄存器类。 寄存器分配器允许指令使用指定寄存器类中的任何寄存器以类似的方式执行指令。 寄存器类从这些集合为指令分配虚拟寄存器,并且寄存器类允许独立于目标的寄存器分配器自动选择实际寄存器。
寄存器的许多代码,包括寄存器定义、寄存器别名和寄存器类,都是由 TableGen 从 XXXRegisterInfo.td
输入文件生成的,并放置在 XXXGenRegisterInfo.h.inc
和 XXXGenRegisterInfo.inc
输出文件中。 XXXRegisterInfo
实现中的某些代码需要手动编码。
定义寄存器¶
XXXRegisterInfo.td
文件通常以目标机器的寄存器定义开头。 Register
类(在 Target.td
中指定)用于为每个寄存器定义一个对象。 指定的字符串 n
成为寄存器的 Name
。 基本的 Register
对象没有任何子寄存器,也没有指定任何别名。
class Register<string n> {
string Namespace = "";
string AsmName = n;
string Name = n;
int SpillSize = 0;
int SpillAlignment = 0;
list<Register> Aliases = [];
list<Register> SubRegs = [];
list<int> DwarfNumbers = [];
}
例如,在 X86RegisterInfo.td
文件中,有一些寄存器定义使用 Register
类,例如
def AL : Register<"AL">, DwarfRegNum<[0, 0, 0]>;
这定义了寄存器 AL
并为其分配了值(使用 DwarfRegNum
),这些值供 gcc
、gdb
或调试信息写入器用于标识寄存器。 对于寄存器 AL
,DwarfRegNum
采用包含 3 个值的数组,表示 3 种不同的模式:第一个元素用于 X86-64,第二个元素用于 X86-32 上的异常处理 (EH),第三个元素是通用的。 -1 是一个特殊的 Dwarf 编号,表示 gcc 编号未定义,-2 表示寄存器编号对于此模式无效。
从先前描述的 X86RegisterInfo.td
文件中的行,TableGen 在 X86GenRegisterInfo.inc
文件中生成此代码
static const unsigned GR8[] = { X86::AL, ... };
const unsigned AL_AliasSet[] = { X86::AX, X86::EAX, X86::RAX, 0 };
const TargetRegisterDesc RegisterDescriptors[] = {
...
{ "AL", "AL", AL_AliasSet, Empty_SubRegsSet, Empty_SubRegsSet, AL_SuperRegsSet }, ...
TableGen 从寄存器信息文件为每个寄存器生成一个 TargetRegisterDesc
对象。 TargetRegisterDesc
在 include/llvm/Target/TargetRegisterInfo.h
中定义,包含以下字段
struct TargetRegisterDesc {
const char *AsmName; // Assembly language name for the register
const char *Name; // Printable name for the reg (for debugging)
const unsigned *AliasSet; // Register Alias Set
const unsigned *SubRegs; // Sub-register set
const unsigned *ImmSubRegs; // Immediate sub-register set
const unsigned *SuperRegs; // Super-register set
};
TableGen 使用整个目标描述文件 (.td
) 来确定寄存器的文本名称(在 TargetRegisterDesc
的 AsmName
和 Name
字段中)以及其他寄存器与已定义寄存器的关系(在其他 TargetRegisterDesc
字段中)。 在此示例中,其他定义将寄存器 “AX
”、“EAX
” 和 “RAX
” 建立为彼此的别名,因此 TableGen 为此寄存器别名集生成一个以 null 结尾的数组 (AL_AliasSet
)。
Register
类通常用作更复杂类的基类。 在 Target.td
中,Register
类是 RegisterWithSubRegs
类的基类,该类用于定义需要在 SubRegs
列表中指定子寄存器的寄存器,如下所示
class RegisterWithSubRegs<string n, list<Register> subregs> : Register<n> {
let SubRegs = subregs;
}
在 SparcRegisterInfo.td
中,为 SPARC 定义了其他寄存器类:Register
子类 SparcReg
,以及进一步的子类:Ri
、Rf
和 Rd
。 SPARC 寄存器由 5 位 ID 号标识,这是这些子类的共同特征。 请注意使用 “let
” 表达式来覆盖最初在超类中定义的值(例如 Rd
类中的 SubRegs
字段)。
class SparcReg<string n> : Register<n> {
field bits<5> Num;
let Namespace = "SP";
}
// Ri - 32-bit integer registers
class Ri<bits<5> num, string n> :
SparcReg<n> {
let Num = num;
}
// Rf - 32-bit floating-point registers
class Rf<bits<5> num, string n> :
SparcReg<n> {
let Num = num;
}
// Rd - Slots in the FP register file for 64-bit floating-point values.
class Rd<bits<5> num, string n, list<Register> subregs> : SparcReg<n> {
let Num = num;
let SubRegs = subregs;
}
在 SparcRegisterInfo.td
文件中,有一些寄存器定义使用 Register
的这些子类,例如
def G0 : Ri< 0, "G0">, DwarfRegNum<[0]>;
def G1 : Ri< 1, "G1">, DwarfRegNum<[1]>;
...
def F0 : Rf< 0, "F0">, DwarfRegNum<[32]>;
def F1 : Rf< 1, "F1">, DwarfRegNum<[33]>;
...
def D0 : Rd< 0, "F0", [F0, F1]>, DwarfRegNum<[32]>;
def D1 : Rd< 2, "F2", [F2, F3]>, DwarfRegNum<[34]>;
上面显示的最后两个寄存器 (D0
和 D1
) 是双精度浮点寄存器,它们是单精度浮点子寄存器对的别名。 除了别名之外,已定义寄存器的子寄存器和超寄存器关系还在寄存器的 TargetRegisterDesc
的字段中。
定义寄存器类¶
RegisterClass
类(在 Target.td
中指定)用于定义表示一组相关寄存器的对象,并定义寄存器的默认分配顺序。 使用 Target.td
的目标描述文件 XXXRegisterInfo.td
可以使用以下类构造寄存器类
class RegisterClass<string namespace,
list<ValueType> regTypes, int alignment, dag regList> {
string Namespace = namespace;
list<ValueType> RegTypes = regTypes;
int Size = 0; // spill size, in bits; zero lets tblgen pick the size
int Alignment = alignment;
// CopyCost is the cost of copying a value between two registers
// default value 1 means a single instruction
// A negative value means copying is extremely expensive or impossible
int CopyCost = 1;
dag MemberList = regList;
// for register classes that are subregisters of this class
list<RegisterClass> SubRegClassList = [];
code MethodProtos = [{}]; // to insert arbitrary code
code MethodBodies = [{}];
}
要定义 RegisterClass
,请使用以下 4 个参数
定义的第一个参数是命名空间的名称。
第二个参数是
ValueType
寄存器类型值的列表,这些值在include/llvm/CodeGen/ValueTypes.td
中定义。 定义的值包括整数类型(例如i16
、i32
和i1
用于布尔值)、浮点类型 (f32
、f64
) 和向量类型(例如,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
对象:FPRegs
、DFPRegs
和 IntRegs
。 对于所有三个寄存器类,第一个参数都使用字符串 “SP
” 定义命名空间。 FPRegs
定义了一组 32 个单精度浮点寄存器 (F0
到 F31
); DFPRegs
定义了一组 16 个双精度寄存器 (D0-D15
)。
// F0, F1, F2, ..., F31
def FPRegs : RegisterClass<"SP", [f32], 32, (sequence "F%u", 0, 31)>;
def DFPRegs : RegisterClass<"SP", [f64], 64,
(add D0, D1, D2, D3, D4, D5, D6, D7, D8,
D9, D10, D11, D12, D13, D14, D15)>;
def IntRegs : RegisterClass<"SP", [i32], 32,
(add L0, L1, L2, L3, L4, L5, L6, L7,
I0, I1, I2, I3, I4, I5,
O0, O1, O2, O3, O4, O5, O7,
G1,
// Non-allocatable regs:
G2, G3, G4,
O6, // stack ptr
I6, // frame ptr
I7, // return address
G0, // constant zero
G5, G6, G7 // reserved for kernel
)>;
将 SparcRegisterInfo.td
与 TableGen 结合使用会生成多个输出文件,这些文件旨在包含在您编写的其他源代码中。 SparcRegisterInfo.td
生成 SparcGenRegisterInfo.h.inc
,应将其包含在您编写的 SPARC 寄存器实现的头文件 (SparcRegisterInfo.h
) 中。 在 SparcGenRegisterInfo.h.inc
中,定义了一个名为 SparcGenRegisterInfo
的新结构,该结构使用 TargetRegisterInfo
作为其基类。 它还指定了基于已定义的寄存器类的类型:DFPRegsClass
、FPRegsClass
和 IntRegsClass
。
SparcRegisterInfo.td
还生成 SparcGenRegisterInfo.inc
,它包含在 SPARC 寄存器实现 SparcRegisterInfo.cpp
的底部。 下面的代码仅显示生成的整数寄存器和关联的寄存器类。 IntRegs
中寄存器的顺序反映了目标描述文件中 IntRegs
定义中的顺序。
// IntRegs Register Class...
static const unsigned IntRegs[] = {
SP::L0, SP::L1, SP::L2, SP::L3, SP::L4, SP::L5,
SP::L6, SP::L7, SP::I0, SP::I1, SP::I2, SP::I3,
SP::I4, SP::I5, SP::O0, SP::O1, SP::O2, SP::O3,
SP::O4, SP::O5, SP::O7, SP::G1, SP::G2, SP::G3,
SP::G4, SP::O6, SP::I6, SP::I7, SP::G0, SP::G5,
SP::G6, SP::G7,
};
// IntRegsVTs Register Class Value Types...
static const MVT::ValueType IntRegsVTs[] = {
MVT::i32, MVT::Other
};
namespace SP { // Register class instances
DFPRegsClass DFPRegsRegClass;
FPRegsClass FPRegsRegClass;
IntRegsClass IntRegsRegClass;
...
// IntRegs Sub-register Classes...
static const TargetRegisterClass* const IntRegsSubRegClasses [] = {
NULL
};
...
// IntRegs Super-register Classes..
static const TargetRegisterClass* const IntRegsSuperRegClasses [] = {
NULL
};
...
// IntRegs Register Class sub-classes...
static const TargetRegisterClass* const IntRegsSubclasses [] = {
NULL
};
...
// IntRegs Register Class super-classes...
static const TargetRegisterClass* const IntRegsSuperclasses [] = {
NULL
};
IntRegsClass::IntRegsClass() : TargetRegisterClass(IntRegsRegClassID,
IntRegsVTs, IntRegsSubclasses, IntRegsSuperclasses, IntRegsSubRegClasses,
IntRegsSuperRegClasses, 4, 4, 1, IntRegs, IntRegs + 32) {}
}
寄存器分配器将避免使用保留寄存器,并且在所有易失性寄存器都已使用之前,不会使用被调用者保存的寄存器。 这通常就足够了,但在某些情况下,可能有必要提供自定义分配顺序。
实现 TargetRegisterInfo
的子类¶
最后一步是手动编码 XXXRegisterInfo
的部分,它实现了 TargetRegisterInfo.h
中描述的接口(请参阅 TargetRegisterInfo 类)。 这些函数返回 0
、NULL
或 false
,除非被覆盖。 以下是在 SparcRegisterInfo.cpp
中为 SPARC 实现覆盖的函数列表
getCalleeSavedRegs
— 返回被调用者保存寄存器的列表,顺序为所需的被调用者保存堆栈帧偏移量。getReservedRegs
— 返回一个按物理寄存器编号索引的位集,指示特定寄存器是否不可用。hasFP
— 返回一个布尔值,指示函数是否应具有专用帧指针寄存器。eliminateCallFramePseudoInstr
— 如果使用了调用帧设置或销毁伪指令,则可以调用此函数来消除它们。eliminateFrameIndex
— 从可能使用抽象帧索引的指令中消除抽象帧索引。emitPrologue
— 将序言代码插入到函数中。emitEpilogue
— 将结语代码插入到函数中。
指令集¶
在代码生成早期阶段,LLVM IR 代码会被转换为 SelectionDAG
,其中节点是 SDNode
类的实例,包含目标指令。SDNode
拥有操作码、操作数、类型要求和操作属性。例如,操作是否可交换,操作是否从内存加载。各种操作节点类型在 include/llvm/CodeGen/SelectionDAGNodes.h
文件中描述(ISD
命名空间中 NodeType
枚举的值)。
TableGen 使用以下目标描述 (.td
) 输入文件来生成大部分指令定义代码
Target.td
— 定义了Instruction
,Operand
,InstrInfo
和其他基本类。TargetSelectionDAG.td
— 被SelectionDAG
指令选择生成器使用,包含SDTC*
类(选择 DAG 类型约束),SelectionDAG
节点的定义(例如imm
,cond
,bb
,add
,fadd
,sub
),以及模式支持 (Pattern
,Pat
,PatFrag
,PatLeaf
,ComplexPattern
)。XXXInstrFormats.td
— 用于定义特定于目标的指令的模式。XXXInstrInfo.td
— 特定于目标的指令模板、条件码和指令集中的指令的定义。对于架构修改,可以使用不同的文件名。例如,对于带有 SSE 指令的 Pentium,此文件是X86InstrSSE.td
,对于带有 MMX 的 Pentium,此文件是X86InstrMMX.td
。
还有一个特定于目标的 XXX.td
文件,其中 XXX
是目标的名称。XXX.td
文件包含其他 .td
输入文件,但其内容仅对子目标直接重要。
您应该描述一个具体的特定于目标的类 XXXInstrInfo
,它表示目标机器支持的机器指令。XXXInstrInfo
包含一个 XXXInstrDescriptor
对象数组,每个对象描述一个指令。指令描述符定义了
操作码助记符
操作数数量
隐式寄存器定义和使用列表
目标无关属性(例如内存访问、是否可交换)
目标特定标志
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 架构手册)。
架构手册中的单个指令通常被建模为多个目标指令,具体取决于其操作数。例如,手册可能描述一个 add 指令,该指令接受寄存器或立即操作数。LLVM 目标可以使用两个名为 ADDri
和 ADDrr
的指令对此进行建模。
您应该为每个指令类别定义一个类,并将每个操作码定义为该类别的子类,并带有适当的参数,例如操作码和扩展操作码的固定二进制编码。您应该将寄存器位映射到指令中编码它们的位(对于 JIT)。您还应该指定在使用自动汇编打印机时应如何打印指令。
正如 SPARC 架构手册第 8 版中所述,指令主要有三种 32 位格式。格式 1 仅用于 CALL
指令。格式 2 用于条件码分支和 SETHI
(设置寄存器高位)指令。格式 3 用于其他指令。
这些格式中的每一种都在 SparcInstrFormat.td
中都有相应的类。InstSP
是其他指令类的基类。为更精确的格式指定了额外的基类:例如在 SparcInstrFormat.td
中,F2_1
用于 SETHI
,F2_2
用于分支。还有其他三个基类:F3_1
用于寄存器/寄存器操作,F3_2
用于寄存器/立即操作,F3_3
用于浮点操作。SparcInstrInfo.td
还为合成 SPARC 指令添加了基类 Pseudo
。
SparcInstrInfo.td
主要包含 SPARC 目标的操作数和指令定义。在 SparcInstrInfo.td
中,以下目标描述文件条目 LDrr
定义了从内存地址到寄存器的字(LD
SPARC 操作码)加载整数指令。第一个参数,值 3 (11
2),是此类操作的操作值。第二个参数 (000000
2) 是 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
指令用于 XOR
和 ADD
指令时(如下所示),它会创建四个指令对象:XORrr
, XORri
, ADDrr
, 和 ADDri
。
defm XOR : F3_12<"xor", 0b000011, xor>;
defm ADD : F3_12<"add", 0b000000, add>;
SparcInstrInfo.td
还包括由分支指令引用的条件码的定义。SparcInstrInfo.td
中的以下定义指示 SPARC 条件码的位位置。例如,第 10 位表示整数的“大于”条件,第 22 位表示浮点数的“大于”条件。
def ICC_NE : ICC_VAL< 9>; // Not Equal
def ICC_E : ICC_VAL< 1>; // Equal
def ICC_G : ICC_VAL<10>; // Greater
...
def FCC_U : FCC_VAL<23>; // Unordered
def FCC_G : FCC_VAL<22>; // Greater
def FCC_UG : FCC_VAL<21>; // Unordered or Greater
...
(请注意,Sparc.h
也定义了与相同 SPARC 条件码对应的枚举。必须注意确保 Sparc.h
中的值与 SparcInstrInfo.td
中的值相对应。即,SPCC::ICC_NE = 9
, SPCC::FCC_U = 23
等。)
指令操作数映射¶
代码生成器后端将指令操作数映射到指令中的字段。每当指令编码 Inst
中的位被分配给没有具体值的字段时,都期望 outs
或 ins
列表中的操作数具有匹配的名称。然后,此操作数填充该未定义的字段。例如,Sparc 目标将 XNORrr
指令定义为具有三个操作数的 F3_1
格式指令:输出 $rd
和输入 $rs1
和 $rs2
。
def XNORrr : F3_1<2, 0b000111,
(outs IntRegs:$rd), (ins IntRegs:$rs1, IntRegs:$rs2),
"xnor $rs1, $rs2, $rd",
[(set i32:$rd, (not (xor i32:$rs1, i32:$rs2)))]>;
SparcInstrFormats.td
中的指令模板显示 F3_1
的基类是 InstSP
。
class InstSP<dag outs, dag ins, string asmstr, list<dag> pattern> : Instruction {
field bits<32> Inst;
let Namespace = "SP";
bits<2> op;
let Inst{31-30} = op;
dag OutOperandList = outs;
dag InOperandList = ins;
let AsmString = asmstr;
let Pattern = pattern;
}
InstSP
定义了 op
字段,并使用它来定义指令的位 30 和 31,但没有为其赋值。
class F3<dag outs, dag ins, string asmstr, list<dag> pattern>
: InstSP<outs, ins, asmstr, pattern> {
bits<5> rd;
bits<6> op3;
bits<5> rs1;
let op{1} = 1; // Op = 2 or 3
let Inst{29-25} = rd;
let Inst{24-19} = op3;
let Inst{18-14} = rs1;
}
F3
定义了 rd
, op3
, 和 rs1
字段,并在指令中使用它们,但同样没有赋值。
class F3_1<bits<2> opVal, bits<6> op3val, dag outs, dag ins,
string asmstr, list<dag> pattern> : F3<outs, ins, asmstr, pattern> {
bits<8> asi = 0; // asi not currently used
bits<5> rs2;
let op = opVal;
let op3 = op3val;
let Inst{13} = 0; // i field = 0
let Inst{12-5} = asi; // address space identifier
let Inst{4-0} = rs2;
}
F3_1
为 op
和 op3
字段赋值,并定义了 rs2
字段。因此,F3_1
格式的指令将需要为 rd
, rs1
, 和 rs2
进行定义,以便完全指定指令编码。
然后,XNORrr
指令在其 OutOperandList 和 InOperandList 中提供了这三个操作数,它们绑定到相应的字段,从而完成指令编码。
对于某些指令,单个操作数可能包含子操作数。如前所示,指令 LDrr
使用 MEMrr
类型的输入操作数。此操作数类型包含两个寄存器子操作数,由 MIOperandInfo
值定义为 (ops IntRegs, IntRegs)
。
def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMrr $rs1, $rs2):$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRrr:$addr))]>;
由于此指令也是 F3_1
格式,因此它将期望操作数名为 rd
, rs1
, 和 rs2
。为了允许这样做,复杂的操作数可以选择为每个子操作数命名。在此示例中,MEMrr
的第一个子操作数命名为 $rs1
,第二个子操作数命名为 $rs2
,并且整个操作数也被命名为 $addr
。
当特定指令不使用指令格式定义的所有操作数时,可以将常量值绑定到一个或所有操作数。例如,RDASR
指令仅接受单个寄存器操作数,因此我们将常量零分配给 rs2
let rs2 = 0 in
def RDASR : F3_1<2, 0b101000,
(outs IntRegs:$rd), (ins ASRRegs:$rs1),
"rd $rs1, $rd", []>;
指令操作数名称映射¶
TableGen 还会生成一个名为 getNamedOperandIdx() 的函数,该函数可用于根据操作数的 TableGen 名称在 MachineInstr 中查找操作数的索引。在指令的 TableGen 定义中设置 UseNamedOperandTable 位会将指令的所有操作数添加到枚举 llvm::XXX:OpName 中,并在 OperandMap 表中为其添加一个条目,可以使用 getNamedOperandIdx() 查询该条目
int DstIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::dst); // => 0
int BIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::b); // => 1
int CIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::c); // => 2
int DIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::d); // => -1
...
OpName 枚举中的条目直接取自 TableGen 定义,因此具有小写名称的操作数在枚举中将具有小写条目。
要在后端包含 getNamedOperandIdx() 函数,您需要在 XXXInstrInfo.cpp 和 XXXInstrInfo.h 中定义一些预处理器宏。例如
XXXInstrInfo.cpp
// For getNamedOperandIdx() function definition.
#define GET_INSTRINFO_NAMED_OPS
#include "XXXGenInstrInfo.inc"
XXXInstrInfo.h
// For OpName enum and getNamedOperandIdx declaration.
#define GET_INSTRINFO_OPERAND_ENUM
#include "XXXGenInstrInfo.inc"
指令操作数类型¶
TableGen 还会生成一个枚举,其中包含后端中定义的所有命名操作数类型,位于 llvm::XXX::OpTypes 命名空间中。一些常见的立即操作数类型(例如 i8、i32、i64、f32、f64)在 include/llvm/Target/Target.td
中为所有目标定义,并且在每个目标的 OpTypes 枚举中可用。此外,只有命名的操作数类型才会出现在枚举中:匿名类型将被忽略。例如,X86 后端定义了 brtarget
和 brtarget8
,它们都是 TableGen Operand
类的实例,表示分支目标操作数
def brtarget : Operand<OtherVT>;
def brtarget8 : Operand<OtherVT>;
这导致
namespace X86 {
namespace OpTypes {
enum OperandType {
...
brtarget,
brtarget8,
...
i32imm,
i64imm,
...
OPERAND_TYPE_LIST_END
} // End namespace OpTypes
} // End namespace X86
在典型的 TableGen 风格中,要使用枚举,您需要定义一个预处理器宏
#define GET_INSTRINFO_OPERAND_TYPES_ENUM // For OpTypes enum
#include "XXXGenInstrInfo.inc"
指令调度¶
可以使用 MCDesc::getSchedClass() 查询指令行程。该值可以通过 TableGen 在 XXXGenInstrInfo.inc 中生成的 llvm::XXX::Sched 命名空间中的枚举来命名。调度类的名称与 XXXSchedule.td 中提供的名称相同,外加一个默认的 NoItinerary 类。
调度模型由 TableGen 通过 SubtargetEmitter 使用 CodeGenSchedModels
类生成。这与指定机器资源使用的行程方法不同。工具 utils/schedcover.py
可用于确定哪些指令已被调度模型描述覆盖,哪些指令未被覆盖。第一步是使用以下指令创建输出文件。然后在输出文件上运行 schedcover.py
$ <src>/utils/schedcover.py <build>/lib/Target/AArch64/tblGenSubtarget.with
instruction, default, CortexA53Model, CortexA57Model, CycloneModel, ExynosM3Model, FalkorModel, KryoModel, ThunderX2T99Model, ThunderXT8XModel
ABSv16i8, WriteV, , , CyWriteV3, M3WriteNMISC1, FalkorWr_2VXVY_2cyc, KryoWrite_2cyc_XY_XY_150ln, ,
ABSv1i64, WriteV, , , CyWriteV3, M3WriteNMISC1, FalkorWr_1VXVY_2cyc, KryoWrite_2cyc_XY_noRSV_67ln, ,
...
要捕获生成调度模型的调试输出,请切换到相应的目标目录并使用以下命令:带有 subtarget-emitter
调试选项的命令
$ <build>/bin/llvm-tblgen -debug-only=subtarget-emitter -gen-subtarget \
-I <src>/lib/Target/<target> -I <src>/include \
-I <src>/lib/Target <src>/lib/Target/<target>/<target>.td \
-o <build>/lib/Target/<target>/<target>GenSubtargetInfo.inc.tmp \
> tblGenSubtarget.dbg 2>&1
其中 <build>
是构建目录,src
是源目录,<target>
是目标的名称。要仔细检查上述命令是否是需要的命令,可以使用以下命令从构建中捕获确切的 TableGen 命令
$ VERBOSE=1 make ...
并在输出中搜索 llvm-tblgen
命令。
指令关系映射¶
此 TableGen 功能用于将指令相互关联。当您有多个指令格式并且需要在指令选择后在它们之间切换时,此功能特别有用。此功能的全部驱动力是关系模型,关系模型可以在 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 转换。BranchFolder
和 IfConverter
机器函数 passes(参见 lib/CodeGen
目录中的源文件 BranchFolding.cpp
和 IfConversion.cpp
)调用 analyzeBranch
以改进表示指令的控制流图。
可以检查 analyzeBranch
的几个实现(对于 ARM、Alpha 和 X86)作为您自己的 analyzeBranch
实现的模型。由于 SPARC 没有实现有用的 analyzeBranch
,因此下面显示了 ARM 目标实现。
analyzeBranch
返回一个布尔值并接受四个参数
MachineBasicBlock &MBB
— 要检查的传入块。MachineBasicBlock *&TBB
— 返回的目标块。对于评估为 true 的条件分支,TBB
是目标。MachineBasicBlock *&FBB
— 对于评估为 false 的条件分支,FBB
作为目标返回。std::vector<MachineOperand> &Cond
— 用于评估条件分支条件的的操作数列表。
在最简单的情况下,如果一个块在没有分支的情况下结束,则它会落入后继块。没有为 TBB
或 FBB
指定目标块,因此两个参数都返回 NULL
。analyzeBranch
的开头(请参阅下面 ARM 目标的代码)显示了函数参数和最简单情况的代码。
bool ARMInstrInfo::analyzeBranch(MachineBasicBlock &MBB,
MachineBasicBlock *&TBB,
MachineBasicBlock *&FBB,
std::vector<MachineOperand> &Cond) const
{
MachineBasicBlock::iterator I = MBB.end();
if (I == MBB.begin() || !isUnpredicatedTerminator(--I))
return false;
如果一个块以单个无条件分支指令结束,则 analyzeBranch
(如下所示)应在 TBB
参数中返回该分支的目标。
if (LastOpc == ARM::B || LastOpc == ARM::tB) {
TBB = LastInst->getOperand(0).getMBB();
return false;
}
如果一个块以两个无条件分支结束,则永远不会到达第二个分支。在这种情况下,如下所示,删除最后一个分支指令并在 TBB
参数中返回倒数第二个分支。
if ((SecondLastOpc == ARM::B || SecondLastOpc == ARM::tB) &&
(LastOpc == ARM::B || LastOpc == ARM::tB)) {
TBB = SecondLastInst->getOperand(0).getMBB();
I = LastInst;
I->eraseFromParent();
return false;
}
一个块可能以单个条件分支指令结束,如果条件评估为 false,则该指令会落入后继块。在这种情况下,analyzeBranch
(如下所示)应在 TBB
参数中返回该条件分支的目标,并在 Cond
参数中返回操作数列表以评估条件。
if (LastOpc == ARM::Bcc || LastOpc == ARM::tBcc) {
// Block ends with fall-through condbranch.
TBB = LastInst->getOperand(0).getMBB();
Cond.push_back(LastInst->getOperand(1));
Cond.push_back(LastInst->getOperand(2));
return false;
}
如果一个块以条件分支和随后的无条件分支结束,则 analyzeBranch
(如下所示)应在 TBB
参数中返回条件分支目标(假设它对应于“true
”的条件评估),并在 FBB
中返回无条件分支目标(对应于“false
”的条件评估)。应在 Cond
参数中返回用于评估条件的操作数列表。
unsigned SecondLastOpc = SecondLastInst->getOpcode();
if ((SecondLastOpc == ARM::Bcc && LastOpc == ARM::B) ||
(SecondLastOpc == ARM::tBcc && LastOpc == ARM::tB)) {
TBB = SecondLastInst->getOperand(0).getMBB();
Cond.push_back(SecondLastInst->getOperand(1));
Cond.push_back(SecondLastInst->getOperand(2));
FBB = LastInst->getOperand(0).getMBB();
return false;
}
对于最后两种情况(以单个条件分支结束或以一个条件分支和一个无条件分支结束),在 Cond
参数中返回的操作数可以传递给其他指令的方法,以创建新分支或执行其他操作。analyzeBranch
的实现需要辅助方法 removeBranch
和 insertBranch
来管理后续操作。
analyzeBranch
应该返回 false,表示在大多数情况下成功。analyzeBranch
仅应在方法无法确定该做什么时返回 true,例如,如果一个块有三个终止分支。analyzeBranch
如果遇到它无法处理的终止符(例如间接分支),则可能会返回 true。
指令选择器¶
LLVM 使用 SelectionDAG
来表示 LLVM IR 指令,而 SelectionDAG
的节点理想情况下表示本机目标指令。在代码生成期间,执行指令选择 pass 以将非本机 DAG 指令转换为本机目标特定指令。在 XXXISelDAGToDAG.cpp
中描述的 pass 用于匹配模式并执行 DAG 到 DAG 的指令选择。可选地,可以定义一个 pass(在 XXXBranchSelector.cpp
中)以对分支指令执行类似的 DAG 到 DAG 操作。稍后,XXXISelLowering.cpp
中的代码替换或删除 SelectionDAG
中本机不支持的操作和数据类型(合法化)。
TableGen 使用以下目标描述输入文件生成指令选择的代码
XXXInstrInfo.td
— 包含特定于目标的指令集中的指令定义,生成XXXGenDAGISel.inc
,该文件包含在XXXISelDAGToDAG.cpp
中。XXXCallingConv.td
— 包含目标架构的调用和返回值约定,并生成XXXGenCallingConv.inc
,该文件包含在XXXISelLowering.cpp
中。
指令选择 pass 的实现必须包含一个标头,该标头声明 FunctionPass
类或 FunctionPass
的子类。在 XXXTargetMachine.cpp
中,Pass Manager (PM) 应该将每个指令选择 pass 添加到要运行的 pass 队列中。
LLVM 静态编译器 (llc
) 是可视化 DAG 内容的绝佳工具。要显示特定处理阶段之前或之后的 SelectionDAG
,请使用 SelectionDAG 指令选择过程 中描述的 llc
的命令行选项。
要描述指令选择器的行为,您应该在 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
,SelectADDRrr
是在 Instructor Selector 的实现(例如 SparcISelDAGToDAG.cpp
)中定义的函数。
在 lib/Target/TargetSelectionDAG.td
中,存储的 DAG 运算符定义如下
def store : PatFrag<(ops node:$val, node:$ptr),
(unindexedstore node:$val, node:$ptr)> {
let IsStore = true;
let IsTruncStore = false;
}
XXXInstrInfo.td
还在 XXXGenDAGISel.inc
中生成了 SelectCode
方法,该方法用于为指令调用适当的处理方法。在此示例中,SelectCode
为 ISD::STORE
操作码调用 Select_ISD_STORE
。
SDNode *SelectCode(SDValue N) {
...
MVT::ValueType NVT = N.getNode()->getValueType(0);
switch (N.getOpcode()) {
case ISD::STORE: {
switch (NVT) {
default:
return Select_ISD_STORE(N);
break;
}
break;
}
...
STrr
的模式已匹配,因此在 XXXGenDAGISel.inc
中的其他地方,为 Select_ISD_STORE
创建了 STrr
的代码。Emit_22
方法也在 XXXGenDAGISel.inc
中生成,以完成此指令的处理。
SDNode *Select_ISD_STORE(const SDValue &N) {
SDValue Chain = N.getOperand(0);
if (Predicate_store(N.getNode())) {
SDValue N1 = N.getOperand(1);
SDValue N2 = N.getOperand(2);
SDValue CPTmp0;
SDValue CPTmp1;
// Pattern: (st:void i32:i32:$src,
// ADDRrr:i32:$addr)<<P:Predicate_store>>
// Emits: (STrr:void ADDRrr:i32:$addr, IntRegs:i32:$src)
// Pattern complexity = 13 cost = 1 size = 0
if (SelectADDRrr(N, N2, CPTmp0, CPTmp1) &&
N1.getNode()->getValueType(0) == MVT::i32 &&
N2.getNode()->getValueType(0) == MVT::i32) {
return Emit_22(N, SP::STrr, CPTmp0, CPTmp1);
}
...
SelectionDAG 合法化阶段¶
合法化阶段将 DAG 转换为使用目标本机支持的类型和操作。对于本机不支持的类型和操作,您需要在特定于目标的 XXXTargetLowering
实现中添加代码,以将不支持的类型和操作转换为受支持的类型和操作。
在 XXXTargetLowering
类的构造函数中,首先使用 addRegisterClass
方法来指定支持哪些类型以及哪些寄存器类与它们关联。寄存器类的代码由 TableGen 从 XXXRegisterInfo.td
生成,并放置在 XXXGenRegisterInfo.h.inc
中。例如,SparcTargetLowering 类(在 SparcISelLowering.cpp
中)的构造函数的实现以以下代码开始
addRegisterClass(MVT::i32, SP::IntRegsRegisterClass);
addRegisterClass(MVT::f32, SP::FPRegsRegisterClass);
addRegisterClass(MVT::f64, SP::DFPRegsRegisterClass);
您应该检查 ISD
命名空间(include/llvm/CodeGen/SelectionDAGNodes.h
)中的节点类型,并确定目标体系结构原生支持哪些操作。对于不原生支持的操作,请为 XXXTargetLowering
类构造函数添加回调,以便指令选择过程知道该怎么做。TargetLowering
类回调方法(在 llvm/Target/TargetLowering.h
中声明)包括
setOperationAction
— 通用操作。setLoadExtAction
— 带扩展的加载。setTruncStoreAction
— 截断存储。setIndexedLoadAction
— 索引加载。setIndexedStoreAction
— 索引存储。setConvertAction
— 类型转换。setCondCodeAction
— 对给定条件码的支持。
注意:在较旧的版本中,使用 setLoadXAction
代替 setLoadExtAction
。此外,在较旧的版本中,可能不支持 setCondCodeAction
。请检查您的版本以查看具体支持哪些方法。
这些回调用于确定操作是否适用于指定的类型(或类型)。在所有情况下,第三个参数都是 LegalAction
类型枚举值:Promote
、Expand
、Custom
或 Legal
。SparcISelLowering.cpp
包含所有四个 LegalAction
值的示例。
Promote¶
对于给定类型缺少原生支持的操作,可以将指定的类型提升为更大的支持类型。例如,SPARC 不支持布尔值(i1
类型)的符号扩展加载,因此在 SparcISelLowering.cpp
中,下面的第三个参数 Promote
会在加载之前将 i1
类型的值更改为更大的类型。
setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
Expand¶
对于缺少原生支持的类型,可能需要进一步分解值,而不是提升。对于缺少原生支持的操作,可以使用其他操作的组合来达到类似的效果。在 SPARC 中,浮点正弦和余弦三角运算通过扩展到其他操作来支持,如 setOperationAction
的第三个参数 Expand
所示
setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);
Custom¶
对于某些操作,简单的类型提升或操作扩展可能不足以满足需求。在某些情况下,必须实现特殊的内在函数。
例如,常量值可能需要特殊处理,或者操作可能需要在堆栈中溢出和恢复寄存器,并与寄存器分配器一起工作。
如以下 SparcISelLowering.cpp
代码所示,要执行从浮点值到有符号整数的类型转换,首先应使用 Custom
作为第三个参数调用 setOperationAction
setOperationAction(ISD::FP_TO_SINT, MVT::i32, Custom);
在 LowerOperation
方法中,对于每个 Custom
操作,应添加一个 case 语句来指示要调用的函数。在以下代码中,FP_TO_SINT
操作码将调用 LowerFP_TO_SINT
方法
SDValue SparcTargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG) {
switch (Op.getOpcode()) {
case ISD::FP_TO_SINT: return LowerFP_TO_SINT(Op, DAG);
...
}
}
最后,实现了 LowerFP_TO_SINT
方法,使用 FP 寄存器将浮点值转换为整数。
static SDValue LowerFP_TO_SINT(SDValue Op, SelectionDAG &DAG) {
assert(Op.getValueType() == MVT::i32);
Op = DAG.getNode(SPISD::FTOI, MVT::f32, Op.getOperand(0));
return DAG.getNode(ISD::BITCAST, MVT::i32, Op);
}
Legal¶
Legal
LegalizeAction
枚举值仅表示操作是原生支持的。Legal
表示默认条件,因此很少使用。在 SparcISelLowering.cpp
中,CTPOP
(用于计算整数中设置的位数的运算)的操作仅在 SPARC v9 中原生支持。以下代码为非 v9 SPARC 实现启用 Expand
转换技术。
setOperationAction(ISD::CTPOP, MVT::i32, Expand);
...
if (TM.getSubtarget<SparcSubtarget>().isV9())
setOperationAction(ISD::CTPOP, MVT::i32, Legal);
调用约定¶
为了支持特定目标的调用约定,XXXGenCallingConv.td
使用在 lib/Target/TargetCallingConv.td
中定义的接口(例如 CCIfType
和 CCAssignToReg
)。TableGen 可以获取目标描述文件 XXXGenCallingConv.td
并生成头文件 XXXGenCallingConv.inc
,该文件通常包含在 XXXISelLowering.cpp
中。您可以使用 TargetCallingConv.td
中的接口来指定
参数分配的顺序。
参数和返回值的位置(即,在堆栈上还是在寄存器中)。
可以使用哪些寄存器。
调用者还是被调用者展开堆栈。
以下示例演示了 CCIfType
和 CCAssignToReg
接口的使用。如果 CCIfType
谓词为真(即,如果当前参数的类型为 f32
或 f64
),则执行该操作。在这种情况下,CCAssignToReg
操作将参数值分配给第一个可用的寄存器:R0
或 R1
。
CCIfType<[f32,f64], CCAssignToReg<[R0, R1]>>
SparcCallingConv.td
包含特定于目标的返回值调用约定 (RetCC_Sparc32
) 和基本的 32 位 C 调用约定 (CC_Sparc32
) 的定义。RetCC_Sparc32
的定义(如下所示)指示哪些寄存器用于指定的标量返回类型。单精度浮点数返回到寄存器 F0
,双精度浮点数返回到寄存器 D0
。32 位整数在寄存器 I0
或 I1
中返回。
def RetCC_Sparc32 : CallingConv<[
CCIfType<[i32], CCAssignToReg<[I0, I1]>>,
CCIfType<[f32], CCAssignToReg<[F0]>>,
CCIfType<[f64], CCAssignToReg<[D0]>>
]>;
SparcCallingConv.td
中 CC_Sparc32
的定义引入了 CCAssignToStack
,它将值分配给具有指定大小和对齐方式的堆栈槽。在下面的示例中,第一个参数 4 指示槽的大小,第二个参数也为 4,指示沿 4 字节单元的堆栈对齐方式。(特殊情况:如果大小为零,则使用 ABI 大小;如果对齐方式为零,则使用 ABI 对齐方式。)
def CC_Sparc32 : CallingConv<[
// All arguments get passed in integer registers if there is space.
CCIfType<[i32, f32, f64], CCAssignToReg<[I0, I1, I2, I3, I4, I5]>>,
CCAssignToStack<4, 4>
]>;
CCDelegateTo
是另一个常用的接口,它尝试查找指定的子调用约定,如果找到匹配项,则调用它。在以下示例(在 X86CallingConv.td
中)中,RetCC_X86_32_C
的定义以 CCDelegateTo
结尾。在将当前值分配给寄存器 ST0
或 ST1
后,将调用 RetCC_X86Common
。
def RetCC_X86_32_C : CallingConv<[
CCIfType<[f32], CCAssignToReg<[ST0, ST1]>>,
CCIfType<[f64], CCAssignToReg<[ST0, ST1]>>,
CCDelegateTo<RetCC_X86Common>
]>;
CCIfCC
是一个接口,它尝试将给定的名称与当前的调用约定进行匹配。如果该名称标识了当前的调用约定,则会调用指定的操作。在以下示例(在 X86CallingConv.td
中)中,如果正在使用 Fast
调用约定,则调用 RetCC_X86_32_Fast
。如果正在使用 SSECall
调用约定,则调用 RetCC_X86_32_SSE
。
def RetCC_X86_32 : CallingConv<[
CCIfCC<"CallingConv::Fast", CCDelegateTo<RetCC_X86_32_Fast>>,
CCIfCC<"CallingConv::X86_SSECall", CCDelegateTo<RetCC_X86_32_SSE>>,
CCDelegateTo<RetCC_X86_32_C>
]>;
CCAssignToRegAndStack
与 CCAssignToReg
相同,但当使用某些寄存器时,也会分配一个堆栈槽。基本上,它的工作方式类似于:CCIf<CCAssignToReg<regList>, CCAssignToStack<size, align>>
。
class CCAssignToRegAndStack<list<Register> regList, int size, int align>
: CCAssignToReg<regList> {
int Size = size;
int Align = align;
}
其他调用约定接口包括
CCIf <predicate, action>
— 如果谓词匹配,则应用该操作。CCIfInReg <action>
— 如果参数标记有 “inreg
” 属性,则应用该操作。CCIfNest <action>
— 如果参数标记有 “nest
” 属性,则应用该操作。CCIfNotVarArg <action>
— 如果当前函数不接受可变数量的参数,则应用该操作。CCAssignToRegWithShadow <registerList, shadowList>
— 类似于CCAssignToReg
,但具有影子寄存器列表。CCPassByVal <size, align>
— 将值分配给具有指定最小大小和对齐方式的堆栈槽。CCPromoteToType <type>
— 将当前值提升为指定的类型。CallingConv <[actions]>
— 定义每个支持的调用约定。
汇编打印器¶
在代码生成阶段,代码生成器可以使用 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
类。该实现必须包含以下头文件,这些头文件声明了 AsmPrinter
和 MachineFunctionPass
类。MachineFunctionPass
是 FunctionPass
的子类。
#include "llvm/CodeGen/AsmPrinter.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
作为 FunctionPass
,AsmPrinter
首先调用 doInitialization
来设置 AsmPrinter
。在 SparcAsmPrinter
中,实例化了一个 Mangler
对象来处理变量名。
在 XXXAsmPrinter.cpp
中,必须为 XXXAsmPrinter
实现 runOnMachineFunction
方法(在 MachineFunctionPass
中声明)。在 MachineFunctionPass
中,runOnFunction
方法调用 runOnMachineFunction
。runOnMachineFunction
的特定于目标的实现有所不同,但通常执行以下操作来处理每个机器函数
调用
SetupMachineFunction
以执行初始化。调用
EmitConstantPool
以打印(到输出流)已溢出到内存的常量。调用
EmitJumpTableInfo
以打印当前函数使用的跳转表。打印当前函数的标签。
打印函数的代码,包括基本块标签和指令的汇编代码(使用
printInstruction
)
XXXAsmPrinter
实现还必须包含由 TableGen 生成的代码,这些代码在 XXXGenAsmWriter.inc
文件中输出。XXXGenAsmWriter.inc
中的代码包含 printInstruction
方法的实现,该方法可以调用以下方法
printOperand
printMemOperand
printCCOperand
(用于条件语句)printDataDirective
printDeclare
printImplicitDef
printInlineAsm
AsmPrinter.cpp
中的 printDeclare
、printImplicitDef
、printInlineAsm
和 printLabel
的实现通常足以打印汇编代码,不需要被覆盖。
printOperand
方法使用长 switch
/case
语句为操作数类型实现:寄存器、立即数、基本块、外部符号、全局地址、常量池索引或跳转表索引。对于具有内存地址操作数的指令,应实现 printMemOperand
方法以生成正确的输出。类似地,应使用 printCCOperand
来打印条件操作数。
应该在 XXXAsmPrinter
中覆盖 doFinalization
,并且应该调用它来关闭汇编打印器。在 doFinalization
期间,全局变量和常量被打印到输出。
子目标支持¶
子目标支持用于告知代码生成过程给定芯片组的指令集变体。例如,提供的 LLVM SPARC 实现涵盖了 SPARC 微处理器架构的三个主要版本:Version 8 (V8,这是一个 32 位架构)、Version 9 (V9,一个 64 位架构) 和 UltraSPARC 架构。V8 有 16 个双精度浮点寄存器,也可以用作 32 个单精度或 8 个四精度寄存器。V8 也是纯大端字节序。V9 有 32 个双精度浮点寄存器,也可以用作 16 个四精度寄存器,但不能用作单精度寄存器。UltraSPARC 架构将 V9 与 UltraSPARC 可视指令集扩展相结合。
如果需要子目标支持,您应该为您的体系结构实现一个特定于目标的 XXXSubtarget
类。此类应处理命令行选项 -mcpu=
和 -mattr=
。
TableGen 使用 Target.td
和 Sparc.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.td
和 Sparc.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
文件,其中包含一个机器函数 pass,该 pass 将目标机器指令转换为可重定位的机器代码。编写一个
XXXJITInfo.cpp
文件,该文件实现 JIT 接口,用于特定于目标的代码生成活动,例如发出机器代码和桩。修改
XXXTargetMachine
,使其通过其getJITInfo
方法提供TargetJITInfo
对象。
编写 JIT 支持代码有几种不同的方法。例如,TableGen 和目标描述文件可以用于创建 JIT 代码生成器,但不是强制性的。对于 Alpha 和 PowerPC 目标机器,TableGen 用于生成 XXXGenCodeEmitter.inc
,其中包含机器指令的二进制编码和用于访问这些代码的 getBinaryCodeForInstr
方法。其他 JIT 实现则不使用。
XXXJITInfo.cpp
和 XXXCodeEmitter.cpp
都必须包含 llvm/CodeGen/MachineCodeEmitter.h
头文件,该文件定义了 MachineCodeEmitter
类,其中包含用于将数据(以字节、字、字符串等形式)写入输出流的几个回调函数的代码。
机器代码发射器¶
在 XXXCodeEmitter.cpp
中,Emitter
类的特定于目标的实现被实现为函数 pass(MachineFunctionPass
的子类)。runOnMachineFunction
的特定于目标的实现(由 MachineFunctionPass
中的 runOnFunction
调用)迭代 MachineBasicBlock
调用 emitInstruction
以处理每个指令并发出二进制代码。emitInstruction
主要使用 XXXInstrInfo.h
中定义的指令类型的 case 语句来实现。例如,在 X86CodeEmitter.cpp
中,emitInstruction
方法围绕以下 switch
/case
语句构建
switch (Desc->TSFlags & X86::FormMask) {
case X86II::Pseudo: // for not yet implemented instructions
... // or pseudo-instructions
break;
case X86II::RawFrm: // for instructions with a fixed opcode value
...
break;
case X86II::AddRegFrm: // for instructions that have one register operand
... // added to their opcode
break;
case X86II::MRMDestReg:// for instructions that use the Mod/RM byte
... // to specify a destination (register)
break;
case X86II::MRMDestMem:// for instructions that use the Mod/RM byte
... // to specify a destination (memory)
break;
case X86II::MRMSrcReg: // for instructions that use the Mod/RM byte
... // to specify a source (register)
break;
case X86II::MRMSrcMem: // for instructions that use the Mod/RM byte
... // to specify a source (memory)
break;
case X86II::MRM0r: case X86II::MRM1r: // for instructions that operate on
case X86II::MRM2r: case X86II::MRM3r: // a REGISTER r/m operand and
case X86II::MRM4r: case X86II::MRM5r: // use the Mod/RM byte and a field
case X86II::MRM6r: case X86II::MRM7r: // to hold extended opcode data
...
break;
case X86II::MRM0m: case X86II::MRM1m: // for instructions that operate on
case X86II::MRM2m: case X86II::MRM3m: // a MEMORY r/m operand and
case X86II::MRM4m: case X86II::MRM5m: // use the Mod/RM byte and a field
case X86II::MRM6m: case X86II::MRM7m: // to hold extended opcode data
...
break;
case X86II::MRMInitReg: // for instructions whose source and
... // destination are the same register
break;
}
这些 case 语句的实现通常首先发出操作码,然后获取操作数。然后,根据操作数,可以调用辅助方法来处理操作数。例如,在 X86CodeEmitter.cpp
中,对于 X86II::AddRegFrm
case,第一个发出的数据(通过 emitByte
)是添加到寄存器操作数的操作码。然后,提取表示机器操作数的对象 MO1
。诸如 isImmediate
、isGlobalAddress
、isExternalSymbol
、isConstantPoolIndex
和 isJumpTableIndex
等辅助方法确定操作数类型。(X86CodeEmitter.cpp
还有诸如 emitConstant
、emitGlobalAddress
、emitExternalSymbolAddress
、emitConstPoolAddress
和 emitJumpTableAddress
等私有方法,这些方法将数据发出到输出流中。)
case X86II::AddRegFrm:
MCE.emitByte(BaseOpcode + getX86RegNum(MI.getOperand(CurOp++).getReg()));
if (CurOp != NumOps) {
const MachineOperand &MO1 = MI.getOperand(CurOp++);
unsigned Size = X86InstrInfo::sizeOfImm(Desc);
if (MO1.isImmediate())
emitConstant(MO1.getImm(), Size);
else {
unsigned rt = Is64BitMode ? X86::reloc_pcrel_word
: (IsPIC ? X86::reloc_picrel_word : X86::reloc_absolute_word);
if (Opcode == X86::MOV64ri)
rt = X86::reloc_absolute_dword; // FIXME: add X86II flag?
if (MO1.isGlobalAddress()) {
bool NeedStub = isa<Function>(MO1.getGlobal());
bool isLazy = gvNeedsLazyPtr(MO1.getGlobal());
emitGlobalAddress(MO1.getGlobal(), rt, MO1.getOffset(), 0,
NeedStub, isLazy);
} else if (MO1.isExternalSymbol())
emitExternalSymbolAddress(MO1.getSymbolName(), rt);
else if (MO1.isConstantPoolIndex())
emitConstPoolAddress(MO1.getIndex(), rt);
else if (MO1.isJumpTableIndex())
emitJumpTableAddress(MO1.getIndex(), rt);
}
}
break;
在前面的示例中,XXXCodeEmitter.cpp
使用变量 rt
,它是 RelocationType
枚举,可用于重定位地址(例如,带有 PIC 基址偏移的全局地址)。该目标的 RelocationType
枚举在简短的特定于目标的 XXXRelocations.h
文件中定义。RelocationType
由 XXXJITInfo.cpp
中定义的 relocate
方法使用,以重写引用的全局符号的地址。
例如,X86Relocations.h
为 X86 地址指定了以下重定位类型。在这四种情况下,重定位的值都会添加到内存中已有的值中。对于 reloc_pcrel_word
和 reloc_picrel_word
,还有一个额外的初始调整。
enum RelocationType {
reloc_pcrel_word = 0, // add reloc value after adjusting for the PC loc
reloc_picrel_word = 1, // add reloc value after adjusting for the PIC base
reloc_absolute_word = 2, // absolute relocation; no additional adjustment
reloc_absolute_dword = 3 // absolute relocation; no additional adjustment
};
目标 JIT 信息¶
XXXJITInfo.cpp
实现 JIT 接口,用于特定于目标的代码生成活动,例如发出机器代码和桩。至少,XXXJITInfo
的特定于目标的版本实现以下内容
getLazyResolverFunction
— 初始化 JIT,为目标提供用于编译的函数。emitFunctionStub
— 返回具有指定回调函数地址的本机函数。relocate
— 根据重定位类型更改引用的全局变量的地址。回调函数,这些函数是函数桩的包装器,当最初不知道真实目标时使用。
getLazyResolverFunction
通常很容易实现。它将传入的参数作为全局 JITCompilerFunction
,并返回将用作函数包装器的回调函数。对于 Alpha 目标(在 AlphaJITInfo.cpp
中),getLazyResolverFunction
实现非常简单
TargetJITInfo::LazyResolverFn AlphaJITInfo::getLazyResolverFunction(
JITCompilerFn F) {
JITCompilerFunction = F;
return AlphaCompilationCallback;
}
对于 X86 目标,getLazyResolverFunction
实现稍微复杂一些,因为它为具有 SSE 指令和 XMM 寄存器的处理器返回不同的回调函数。
回调函数最初保存,然后恢复被调用者寄存器值、传入的参数以及帧和返回地址。回调函数需要低级访问寄存器或堆栈,因此通常使用汇编程序实现。