介绍
pdf清晰版
这个文档描述了编写编译器后端的技术,将llvm IR转化为定制的机器代码或者其他语言。意图生成的特定机器码可以是汇编形式或者二进制形式(能够被JIT编译器使用)。
LLVM的后端有一个目标无关为特征的代码生成器能够创建多种目标CPU类型的输出——包括X86,PowerPC,ARM和SPARC。后端也能够被用来生成特定的单元处理器或者GPU的SPU代码来支持内核计算的执行。
这个文档关注于已经下载了的LLVM正式版的/llvm/lib/Target目录下已有例子。特别地,这个文档关注为SPARC目标创建静态编译器(产生汇编代码的)拥有相当标准的特征,类如RISC指令集与前向调用规范。
读者
这个文档的读者是所有需要写一个LLVM后端来生成特定硬件或者软件目标代码的人。
前导阅读
这些基础文档必须首先阅读:
• LLVM Language Reference Manual —LLVM汇编参考
• The LLVM Target-Independent Code Generator — 翻译LLVM内部表示到目标平台机器码的主件向导。特别注意代码生成阶段的描述:指令选择、调度和构造、基于SSA的优化、寄存器分配、前/后缀代码的插入、后机器码优化与代码发送。
• 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汇编格式(GAS),看Using As ,尤其对于汇编打印器。“Using As”包含了一系列目标机器相关的特性。
基本步骤
一个转换IR为特定目标机器指令或者语言的编译器后端,完成下列步骤:
• 创建TargetMachine类的子类来描述你的目标机器的特性。拷贝特定目标机器类和头文件的已存在例子。比如以SparcTargetMachine.cpp和SparcTargetMachine.h,但是要为你的目标平台修改文件名。相似地,改变引用了Sparc的代码为你目标平台。
• 描述目标平台的寄存器集,用TableGen从一个特定目标平台的RegisterInfo.td输入文件来生成寄存器定义,寄存器别名,寄存器类。你应该为TargetRegisterInfo类写一些额外的代码来表示该类文件数据用来为寄存器分配并且描述它们之间的相互作用关系。
• 描述目标平台的指令集。通过TableGen使用特定目标平台版本的TargetInstrFormat.td与TargetInstrInfo.td来生成特定目标平台的指令代码。你需要为TargetInstrInfo类写一些额外的代码来表示目标机器支持的机器指令。
• 描述从指令的DAG表示到本地目标机器平台的指令的LLVM IR的选择与转化。使用TableGen来生成匹配模式的代码和选择基于目标特定版本的TargetInstrInfo.td中的额外信息的指令。并且在XXXIselLowering.cpp编写代码来替换或者删除在SelectionDAG中不被本地支持操作和数据类型。
• 编写写为目标平台将LLVM IR转为一个GAS格式的汇编打印器。你应该添加汇编字符串到你指定目标平台版本的TargetInstrInfo.td中定义的指令。你需要为AsmPringter的子类写一些进行LLVM到汇编的转化与一个TargetAsmInfo无关紧要的子类。
• 可选地,增加对子平台的支持(比如,不同功能的变量)。你应该编写TargetSubtarget类的子类代码,来允许你可以使用 -mcpu= 和 -mattr=命令行选项。
• 可选地,增加JIT支持与创建机器码发射器用来直接发射二进制代码到内存中。
在.cpp与.h文件中,初始化地设置了这些方法的桩然后实现它们。开始时,你可能不知道哪些是私有成员那类用得到和哪些组件在子类中是需要的。
前导
实际上创建你的编译器后端,你需要创建和修改许多文件。这里讨论最小的情况。但是实际应用LLVM目标无关的代码生成器,你必须按照
首先,你应该 lib/Target下创建一个子目录来存放所有目标相关的文件。如果你的目标平台叫做“Dummy”,创建目录lib/Target/Dummy。
在这个新目录中,创建CMakeList.txt文件。最简单的方式是复制其他目标平台的CMakeLists.txt文件并且修改它。它至少需要包含LLVM_TARGET_DEFINITIONS变量。library命名为LLVMDummy(比如,看MIP平台)。另外,你可以把library分为LLVMDummyCodeGen和LLVMDummyAsmPrinter,后者应该在 lib/Target/Dummy子目录中被实现(例如,看PowerPC平台)。
注意这两种命名风格都硬编码进llvm-config。使用另外的编码风格可能混淆llvm-config和在llc连接过程中产生很多连接器错误。
完成你的目标平台实际上需要做一些事情,你需要实现一个TargetMachine子类。这个实现通常应该放在lib/Target/DummyTargetMachine.cpp文件中,但是所有在lib/Target目录的文件将被built并生效。使用LLVM的目标无关代码生成器,你需要做所有当前机器后端做的:创建LLVMTargetMachine的子类。(若从头创建一个目标,创建TargetMachine的子类)。
获得LLVM实际创建并关联你的目标平台,你需要运行cmake -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy。这样将创建你的目标平台无需把它增加到你所有的目标序列中。一旦你的目标平台稳定,你就能把它增加到LLVM_ALL_TARGETS变量所在的主要的CMakeLists.txt中。
目标机器
LLVMTargetMachine被设计为一个目标平台的基类实现了LLVM目标平台无关的代码生成器。LLVMTargetMachine类应该被具体的目标平台类特化,实现大量的虚函数。
LLVMTargetMachine定义为在文件include/llvm/Target/TargetMachine.h中TargetMachine的子类。TargetMachine类实现也处理很多命令行选项。
创建具体的平台相关的LLVMTargetMachine子类,以复制已经存在的TargetMachine类与头文件开始。你应该把这些文件命名为反应你目标平台的名字。例如,对于SPARC平台,命名为SparcTargetMachine.h和SparcTargetMachine.cpp。
对于目标机器XXX,XXXTargetMachine的实现必须有代表目标平台组件的访问方法。这些方法被命名为getInfo,和试图获得指令集,寄存器集,栈帧布局等类似信息。XXXTargetMachine必须也实现getDataLayout方法来访问一个对象和特定目标数据特征,比如数据类型大小和对齐要求。例如对于SPARC平台,头文件SparcTargetMachine.h声明了很多getInfo的原型和简单地返回一个类成员的getDataLayou方法。
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; }
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) {
}
连字符分割了TargetDescription串的各个部分。
• 字符串中一个大写的“E”表示一个大尾端的目标平台数据模型。小写的“e”表示小尾端。
• “p:”后跟随着指针信息:大小、ABI对齐和偏好对齐。如果只有两个数字在“p”后面,那么第一个值是指针大小,第二个值同时是ABI对齐与偏好对齐。
• 数字类型对齐的字母:“i”、“f”、“v”或者“a”(对应于整型,浮点型,向量型或聚合型)。“i”, “v”, or “a”后跟着ABI对齐与优化对齐。“f”跟随着三个值:第一个值表示long double的大小,然后是ABI对齐,另外是ABI优化对齐。
注册Target
你必须用TargetRegistry注册你的target,这样在运行时其他的LLVM工具能够用它来查找和使用你的target。TargetRegistry能被直接使用,但是对于大多数targets,没有帮助性质模版在对你的工作有助益。
所有的targets必须在注册时声明一个全局的Target对象用于表示target。然后,在target的TargetInfo库,target应该定义哪个对象和使用RegisterTarget模版来注册target。例如,Sparc注册代码像这这样:
Target llvm::getTheSparcTarget();
extern "C" void LLVMInitializeSparcTargetInfo() {
RegisterTarget<Triple::sparc, /*HasJIT=*/false>
X(getTheSparcTarget(), "sparc", "Sparc");
}
这样就允许TargetRegistry查找根据名字或者target triple查找target。此外,多数target会注册额外特征分散在各个libraries中。这些注册步骤都是独立的,因为一些客户端可能希望仅链接target的某些部分——JIT代码生成器不要求使用汇编打印器,例如。这是一个注册Sparc汇编打印器的例子:
extern "C" void LLVMInitializeSparcAsmPrinter() {
RegisterAsmPrinter<SparcAsmPrinter> X(getTheSparcTarget());
}
获取更多的信息,看 “llvm/Target/TargetRegistry.h”
寄存器集与寄存器类
你应该描述一个具体的target-specific类来表示目标机器的寄存器文件。这个类称为XXXResigsterInfo(XXX区别目标平台)和表示这个类及创建文件数据用来寄存器分配。它描述了寄存器之间的相互作用。
你还需要定义寄存器类来分类相关的寄存器。为一组寄存器增加一个寄存器类,所有在该类中的寄存器在某些指令中都被以相同的方式处理。典型的例子是寄存器对于整型、浮点型或者向量型的寄存器分类。一个寄存器分配齐允许一个指令使用所有在特定寄存器类中的寄存器并且有相似的行为。寄存器类从集合中分配虚拟寄存器给指令让无关目标平台寄存器分配器自动选择实际的寄存器。
寄存器的许多代码,包括寄存器的定义、别名和寄存器类由TableGen从XXXRegisterInfo.td输入文件生成并被放进XXXGenRegisterInfo.h.inc和XXXGenRegisterInfo.inc输入文件中。一些XXXRegisterInfo的实现代码需要手动编写。
定义一个Register
XXXRegisterInfo.td文件通常以目标平台机器的寄存器定义开始。Register类(在Target.td文件中)用于定义每个寄存器的队形。string n指定为寄存器的名字。基本的寄存器对象没有子寄存器和没有指定别名。
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携带三个值的数组表示三种不同的模式:第一个元素用来表示X86-64,第二个用来在X86-32上的异常处理(EH),第三个是一般用途。-1是个表示gcc数字没有定义,-2表示该模式下寄存器号无效。
从X86RegisterInfo.td文件的之前描述的,TableGen在X86RegisterInfo.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 }, …
对于寄存器的info文件,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个位的数字来识别,这个特征对所有的子类都通用。注意了“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的字段中。
定义Register Class
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, 使用下面四个参数:
• 定义的第一个参数是命名空间的名字。
• 第二个参数是一个定义在include/llvm/CodeGen/ValueTypes.td中的寄存器类型值ValueType的列表。定义的值包括整型类型(例如i16、i32和作为Boolean的i1),浮点类型(f32、f64)和向量类型(例如,v8i16表示8*i16向量)。在RegisterClass的寄存器必须有相同的ValueType,但是一些寄存器可以存储向量数据在不同的配资下。比如一个寄存器能处理一个128位的响亮可能处理16个8位的整型元素、8个16位整型、4个32位整型等等。
• RegisterClass定义中的第三个参数指定了在store或load内存时要求的寄存器对齐。
• 最后一个参数,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生成许多输出文件用于包含在其他你写的源代码。SparcRegisterIno.td生成SparcGenRegisterInfo.h.inc,包含在你写的SPARC寄存器的实现头文件(SparcRegisterINfo.h)中。在SparcGenRegisterInfo.h.inc一个被称为SparcGenRegisterInfo的新结构被定义,TargetRegisterInfo作为基础。
SparcRegisterInfo.td还生成SparcGenRegisterInfo.inc,在SparcRegisterInfo.cpp的最底部include,即Sparc寄存器的实现文件。下面展示的代码仅产生了整型寄存器和相关的寄存器类。在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 Classess...
static const TargetRegisterClass* const IntRegsSubRegClasses [] = {
NULL
};
...
// IntRegs Super-register Classess...
static const TargetRegisterClass* const IntRegsSuperRegClasses [] = {
NULL
};
...
// IntRegs Register Class sub-classes...
static const TargetRegisterClass* const IntRegsSubclasses [] = {
NULL
};
...
// IntRegs Register Class super-classes...
static const TargetRegisterClass* const IntRegsSuperclasses [] = {
NULL
};
IntRegsClass::IntRegsClass() : TargetRegisterClass(IntRegsRegClassID,
IntRegsVTs, IntRegsSubclasses, IntRegsSuperclasses, IntRegsSubRegClasses,
IntRegsSuperRegClasses, 4, 4, 1, IntRegs, IntRegs + 32) {}
}
寄存器分配器将避免使用保留的寄存器,被调用寄存器保存寄存器不被使用直到所有volatile(表示能被频繁占用)类型的寄存器被用完。这通常足够好,但是在某些情况下它有必要体统自定义的分配次序。
实现TargetRegisterInfo的子类
最后一步是手写部分XXXRegisterInfo代码实现TargetRegisterInfo.h中描述的接口。(看 The TargetRegisterInfo class)。这些函数返回0、NULL或者false,除非被重载。这里列举了SparcRegisterInfo.cpp文件中SPARC实现的函数重载:
• getCalleeSavedRegs — 返回按要求栈帧偏移排列的被调用者寄存器列表
• getReservedRegs — 返回一个被物理寄存器号索引的位集合,指示一个特殊的寄存器是否不可用。
• hasFP — 返回一个布尔值表示一个函数是否应该有一个精巧的帧指针寄存器。
• eliminateCallFramePseudoInstr — 如果调用帧创建与销毁伪指令被使用,这样能被调用来销毁。
• eliminateFrameIndex — 销毁指令可能使用的抽象帧索引。
• emitPrologue — 插入前言代码到函数。
• emitEpilogue —插入后缀代码到函数。
Instruction 集
在早期的代码生成阶段,LLVM IR代码被转化为包含目标指令SDNode类的实例的节点组成的SelectionDAG。一个SDNode有一个操作符、操作数、类型要求、操作属性。例如,一个满足交换律的操作,进行一个load操作。不同的操作节点类型在 include/llvm/CodeGen/SelectionDAGNodes.h文件中描述(NodeType的枚举值,在ISD命名空间中)。TableGen使用下面的目标描述文件(.td)来生成指令定义的代码:
• Target.td — Instruction, Operand, InstrInfo和其他几本类定义的地方。
• 被SelectionDAG指令选择生成器使用,包含SDTC*这样的类(选择DAG类型约束),SelectionDAG节点的定义(比如imm,cond,bb,add,fadd,sub)和模式支持(Pattern, Pat, PatFrag, PatLeaf, ComplexPattern)。
• XXXInstrFormats.td — 目标指令的定义模式。
• XXXInstrInfo.td —指令模版的目标平台的定义,条件代码和指令集的指令。为了便于架构的修改,使用不用的文件名。例如,对于Pentium的SSE 指令,这个文件是X86InstrSSE.td,对于Pentium的MMX,文件为X86InstrMMX.td。
还有一个目标平台的XXX.td文件,XXX是目标平台的名字。XXX.td文件包含其他的.td输入文件,但是他的内容仅对子平台有直接意义的重要性。
你得描述一个具体的目标平台类XXXInstrInfo来表示目标平台支持的指令。XXXInstrInfo包含一个XXXInstrDescr-iptor对象的数组,它们每一个都描述一条指令。一条指令描述父如下定义:
• 操作码助记符
• 操作数的数量
• 隐藏寄存器定义和使用的列表
• 目标平台无关的特性(例如内存访问与可交换性)
• 目标平台标示
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目标可能模块化这个指令为两条叫做ADDri和ADDrr的指令。
你得为每一个指令类别定义个类和定义每个操作码作为这个类别的子类和准确的参数比如操作码和扩展的操作码的固定的二进制编码。你应该映射寄存器位到指令的编码位中。当使用自动汇编打印器时,你还要指定指令改如何被打印。
正如SPARC架构手册Version 8所描述的,有三个主要的32位指令格式。格式1仅用于CALL指令。格式2于来条件分支和SETHI(设置寄存器高位)指令。格式3用于其他指令。
每一种格式在SparcInstrFormat.td中有对应的类。InstSP是其他指令类的基类。其他基类被指定用于更惊觉的格式:例如在SparcInstrFormat.td中,F2_1用于SETHI,而F3_2用于分支。有三种其他的基类:F3_1用于寄存器/寄存器操作,F3_2用语寄存器/立即数操作和F3_3用语浮点数操作。SparcInstrInfo.td还增加了Pseudo基类用于合成的SPARC指令。
对于SPARC平台,SparcInstrInfo.td由大量地操作数和和指令定义组成。在SparcInstrInfo.td中,下面的平台描述文件的词条-LDrr,定义了从内存地址取一个Word到寄存器的Load整型指令。第一个参数值为3(二进制位11)是这个操作类别的操作值。第二个参数(二进制000000)是一个指定操作值用于LD/Load Word。第三个草书是输出目的地,是定义在Register描述文件中的一个寄存器数(IntRegs)。
def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$dst), (ins MEMrr:$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRrr:$addr))]>;
第四个参数是输入源,使用了内存地址操作数MEMrr(MEMrr早先定义在SparcInstrInfo.td中):
def MEMrr : Operand<i32> {
let PrintMethod = "printMemOperand";
let MIOperandInfo = (ops IntRegs, IntRegs);
}
第五个参数是一个字符串被汇编打印器使用,能使用空字符串直到会变打印器接口被实现。第六个即最后的参数是在SelectionDAG选择阶段(描述于 The LLVM Target-Independent Code Generator)被用来匹配指令的模式。这个参数在下一节详细介绍, Instruction Selector 。
Instruction类定义不能用来重载不同操作数类型,所以需要为寄存器、内存或者立即数需要独立版本的指令。例如,为从立即数获取一个Word到寄存器使用Load整型指令,定义了下面的指令类:
def LDri : F3_2 <3, 0b000000, (outs IntRegs:$dst), (ins MEMri:$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRri:$addr))]>;
为这么多相似的指令写定义可以采取大量的粘贴复制。在.td文件中,multiclass 指示符能够令模版的创建立即用于定义多个instruction类(使用defm指示符)。例如在SparcInstrInfo.td中,multiclass模式F3_12被定义来创建调用F3_12一次定义两个指令类。
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指令时,正如下面看到的,它创建了四个指令对象:XORrr、XORri、ADDrr和ANDrr。
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等等)。
指令操作数的映射
代码生成器后端映射指令操作数到指令的字段。操作数被按照指令中定义的次序赋值到指令的各个没有的字段。字段在赋值后被绑定。例如,Sparc平台定义了XNORrr指令为F3_1个市的指令有三个操作数。
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; // Op = 2 or 3
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; // 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绑定op3字段和定义了rs2字段。F3_1格式的指令将绑定操作数到rd、rs1和rs2字段。这导致在XNORrr指令中分别将 d s t 、 dst、 dst、b和$c操作数绑定到rd、rs1和rs2字段。
指令操作数名字映射
TableGen会生成一个叫做getNamedOperandIdx()的函数用于查找在基于它的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的定义中提取,那么小写操作数名会有小写的词条在枚举中。
要include那个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中所有的目标平台,并且每一个Target的OpTypes枚举都是可以用的。还有,淡淡命名了的操作数类型出现在枚举结构中:匿名类型将被忽略。例如,X86后端定义了brtarget和brtarget8,两个Tablegen Operand类的实例,代表不同平台操作码。
def brtarget : Operand<OtherVT>;
def brtarget8 : Operand<OtherVT>;
This results in:
namespace X86 {
namespace OpTypes {
enum OperandType {
...
brtarget,
brtarget8,
...
i32imm,
i64imm,
...
OPERAND_TYPE_LIST_END
} // End namespace OpTypes
} // End namespace X86
以典型的TableGen风格使用enum,你需要定义预处理宏:
#define GET_INSTRINFO_OPERAND_TYPES_ENUM // For OpTypes enum
#include "XXXGenInstrInfo.inc"
指令的调度
指令日程能通过MCDesc::getSchedClass()来查询。它的值能被在XXXGenInstrInfo.inc由TableGen生成的llvm::XXX::Shed命名空间中的一个枚举命名。调度类的名字与在XXXSchedule.td中提供的相同加上一个默认的NoItinerary类。
指令的关系映射
TableGen的特性是用来构建指令之间的关联。当你需要多种指令格式和需要在指令选择之后在它们之间相互切换时非常有用。全部特性由被定义在XXXINstrInfo.td文件中的根据目标平台指令集的关系模型所驱动。使用InstrMapping类定义关系模型。TableGen解析了所有的模型并且使用特定的信息长生了指令的关系映射。关系映射被以表格的形式发射入XXXGenInstrInfo.inc文件随同一些查询函数。对于如何使用这种特性的详细信息,请参考 How To Use Instruction Mappings。
实现TargetInstrInfo子类
最后的一步是编写XXXInstrInfo,实现TargetInstrInfo.h描述的接口(看 The TargetInstrInfo class)。这些函数返回0或者一个Boolean或者它assert,除非被重载。这里是SparcInstrInfo.cpp中SPARC实现的一系列函数的重载:
• isLoadFromStackSlot — 如果机器指令直接从栈槽中加载,返回目的寄存器号和栈槽的帧索引。
• isStoreToStackSlot —如果指定的机器指令直接存储到栈槽,返回目的寄存器号和栈槽帧索引。
• copyPhysReg — 在两个物理寄存器之间赋值值。
• storeRegToStackSlot —存储一个寄存器值到栈槽。
• loadRegFromStackSlot — 从栈槽加载一个寄存器值。
• storeRegToAddr — 存储一个寄存器值到内存。
• loadRegFromAddr — 从内存加载一个寄存器值。
• foldMemoryOperand — 试图为特定的操作数合并所有load/store指令。
分支折叠与If转化
通过合并指令或者消除用不执行的指令能改善性能。XXXInstrInfo中的AnalyzeBranch方法实现检查条件指令与删除不必要的指令。AnalyzeBranch查看机器MBB的最后寻找改善的机会,例如分枝折叠与if转化。BranchFolder和IfConverter机器函数Pass(看lib/CodeGen下的BranchFolding.cpp和IfCoversion.cpp源代码文件)调用AnalyzeBranch来改善表示指令的控制流图(CFG)。
许多AnalyzeBranch的实现可以作为模型来检视以创建自己的AnalyzeBranch实现。由于SPARC没有实现一个有用的AnalyzeBranch,下面展示ARM版本的实现。
AnalyzeBranch返回一个Boolean值并携带四个参数:
• MachineBasicBlock &MBB — 被检视的block。
• MachineBasicBlock *&TBB — 返回的目的block。对于分支被估值为true,TBB作为目的block;
• MachineBasicBlock *&FBB —对于分支被估值为false,FBB作为目的block返回。
• std::vector &Cond — 一系列的操作数作为条件分支。
在这个最简单情况下,如果block结束时没有分支,那么它检视(falls through不知道怎么翻译)后继的block。没有目的block为TBB或者FBB指定,所以两个参数返回NULL。AnalyzeBranch的开始展示了最简单的情况下的函数的参数和代码。
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;
如果block以单个无条件指令结束,那么AnalyzeBranch(下面展示的)应该返回TBB参数中的那个分支的目的block。
if (LastOpc == ARM::B || LastOpc == ARM::tB) {
TBB = LastInst->getOperand(0).getMBB();
return false;
}
如果block以两个无条件的分支结束,那么第二个分支永远不会被执行。在那种情况下,正如下面所示,移除最后的分支指令并情返回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;
}
一个block以单个条件分支指令结束那么跌入后继block,如果条件被估值为false。在那种情况,AnalyzeBranch(下面展示的)应该返回TBB参数中条件分支的目的block并且以con参数里的操作数作为条件。
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;
}
如果block同时以一个条件分支和一个确信的无条件分支结束,那么AnalyzeBranch(下面所示)应该返回在TBB参数中条件分支的目的block(假设它对应一个条件估值为true)并且FBB(对应于条件估值为false)中的分支目的block。用来计算条件的一系列的操作数应该在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的实现要求helper方法RemoveBranch和InsertBranch来管理接下来的操作。在大多数情况下,AnalyzeBranch应该返回false以指示成功。AnalyzeBranch应该仅返回true。在这个方法难于决定做什么时,比如,如果一个blockyou三个结束分支。如果AnalyzeBranch遇到一个终结符它不能处理可能返回true,像一个非直接的分支。
Instruction Selector选择器
LLVM使用一个SelectionDAG表示LLVM IR指令,且SelectionDAG的节点理想地表示本地目标的指令。在代码生成阶段,指令选择Pass进行指令的合法化。描述在XXXISelDAGToDAG.cpp的Pass被用来匹配模式并进行DAG-DAG的指令选择。
可选地,对于分支指令,一个Pass可被定义(在XXXBranchSelectior.cpp)来进行类似的DAG-DAG操作。
TableGen使用下面的目标描述输入文件为指令选择产生代码:
•包含在特定目标指令集中指令的定义,产生XXXGenDAGISel.inc文件被include到XXXIselDAGToDAG.cpp中。
•包含特定目标平台架构的调用与返回约定,并且它产生一个XXXGenCallingConv.inc文件将被XXXIselLower-ing.cpp include。
指令选择Pass的实现必须包含一个生命了FunctionPass的类或子类的头文件。在XXXTargetMachine.cpp中,一个Pass管理器(PM)应该增加每一个指令选择器到Pass队列中以运行。
LLVM静态编译器(llc)是一个出色的工具实现了可视化浏览DAG的内容。llc使用命令行选项,显示特定处理前后的SelectionDAG,描述于 SelectionDAG Instruction Selection Process。
描述指令选择器的行为,你得为loweringLLVM代码增加模式(Pattern)到SelectionDAG作为XXXInstrInfo.td中的指令定义的最后一个参数中。例如,在SparcInstrInfo.td,这个词条定义了一个寄存器的store操作,并且最后的参数描述了一个有store DAG操作符的模式。
def STrr : F3_1< 3, 0b000100, (outs), (ins MEMrr:$addr, IntRegs:$src),
"st $src, [$addr]", [(store i32:$src, ADDRrr:$addr)]>;
ADDRrr is a memory mode that is also defined in SparcInstrInfo.td:
def ADDRrr : ComplexPattern<i32, 2, "SelectADDRrr", [], []>;
ADDrr的定义涉及了SelectADDrr,这个函数定义在指令选择器的实现中(比如SparcISelDAGToDAG.cpp)。
在lib/Target/TargetSelectionDAG.td中,store指令的DAG操作符被定义如下:
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还生成(在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代码。在XXXGenDAGISel.inc中生成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);
}
…
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 — 舍弃Store操作。
• setIndexedLoadAction — 索引了的Load。
• setIndexedStoreAction — 索引了的Store。
• setConvertAction — 类型转换。
• setCondCodeAction — 条件码支持。
注意:在之前的版本中,setLoadXAction用以替代setLoadExtAction。还有,在一些较老的版本,setCondCodeAction可能不被支持。检查你的版本看什么方法是支持的。
这些回调用来决定一个操作是否与指定的类型配合有效。并且在所有的情况下,第三个参数是一个LegalAction类型的枚举值:Promote、Expand、Custom、或者Legal。SparcISelLowering.cpp有四个LegalAction值的样例。
Promote
对于给定一个类型本地并不支持的操作,特定的类型可能要被提升为较大的类型以支持。例如对于Boolean值(i1类型)SPARC不支持符号扩展的load,如此在SparcISelLowering.cpp中第三个参数Promote,在load之前改变i1类型的值为一个较大的值。
setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
Expand
对于本地不支持的类型,一个值可能需要被打散,与其提升。对于一个本地不支持的操作操作,其他操作的合并可能被用来达到相似的效果。在SPARC中,浮点Sine和cosine三角操作通过扩展其他操作来支持,在第三个参数中指定为Expand给setOperationAction:
setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);
Custom
对于某些操作,简单的类型提升或者操作扩展并不充分。在某些时候,一个特殊的固有函数必须实现。
例如,一个常数值可能要求特殊的处理或者一个操作可能需要在栈中溢出和恢复寄存器并且与寄存器分配器一起工作。
正如在下面SparcISelLowering.cpp代码中看到的,进行一个从浮点型的值转为带符号的整型的转化,首先setOperationAction得被调用并将第三个参数设置为Custom;
setOperationAction(ISD::FP_TO_SINT, MVT::i32, Custom);
在LoweringOperation的方法中,对每一个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);
}
Legal
Legal这个LegalizeAction枚举值简单地指示了一个操作本地是支持的。Legal表示默认的情形,所以很少使用。在SparcISelLowering.cpp中,CTPOP(一个计算整型位的集合的操作)的action只在SPARC v9上支持。接下来的代码为一个非v9 SPARC实现开启Expand转化技术。
setOperationAction(ISD::CTPOP, MVT::i32, Expand);
...
if (TM.getSubtarget<SparcSubtarget>().isV9())
setOperationAction(ISD::CTPOP, MVT::i32, Legal);
Calling Conventions 调用约定
为了支持特定平台的调用约定,XXXGenCallingConv.td使用了定义在 lib/Target/TargetCallingConv.td的接口(比如CCIfType和CCAssignToReg)。TableGen能够处理目标描述文件XXXGenCallingConv.td并且声称一个XXXGenCallingConv.inc的头文件。你能后使用在TargetCallingConv.td中的接口来指定:
• 参数分配的次序。
• 参数和返回值被放在哪里(也就是说放在栈上还是寄存器中)。
• 那些寄存器可以被使用。
• 被调用与调用者是否展开栈。
下面的例子演示CCIfType和CCAssignToReg接口的用法。如果CCIfType predicate是true(也就是说,如果当前的参数是f32类型或者f64类型),那么action就被执行。在这种情况下,CCAssignToReg的action 赋参数值给第一个可用的寄存器:要么R0要么R1。
CCIfType<[f32,f64], CCAssignToReg<[R0, R1]>>
SparcCallingConv.td包含目标平台返回值的调用约定(RetCC_Sparc32)的定义河基本的32位C调用约定(CC_Sparc32)。RetCC_Sparc32(如下所示)指示了那个寄存器被指定标量返回值所使用。一个单精度的返回值给寄存器F0,和一个双精度浮点放入D0.一个32-bit的zhengxing数返回到寄存器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个字节单元对齐。(特殊情况:如果大小是0,那么ABI大小就被使用;,如果对齐是0,那么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是一个尝试匹配给定当前调用约定名字的接口。如果名字标示了当前的调用约定,那么一个特定的action被调用。在下面的例子(在X86CallConv.td文件中),如果Fast调用约定被使用,那么RetCC_X86_32_Fast被调用。如果SSECall调用约定被使用,那么RetCC_X86_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匹配,应用这个action。
• CCIfInReg —如果参数标记为“inreg”属性,那么应用action。
• CCIfNest — 如果参数标记为“nest”属性,那么使用action。
• CCIfNotVarArg — 如果当前函数不懈怠一个可变数目的参数,使用改action。
• CCAssignToRegWithShadow <registerList, shadowList> — 类似CCAssignToReg,但是有个寄存器的影子列表。
• CCPassByVal <size, align> —赋值给栈槽最小的指定大小和对齐方式。
• CCPromoteToType — 提升当前的值为特定的类型。
• CallingConv <[actions]> — 定义每一个支持的调用约定。
汇编打印器
在代码发射阶段,代码生成器会利用一个LLVM Pass来生成汇编输出。为完成这个任务,你想要为你的目标平台实现一
个打印器转化LLVMIR为GAS格式的汇编语言的代码,使用下列步骤:
• 为你的目标平台定义所有的汇编字符串,添加他们到XXXInstrInfo.td文件中定义的指令当中去(看 Instruction Set)。TableGen会为XXXAsmPrinter类生成一个printInstrunction方法的输出文件(XXXGenAsmWriter.inc)。
• 编写XXXTargetAsmInfo.h,包含声明了XXXTargetAsmInfo(一个TargetAsmInfo的子类)类的骨架。
• 编写XXXTargetAsmInfo.cpp,包含特定平台的TargetAsmInfo属性的值和方法的新的实现。
• 编写XXXAsmPrinter.cpp,实现AsmPrinter类以进行LLVM到汇编码的转化。
在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";
}
The X86 assembly printer implementation (X86TargetAsmInfo) is an example where the target specific TargetAsmInfo class uses an overridden methods: ExpandInlineAsm.
A target-specific implementation of AsmPrinter is written in XXXAsmPrinter.cpp, which implements the AsmPrinter class that converts the LLVM to printable assembly. The implementation must include the following headers that have declarations for the AsmPrinter and MachineFunctionPass classes. The MachineFunctionPass is a subclass of FunctionPass.
X86汇编打印器的实现是一个特定TargetAsmInfo类使用重载方法:ExpandInlineAsm的例子:
#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打印出被当前函数使用的jump table。
• 打印出当前函数的标签。
• 打印函数代码,包括基本块标签和指令汇编(使用printInstruction)。
XXXAsmPrinter的实现必须包含下面由TableGen产生的输出文件XXXGenAsmWriter.inc中的代码。XXXGenAsmWriter.inc的代码包含printInstruction方法的实现,可能会调用这些方法:
• printOperand
• printMemOperand
• printCCOperand (for conditional statements)
• printDataDirective
• printDeclare
• printImplicitDef
• printInlineAsm
printDeclare, printImplicitDef, printInlineAsm, and printLabel在Asmprinter.cpp中的实现大致足够打印汇编且不需要重载。
实现一个由长的switch/case语句的printOperand方法的操作数类型为:寄存器、立即数、基本块、外部符号、全局地址、常量池索引或者跳转表索引。一个由内存地址操作数饿指令,应该实现printMemOperand方法来生成正确的输出。相似地,应该使用printCCoperand打印一个条件操作数。
必须在XXXAsmPrinter中重载doFinalization,且调用它来关闭汇编打印器。在doFianlization期间,全局变量和常数被打印至输出。
子平台的支持
使用子平台支持来赋给给定的芯片集的指令差异的代码生成过程特征。例如,LLVM SPARC实现提供了覆盖三个主要版本的SPARC微处理器的架构:Version 8(32-bit架构),Version 9(64-bit架构)和UltraSPARC架构。V8由16个双精度浮点数寄存器,也被用位32个单精度或者8个4精度寄存器。V8也是纯粹地大尾端。V9由32个双精度浮点寄存器,也可以用为16个4精度的寄存器,但是不能用为单精度寄存器。UltraSPARC架构合并了V9与视觉指令集的扩展。
如果需要子平台支持,你得为你的架构实现一个特定目标平台的XXXSubTarget类。这个类必须处理命令行选项-mcpu=与-mattr=。TableGen使用Target.td中的定义和Sparc.td文件来生成SparcGenSubtarget.inc中的代码。在Target.td中,如下所示,定义了SubtargetFeature接口。SubtargetFeature接口的开头4个字符串类型的参数分别是特性名称、特性的属性、属性的值和特性的描述。(第五个参数是隐含特性的列表,默认值是空数组。)
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]>;
从Target.td和Sparc.td文件中,由此产生的SparcGenSubTarget.inc指定枚举值标示特性,常量数组表示CPU特性和子类型,且ParseSubtargetFeatures方法解析指定子平台选项的特性字符串。长生的SparcGenSubtarget.inc文件必须被include到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代码的生成:
• 编写包含一个机器函数的Pass转化机器指令为可重定位的机器码的XXXCodeEmitter.cpp文件。
• 编写实现目标平台代码生成活动的JIT接口的XXXJITInfo.cpp文件,比如发射机器码和桩。
• 修改XXXTargetMachine以至于它提供一个TaretJITInfo对象给它的getJITInfo方法。
有多种不同的编写写支持JIT代码的方法。例如,TableGen和目标描述文件被用来创建一个JIT代码生成器,但是不是法定的。对于Alpha和PowerPC目标机器,TableGen被用来生成XXXGenCodeEmitter.inc,包括机器指令的二进制码编写和访问这些代码的getBinaryCodeForInstr方法,其他的JIT实现并不这样。
XXXJITInfo.cpp和XXXCodeEmitter.cpp同时必须include 定义了含有用来写数据到输出流的多个回调函数的MachineCodeEmitter类的llvm/CodeGen/MachineCodeEmitter.h头文件。
机器码发射器
在XXXcodeEmitter.cpp,特定目标平台的Emitter类被作为函数Pass(MachineFunctionPass的子类)实现。特定平台的runOnMachineFunction的实现迭代MachineBasicBlock调用emitInstruction来处理每一条指令并发射二进制代码。通过使用大量的case语句在XXXInstrInfo.h中定义的指令类型上来实现emitInstruction。例如,在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中,以X86::AddRegFrm为例,第一个被发射的数据(被EmitByte)是被添加到寄存器操作数的操作码。然后代表这个机器吗的对象,MO1,被提取。例如isImmediate、isGlobalAddress、isExternalSymbol、isConstantPoolIndex和isJumptableIndex这样的助手方法决定操作数的类型。(X86CodeEmitter.cpp还有私有的方法比如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使用RelocationType枚举类型的变量rt会被用来重定位寻址(比如,一个全局地址与一个PIC 基偏移)。目标平台的RelocationType枚举结构被定义在短小的目标平台的XXXRelocations.h文件中。使用在XXXJITInfo.cpp中RelocationType的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
};
Target JIT Info
XXXJITInfo.cpp实现了目标平台代码生成行为的JIT接口,比如发射机器码和桩。最小尺度目标平台的XXXJITInfo的实现遵循接下来的步骤:
• getLazyResolverFunction —初始化JIT,给目标平台一个用来编译的函数。
• emitFunctionStub — 返回一个指定了回调函数地址的本地函数。
• relocate —根据重定位类型改变引用的全局变量或常数的地址。
• 当整个的目标平台开始时并不知道,使用被包装为一个函数桩的回调函数。
getLazyResoulverFunction大体上对实现无关紧要。它使传进来的参数作为全局的JITCompilerFunction并返回将使用函数包装器的回调函数。对于Alpha目标平台(在AlphaJITInfo.cpp中),getLazyResolverFunction实现非常简单:
TargetJITInfo::LazyResolverFn AlphaJITInfo::getLazyResolverFunction(
JITCompilerFn F) {
JITCompilerFunction = F;
return AlphaCompilationCallback;
}
对于X86目标平台,getLazyResolverFunction实现有点复杂,因为它为有SSE指令和XMM寄存器的处理器返回了不同的回调函数。
回调函数起先保存和延迟恢复被调用者寄存器的值、传入的参数和帧与返回地址。回调函数需要对寄存器或栈低层次的访问,这样通常与汇编器一起实现。