目录
一、寄存器
1.1 寄存器定义
1.2 寄存器分类
二、指令集
2.1 指令集定义
2.2 模式匹配
2.2.1 PatFrags与PatFrag
2.2.2 OutPatFrag
2.2.3 PatLeaf
2.2.4 ImmLeaf
2.2.5 IntImmLeaf和FPImmLeaf
2.2.6 Pat
2.2.7 ComplexPattern
2.3 指令合法化
2.3.1 Promote
2.3.2 Expand
2.3.3 Custom
2.3.4 Legal
2.4 模式匹配的底层实现(先记录,待扩展)
2.5 总结
三、calling convention
一、寄存器
1.1 寄存器定义
class X86Reg<string n, bits<16> Enc, list<Register> subregs = []> : Register<n> {
let Namespace = "X86";
let HWEncoding = Enc;
let SubRegs = subregs;
}
def AL : X86Reg<"al", 0>;
def DL : X86Reg<"dl", 2>;
def CL : X86Reg<"cl", 1>;
def BL : X86Reg<"bl", 3>;
这是LLVM后端td文件中定义寄存器的例子,继承自Target.td文件中的Register父类,这里我们介绍一下父类Register中的两个结构,SubRegs和RegAltNameIndices,SubRegs指的是子寄存器,RegAltNameIndices表示寄存器的备用名称索引,这么解释可能不太好理解,我们看了例子就明白了。
// Target.td
class SubRegIndex<int size, int offset = 0> {
...
}
// AArch64RegisterInfo.td
class AArch64Reg<bits<16> enc, string n, list<Register> subregs = [],
list<string> altNames = []>
: Register<n, altNames> {
let HWEncoding = enc;
let Namespace = "AArch64";
let SubRegs = subregs;
}
def bsub : SubRegIndex<8>;
def dsub : SubRegIndex<64>;
def B0 : AArch64Reg<0, "b0">, DwarfRegNum<[64]>;
let SubRegIndices = [bsub] in {
def H0 : AArch64Reg<0, "h0", [B0]>, DwarfRegAlias<B0>;
...
}
...
let SubRegIndices = [dsub], RegAltNameIndices = [vreg, vlist1] in {
def Q0 : AArch64Reg<0, "q0", [D0], ["v0", ""]>, DwarfRegAlias<B0>;
...
}
我们在文章AArch64 ARM64 寄存器介绍 简单介绍过AArch64架构的寄存器,我们看一下AArch64上的浮点寄存器的一个例子。我们先定义了8位的B系列寄存器,然后定义了H系列寄存器,且指定了subregs集合是对应的B系列寄存器,这个有什么效果呢?H寄存器用一个let SubRegIndices = [bsub]包了起来,意思就是H系列寄存器的SubRegIndices属性都为bsub,然后我们再看bsub的定义,是一个SubRegIndex类型的对象,SubRegIndex对象有两个参数第一个是size第二个是offset偏移(默认是0),因此H0与B0寄存器的关系就是,H0寄存器中从偏移0的位置开始8size的内容为B0寄存器,这个与我们之前介绍的B系列与H系列寄存器的关系也相符合。
DwarfRegAlias是gdb等调试器识别的寄存器编码,这里的意思是H0与B0的编码是别名关系,也就相当于H0与B0使用一样的编码,也就是B0定义中的DwarfRegNum<[64]>的64。名字中带有Dwarf的多数都是与调试有关的一些信息。
然后我们再看Q0寄存器,我们知道AArch64上Q系列是128位的寄存器,D系列是64位的寄存器,根据上边的分析很好理解定义了Q的前64位为对应的D系列寄存器,这里还使用了AArch64Reg的第四个参数altNames属性,这个是干什么的呢?根绝之前对于AArch64寄存器的介绍,其实Q系列和V系列的寄存器都是128位的,其实是相同的寄存器,就是有时候用的不同表示,这个就是备用名的意思。
在Aarch64架构中,
q0
和v0
寄存器实际上是同一个寄存器的不同表示方式。在Aarch64中,寄存器q0
和v0
都代表了128位的寄存器,用于存储128位的向量数据。q0
是Quad Register
的缩写,表示一个128位的寄存器,而v0
则表示一个向量寄存器。因此,q0
和v0
在Aarch64架构中是等效的,都指代同一个128位的向量寄存器,只是在不同的上下文中可能会使用不同的命名方式。
这里是GPT对于Q和V的解释,也可以进一步参考一下。AArch64上只有1个子寄存器,我们再看一个X86寄存器的例子。
let Namespace = "X86" in {
def sub_8bit : SubRegIndex<8>;
def sub_8bit_hi : SubRegIndex<8, 8>;
def sub_8bit_hi_phony : SubRegIndex<8, 8>;
def sub_16bit : SubRegIndex<16>;
def sub_16bit_hi : SubRegIndex<16, 16>;
def sub_32bit : SubRegIndex<32>;
...
}
// 8-bit registers, low registers
def AL : X86Reg<"al", 0>;
// 8-bit registers, high registers
def AH : X86Reg<"ah", 4>;
// 16-bit registers
let SubRegIndices = [sub_8bit, sub_8bit_hi], CoveredBySubRegs = 1 in {
def AX : X86Reg<"ax", 0, [AL,AH]>;
...
}
let isArtificial = 1 in {
...
// High word of the low 32 bits of the super-register:
def HAX : X86Reg<"", -1>;
}
let SubRegIndices = [sub_16bit, sub_16bit_hi], CoveredBySubRegs = 1 in {
def EAX : X86Reg<"eax", 0, [AX, HAX]>, DwarfRegNum<[-2, 0, 0]>;
...
}
let SubRegIndices = [sub_32bit] in {
def RAX : X86Reg<"rax", 0, [EAX]>, DwarfRegNum<[0, -2, -2]>;
...
}
X86_64 寄存器介绍 文章中有对X86寄存器的大体的介绍。根据上边的分析,我们很容易知道,AX寄存器的低8位是AL,高8位是AH,然后这里还有一个CoveredBySubRegs = 1的属性,这个属性指的是AX的子寄存器是否完全覆盖了AX,这里AX是16位寄存器,高低8位都有对应的子寄存器,因此完全覆盖了,所以CoveredBySubRegs为true,上面AArch64的例子中只使用了低位部分,因此没有完全覆盖,CoveredBySubRegs默认为false。
然后EAX的定义中低16位为AX,高16位为HAX,HAX定义中有一个isArtificial=1的属性,这个指的是这个寄存器是“人工”的,寄存器可以被指定为“人工”,这意味着它在关联架构中没有对应的物理寄存器。这对于创建与实际(非人工)寄存器的无法寻址部分对应的寄存器可能很有用。
DwarfRegNum定义了debug信息,用于gcc、gdb或者一个调试信息输入来标识一个寄存器。对于寄存器EAX,DwarfRegNum携带三个值的数组表示三种不同的模式:第一个元素用来表示X86-64,第二个用来在X86-32上的异常处理(EH),第三个是一般用途。-1是个表示gcc数字没有定义,-2表示该模式下寄存器号无效。
1.2 寄存器分类
// RegisterClass - Now that all of the registers are defined, and aliases
// between registers are defined, specify which registers belong to which
// register classes. This also defines the default allocation order of
// registers by register allocators.
//
class RegisterClass<string namespace, list<ValueType> regTypes, int alignment,
dag regList, RegAltNameIndex idx = NoRegAltName>
: DAGOperand {
...
}
- 参数一:namespace的名字,一般指的是架构名
- 参数二:第二个参数是在include/llvm/CodeGen/ValueTypes.td中定义的ValueType寄存器类型值的列表。定义的值包括整数类型(例如i16、i32和i1)、浮点类型(f32、f64)和向量类型(例如v8i16表示8 x i16向量)。RegisterClass中的所有寄存器必须具有相同的ValueType,但某些寄存器可能以不同的配置存储矢量数据。例如,一个能处理128位矢量的寄存器可能能够处理16个8位整数元素、8个16位整数、4个32位整数等。
- 参数三:指定了将寄存器存储或加载到内存时所需的对齐方式。
- 参数四:最后一个参数regList指定了属于这个类的寄存器。如果未指定替代的分配顺序方法,则regList还定义了寄存器分配器使用的分配顺序。除了简单地列出寄存器(如(add R0, R1, ...)),还可以使用更高级的集合运算符。
def FPR64 : RegisterClass<"AArch64", [f64, i64, v2f32, v1f64, v8i8, v4i16, v2i32,
v1i64, v4f16, v4bf16],
64, (sequence "D%u", 0, 31)>;
// For tail calls, we can't use callee-saved registers, as they are restored
// to the saved value before the tail call, which would clobber a call address.
// This is for indirect tail calls to store the address of the destination.
def tcGPR64 : RegisterClass<"AArch64", [i64], 64, (sub GPR64common, X19, X20, X21,
X22, X23, X24, X25, X26,
X27, X28, FP, LR)>;
例如AArch64中的FPR64和tcGPR64这两个类,FPR64是一个64位浮点类型的寄存器类,sequence "D%u", 0, 31表示了D0-D31这32个寄存器,然后支持多种类型,我们能看到这些类型都是64位的各种整形、浮点及向量类型,因此寄存器可以支持不同的类型。
tcGPR64是个用于尾调用的寄存器类,这里做了一个集合的运算,将GPR64common寄存器类中的寄存器减去后边列出来的那些寄存器(callee saved寄存器)就是tcGPR64里边的寄存器。集合运算的话也可以做多次,例如我们sub之后可以再add,这些都是允许的操作。
二、指令集
2.1 指令集定义
- Target.td — 定义了指令、操作数、InstrInfo等基本类的地方。
- TargetSelectionDAG.td — 用于SelectionDAG指令选择生成器,包含SDTC*类(选择DAG类型约束)、SelectionDAG节点的定义(如imm、cond、bb、add、fadd、sub)以及模式支持(Pattern、Pat、PatFrag、PatLeaf、ComplexPattern)。
- XXXInstrFormats.td — 用于定义特定目标指令的模式。
- XXXInstrInfo.td — 指定了特定目标的指令模板、条件码和指令集的指令。对于架构修改,可能会使用不同的文件名。例如,对于带有SSE指令的奔腾处理器,该文件名为X86InstrSSE.td,对于带有MMX的奔腾处理器,该文件名为X86InstrMMX.td。
上述是指令集定义涉及到的几个文件,Target.td和TargetSelectionDAG.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 ...
}
这是Instruction父类的定义,架构手册中的单条指令通常被建模为多个目标指令,取决于其操作数。例如,手册可能描述一个加法指令,它接受寄存器或立即数操作数。LLVM目标可以使用两个指令ADDri和ADDrr来模拟这种情况。
// Class specifying the SSE execution domain, used by the SSEDomainFix pass.
// Keep in sync with tables in X86InstrInfo.cpp.
class Domain<bits<2> val> {
bits<2> Value = val;
}
def GenericDomain : Domain<0>;
def SSEPackedSingle : Domain<1>;
def SSEPackedDouble : Domain<2>;
def SSEPackedInt : Domain<3>;
class X86Inst<bits<8> opcod, Format f, ImmType i, dag outs, dag ins,
string AsmStr, Domain d = GenericDomain>
: Instruction {
let Namespace = "X86";
bits<8> Opcode = opcod;
Format Form = f;
...
}
class I<bits<8> o, Format f, dag outs, dag ins, string asm,
list<dag> pattern, Domain d = GenericDomain>
: X86Inst<o, f, NoImm, outs, ins, asm, d> {
let Pattern = pattern;
let CodeSize = 3;
}
def MOV8rr : I<0x88, MRMDestReg, (outs GR8 :$dst), (ins GR8 :$src),
"mov{b}\t{$src, $dst|$dst, $src}", []>;
def MOV16rr : I<0x89, MRMDestReg, (outs GR16:$dst), (ins GR16:$src),
"mov{w}\t{$src, $dst|$dst, $src}", []>, OpSize16;
def MOV32rr : I<0x89, MRMDestReg, (outs GR32:$dst), (ins GR32:$src),
"mov{l}\t{$src, $dst|$dst, $src}", []>, OpSize32;
def MOV64rr : RI<0x89, MRMDestReg, (outs GR64:$dst), (ins GR64:$src),
"mov{q}\t{$src, $dst|$dst, $src}", []>;
第一个参数表示了操作数,第二个参数表示了指令类型,第三个参数表示了指令输出,第四个参数表示了指令输入,第五个参数表示了匹配模式,第五个参数是X86定一的表示是否是SSE指令集,默认不是。
这里是X86架构上对于各个size的mov指令的定义,MOV8rr等是mov指令在Machine code中的表现形式,movb等是输出到汇编中的形式,我们能看到指令定义的第二行的汇编表示形式用|符号链接了两句,这不是要输出两句,这是定义了两种汇编输出形式,AT&T和Intel形式。
2.2 模式匹配
当前存在的其中模式匹配的类型:
2.2.1 PatFrags与PatFrag
//===----------------------------------------------------------------------===//
// Selection DAG Node Transformation Functions.
//
// This mechanism allows targets to manipulate nodes in the output DAG once a
// match has been formed. This is typically used to manipulate immediate
// values.
//
class SDNodeXForm<SDNode opc, code xformFunction> {
SDNode Opcode = opc;
code XFormFunction = xformFunction;
}
def NOOP_SDNodeXForm : SDNodeXForm<imm, [{}]>;
// Selection DAG Pattern Operations
class SDPatternOperator {
list<SDNodeProperty> Properties = [];
}
/// PatFrags - Represents a set of pattern fragments. Each single fragment
/// can match something on the DAG, from a single node to multiple nested other
/// fragments. The whole set of fragments matches if any of the single
/// fragments match. This allows e.g. matching and "add with overflow" and
/// a regular "add" with the same fragment set.
///
class PatFrags<dag ops, list<dag> frags, code pred = [{}],
SDNodeXForm xform = NOOP_SDNodeXForm> : SDPatternOperator {
dag Operands = ops;
list<dag> Fragments = frags;
code PredicateCode = pred;
code GISelPredicateCode = [{}];
code ImmediateCode = [{}];
SDNodeXForm OperandTransform = xform;
...
}
// PatFrag - A version of PatFrags matching only a single fragment.
class PatFrag<dag ops, dag frag, code pred = [{}],
SDNodeXForm xform = NOOP_SDNodeXForm>
: PatFrags<ops, [frag], pred, xform>;
PatFrags与PatFrag的区别是很好区分的,PatFrags是多模式匹配,多个匹配规则中只要有一个匹配上就算匹配成功,PatFrag是单模式匹配,只有一个匹配规则。他们的参数都完全一样的。第一个参数指的是指令的操作数,第二个参数指的是匹配规则,PatFrags的这个参数类型是个list,表示可以有多个匹配规则,第三个参数是code类型,指的是调用cpp代码里边生成的一些谓词条件,第四个参数主要是用于表示立即数的。比较重要的是前两个参数,也是较常用的两个参数。
def AArch64sabd : PatFrags<(ops node:$lhs, node:$rhs),
[(abds node:$lhs, node:$rhs),
(int_aarch64_neon_sabd node:$lhs, node:$rhs)]>;
defm SABD : SIMDThreeSameVectorBHS<0,0b01110,"sabd", AArch64sabd>;
这是AArch64中对于一个PatFrags使用的例子,SABD指令在AArch64架构中代表"Signed Absolute Difference",用于计算两个操作数的绝对差值,并保留符号。AArch64sabd匹配模式中有两条匹配规则,只要有一个匹配上了,就认为匹配成功,然后生成对应的sabd指令。
2.2.2 OutPatFrag
// OutPatFrag is a pattern fragment that is used as part of an output pattern
// (not an input pattern). These do not have predicates or transforms, but are
// used to avoid repeated subexpressions in output patterns.
class OutPatFrag<dag ops, dag frag>
: PatFrag<ops, frag, [{}], NOOP_SDNodeXForm>;
// PPCInstr64Bit.td
def i64not : OutPatFrag<(ops node:$in),
(NOR8 $in, $in)>;
def : Pat<(not i64:$in),
(i64not $in)>;
OutPatFrag主要是用于输出匹配的,经常作为Pat的result项,如果not i64的形式匹配上了,那么在PPC架构下展开成NOR8指令。
2.2.3 PatLeaf
// PatLeaf's are pattern fragments that have no operands. This is just a helper
// to define immediates and other common things concisely.
class PatLeaf<dag frag, code pred = [{}], SDNodeXForm xform = NOOP_SDNodeXForm>
: PatFrag<(ops), frag, pred, xform>;
// X86InstrInfo.td
def MOV8mi : Ii8 <0xC6, MRM0m, (outs), (ins i8mem :$dst, i8imm :$src),
"mov{b}\t{$src, $dst|$dst, $src}",
[(store (i8 imm_su:$src), addr:$dst)]>;
def imm_su : PatLeaf<(imm), [{
return !shouldAvoidImmediateInstFormsForSize(N);
}]>;
PatLeaf继承自PatFrag,操作数项为空,主要是通过frag和pred来进行匹配。例如,对于X86中的这个指令和匹配规则,MOV8mi的匹配规则是匹配一条store指令,然后store指令的src项要满足imm_su这条匹配规则。imm_su这条匹配的是一个不满足cpp内定义的shouldAvoidImmediateInstFormsForSize这个函数结果的立即数(shouldAvoidImmediateInstFormsForSize是X86ISelDAGToDAG.cpp内的函数),如果全部匹配成功就会生成MOV8mi指令。
2.2.4 ImmLeaf
// ImmLeaf is a pattern fragment with a constraint on the immediate. The
// constraint is a function that is run on the immediate (always with the value
// sign extended out to an int64_t) as Imm. For example:
//
// def immSExt8 : ImmLeaf<i16, [{ return (char)Imm == Imm; }]>;
//
// this is a more convenient form to match 'imm' nodes in than PatLeaf and also
// is preferred over using PatLeaf because it allows the code generator to
// reason more about the constraint.
//
// If FastIsel should ignore all instructions that have an operand of this type,
// the FastIselShouldIgnore flag can be set. This is an optimization to reduce
// the code size of the generated fast instruction selector.
class ImmLeaf<ValueType vt, code pred, SDNodeXForm xform = NOOP_SDNodeXForm,
SDNode ImmNode = imm>
: PatFrag<(ops), (vt ImmNode), [{}], xform> {
let ImmediateCode = pred;
bit FastIselShouldIgnore = false;
// Is the data type of the immediate an APInt?
bit IsAPInt = false;
// Is the data type of the immediate an APFloat?
bit IsAPFloat = false;
}
// X86InstrInfo.td
def i16immSExt8 : ImmLeaf<i16, [{ return isInt<8>(Imm); }]>;
ImmLeaf是专门用于立即数的匹配模式,它继承自PatFrag类,操作数为空,第一个参数是一个类型,第二个参数是code代码,也就是主要的匹配规则。例如X86这里的匹配规则就是判断一个i16类型的立即数是不是Int8范围内的数。
2.2.5 IntImmLeaf和FPImmLeaf
// An ImmLeaf except that Imm is an APInt. This is useful when you need to
// zero-extend the immediate instead of sign-extend it.
//
// Note that FastISel does not currently understand IntImmLeaf and will not
// generate code for rules that make use of it. As such, it does not make sense
// to replace ImmLeaf with IntImmLeaf. However, replacing PatLeaf with an
// IntImmLeaf will allow GlobalISel to import the rule.
class IntImmLeaf<ValueType vt, code pred, SDNodeXForm xform = NOOP_SDNodeXForm>
: ImmLeaf<vt, pred, xform> {
let IsAPInt = true;
let FastIselShouldIgnore = true;
}
// An ImmLeaf except that Imm is an APFloat.
//
// Note that FastISel does not currently understand FPImmLeaf and will not
// generate code for rules that make use of it.
class FPImmLeaf<ValueType vt, code pred, SDNodeXForm xform = NOOP_SDNodeXForm>
: ImmLeaf<vt, pred, xform, fpimm> {
let IsAPFloat = true;
let FastIselShouldIgnore = true;
}
IntImmLeaf和FPImmLeaf是继承自ImmLeaf类的,分别用整数立即数和浮点立即数的匹配模式,用法上与ImmLeaf相同。
2.2.6 Pat
class Pattern<dag patternToMatch, list<dag> resultInstrs> {
dag PatternToMatch = patternToMatch;
list<dag> ResultInstrs = resultInstrs;
list<Predicate> Predicates = []; // See class Instruction in Target.td.
int AddedComplexity = 0; // See class Instruction in Target.td.
}
// Pat - A simple (but common) form of a pattern, which produces a simple result
// not needing a full list.
class Pat<dag pattern, dag result> : Pattern<pattern, [result]>;
// X86InstrInfo.td
def : Pat<(i32 relocImm:$src), (MOV32ri relocImm:$src)>;
Pat使用起来很简单,一共就是两个操作数,第一个是要匹配的形式,第二个是要输出的结果。X86的这个例子就是如果匹配到满足relocImm规则的i32的立即数,那么就将其转化成对应的MOV32ri指令。
Pat匹配模式使用起来比较简单,但是千万不要低估他,很有用。它可以匹配多条指令复合的形式,来生成对应结果的一条指令,做一些架构上的指令优化。它还可以匹配一些当前架构不支持的指令,然后生成当前架构支持的对应功能的指令(可能是多条指令)。
2.2.7 ComplexPattern
// Complex patterns, e.g. X86 addressing mode, requires pattern matching code
// in C++. NumOperands is the number of operands returned by the select function;
// SelectFunc is the name of the function used to pattern match the max. pattern;
// RootNodes are the list of possible root nodes of the sub-dags to match.
// e.g. X86 addressing mode - def addr : ComplexPattern<4, "SelectAddr", [add]>;
//
class ComplexPattern<ValueType ty, int numops, string fn,
list<SDNode> roots = [], list<SDNodeProperty> props = [],
int complexity = -1> {
ValueType Ty = ty;
int NumOperands = numops;
string SelectFunc = fn;
list<SDNode> RootNodes = roots;
list<SDNodeProperty> Properties = props;
int Complexity = complexity;
}
// X86InstrInfo.td
// Define X86-specific addressing mode.
def addr : ComplexPattern<iPTR, 5, "selectAddr", [], [SDNPWantParent]>;
def lea32addr : ComplexPattern<i32, 5, "selectLEAAddr",
[add, sub, mul, X86mul_imm, shl, or, xor, frameindex],
[]>;
// X86ISelDAGToDAG.cpp
bool selectAddr(SDNode *Parent, SDValue N, SDValue &Base,
SDValue &Scale, SDValue &Index, SDValue &Disp,
SDValue &Segment);
bool selectLEAAddr(SDValue N, SDValue &Base,
SDValue &Scale, SDValue &Index, SDValue &Disp,
SDValue &Segment);
我们上边介绍的各种匹配模式的功能总归是有限的,有时候可能不能满足我们的需求,这个时候我们可以考虑用ComplexPattern匹配模式。第一个参数是类型第二个参数是操作数的数目,第三个函数是匹配函数,第四个参数是根节点,第五个参数是SDNode的一些属性,第六个参数是复杂度。
addr的匹配规则是selectAddr这个函数,我们能看到操作数是5,但是函数的实际操作数是7,这是为什么呢?这是因为复杂模式匹配自带一个SDValue N这个操作数,然后addr又指定了需要父节点的这个属性,因此会多一个SDNode *Parent的参数,因此共有7个参数。下方的lea32addr就只有6个参数。
2.3 指令合法化
LLVM中如何检查目标架构原生支持的操作,并为没有原生支持的操作添加回调函数到XXXTargetLowering类的构造函数中,以便指令选择过程知道如何处理。TargetLowering类的回调方法(声明在llvm/Target/TargetLowering.h中)包括:
- setOperationAction — 通用操作。
- setLoadExtAction — 带扩展的加载。
- setTruncStoreAction — 截断存储。
- setIndexedLoadAction — 索引加载。
- setIndexedStoreAction — 索引存储。
- setConvertAction — 类型转换。
- setCondCodeAction — 支持给定条件码。
这些回调用于确定操作是否适用于指定类型(或类型)。在所有情况下,第三个参数是LegalAction类型的枚举值:Promote、Expand、Custom或Legal。
2.3.1 Promote
对于没有对给定类型进行本地支持的操作,指定的类型可能会被提升为一个更大的支持的类型。例如,SPARC不支持对布尔值(i1类型)进行符号扩展加载,因此在SparcISelLowering.cpp中,下面的第三个参数,Promote,会在加载之前将i1类型的值更改为一个Sparc架构所支持的更大的类型。
setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
2.3.2 Expand
对于没有本地支持的类型,一个值可能需要进一步分解,而不是提升。对于没有本地支持的操作,可能需要使用其他操作的组合来达到类似的效果。在SPARC中,浮点正弦和余弦三角函数操作通过扩展到其他操作来实现支持,这由setOperationAction的第三个参数Expand指示。
setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);
2.3.3 Custom
对于某些操作,简单的类型提升或操作扩展可能不足够。在某些情况下,必须实现特殊的内部函数。
例如,常量值可能需要特殊处理,或者某个操作可能需要在堆栈中溢出和恢复寄存器,并与寄存器分配器一起工作。
如下所示,在X86ISelLowering.cpp代码中,要将处理绝对值运算,首先应该调用setOperationAction,将Custom作为第三个参数:
// Integer absolute.
if (Subtarget.canUseCMOV()) {
setOperationAction(ISD::ABS , MVT::i16 , Custom);
setOperationAction(ISD::ABS , MVT::i32 , Custom);
if (Subtarget.is64Bit())
setOperationAction(ISD::ABS , MVT::i64 , Custom);
}
在LowerOperation方法中,对于每个Custom操作,应添加一个case语句来指示调用哪个函数。在下面的代码中,ABS操作码将调用LowerABS方法:
case ISD::ABS: return LowerABS(Op, Subtarget, DAG);
最后,实现LowerABS方法。
2.3.4 Legal
在SparcISelLowering.cpp中,LegalizeAction枚举值Legal表示操作具有本地支持的默认条件,因此很少使用。对于CTPOP(用于计算整数中设置的位数的操作),仅在SPARC v9中本地支持。以下代码为非v9 SPARC实现启用了Expand转换技术。
setOperationAction(ISD::CTPOP, MVT::i32, Expand);
...
if (TM.getSubtarget<SparcSubtarget>().isV9())
setOperationAction(ISD::CTPOP, MVT::i32, Legal);
2.4 模式匹配的底层实现(先记录,待扩展)
LLVM模式匹配的代码主要在CodeGenDAGPatterns.cpp这个文件里。
LLVM模式匹配通过模式匹配树来实现的,具体实现逻辑待日后进一步补充,这里先记录下。
2.5 总结
以上就是对后端指令集和匹配模式等的简单介绍。td文件的功能总归是有限的,llvm希望将所有架构相关的内容都在td文件里实现,不过目前还没有彻底实现这个想法,因此对于一些复杂的需求的话我们需要在XXXInstrInfo.cpp文件里进行进一步实现。
三、calling convention
def CC_AArch64_AAPCS : CallingConv<[
CCIfType<[iPTR], CCBitConvertToType<i64>>,
CCIfType<[v2f32], CCBitConvertToType<v2i32>>,
CCIfType<[v2f64, v4f32], CCBitConvertToType<v2i64>>,
// Big endian vectors must be passed as if they were 1-element vectors so that
// their lanes are in a consistent order.
CCIfBigEndian<CCIfType<[v2i32, v2f32, v4i16, v4f16, v4bf16, v8i8],
CCBitConvertToType<f64>>>,
CCIfBigEndian<CCIfType<[v2i64, v2f64, v4i32, v4f32, v8i16, v8f16, v8bf16, v16i8],
CCBitConvertToType<f128>>>,
// In AAPCS, an SRet is passed in X8, not X0 like a normal pointer parameter.
// However, on windows, in some circumstances, the SRet is passed in X0 or X1
// instead. The presence of the inreg attribute indicates that SRet is
// passed in the alternative register (X0 or X1), not X8:
// - X0 for non-instance methods.
// - X1 for instance methods.
// The "sret" attribute identifies indirect returns.
// The "inreg" attribute identifies non-aggregate types.
// The position of the "sret" attribute identifies instance/non-instance
// methods.
// "sret" on argument 0 means non-instance methods.
// "sret" on argument 1 means instance methods.
CCIfInReg<CCIfType<[i64],
CCIfSRet<CCIfType<[i64], CCAssignToReg<[X0, X1]>>>>>,
CCIfSRet<CCIfType<[i64], CCAssignToReg<[X8]>>>,
// Put ByVal arguments directly on the stack. Minimum size and alignment of a
// slot is 64-bit.
CCIfByVal<CCPassByVal<8, 8>>,
// The 'nest' parameter, if any, is passed in X18.
// Darwin uses X18 as the platform register and hence 'nest' isn't currently
// supported there.
CCIfNest<CCAssignToReg<[X18]>>,
// Pass SwiftSelf in a callee saved register.
CCIfSwiftSelf<CCIfType<[i64], CCAssignToReg<[X20]>>>,
// A SwiftError is passed in X21.
CCIfSwiftError<CCIfType<[i64], CCAssignToReg<[X21]>>>,
// Pass SwiftAsync in an otherwise callee saved register so that it will be
// preserved for normal function calls.
CCIfSwiftAsync<CCIfType<[i64], CCAssignToReg<[X22]>>>,
CCIfConsecutiveRegs<CCCustom<"CC_AArch64_Custom_Block">>,
CCIfType<[nxv16i8, nxv8i16, nxv4i32, nxv2i64, nxv2f16, nxv4f16, nxv8f16,
nxv2bf16, nxv4bf16, nxv8bf16, nxv2f32, nxv4f32, nxv2f64],
CCAssignToReg<[Z0, Z1, Z2, Z3, Z4, Z5, Z6, Z7]>>,
CCIfType<[nxv16i8, nxv8i16, nxv4i32, nxv2i64, nxv2f16, nxv4f16, nxv8f16,
nxv2bf16, nxv4bf16, nxv8bf16, nxv2f32, nxv4f32, nxv2f64],
CCPassIndirect<i64>>,
CCIfType<[nxv1i1, nxv2i1, nxv4i1, nxv8i1, nxv16i1],
CCAssignToReg<[P0, P1, P2, P3]>>,
CCIfType<[nxv1i1, nxv2i1, nxv4i1, nxv8i1, nxv16i1],
CCPassIndirect<i64>>,
// Handle i1, i8, i16, i32, i64, f32, f64 and v2f64 by passing in registers,
// up to eight each of GPR and FPR.
CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,
CCIfType<[i32], CCAssignToReg<[W0, W1, W2, W3, W4, W5, W6, W7]>>,
// i128 is split to two i64s, we can't fit half to register X7.
CCIfType<[i64], CCIfSplit<CCAssignToRegWithShadow<[X0, X2, X4, X6],
[X0, X1, X3, X5]>>>,
// i128 is split to two i64s, and its stack alignment is 16 bytes.
CCIfType<[i64], CCIfSplit<CCAssignToStackWithShadow<8, 16, [X7]>>>,
CCIfType<[i64], CCAssignToReg<[X0, X1, X2, X3, X4, X5, X6, X7]>>,
CCIfType<[f16], CCAssignToReg<[H0, H1, H2, H3, H4, H5, H6, H7]>>,
CCIfType<[bf16], CCAssignToReg<[H0, H1, H2, H3, H4, H5, H6, H7]>>,
CCIfType<[f32], CCAssignToReg<[S0, S1, S2, S3, S4, S5, S6, S7]>>,
CCIfType<[f64], CCAssignToReg<[D0, D1, D2, D3, D4, D5, D6, D7]>>,
CCIfType<[v1i64, v2i32, v4i16, v8i8, v1f64, v2f32, v4f16, v4bf16],
CCAssignToReg<[D0, D1, D2, D3, D4, D5, D6, D7]>>,
CCIfType<[f128, v2i64, v4i32, v8i16, v16i8, v4f32, v2f64, v8f16, v8bf16],
CCAssignToReg<[Q0, Q1, Q2, Q3, Q4, Q5, Q6, Q7]>>,
// If more than will fit in registers, pass them on the stack instead.
CCIfType<[i1, i8, i16, f16, bf16], CCAssignToStack<8, 8>>,
CCIfType<[i32, f32], CCAssignToStack<8, 8>>,
CCIfType<[i64, f64, v1f64, v2f32, v1i64, v2i32, v4i16, v8i8, v4f16, v4bf16],
CCAssignToStack<8, 8>>,
CCIfType<[f128, v2i64, v4i32, v8i16, v16i8, v4f32, v2f64, v8f16, v8bf16],
CCAssignToStack<16, 16>>
]>;
这是AArch64CallingConvention.td文件内一条AArch64的calling convention的定义,我们简单来分析下。
/// CCIfType - If the current argument is one of the specified types, apply
/// Action A.
class CCIfType<list<ValueType> vts, CCAction A> : CCPredicateAction<A> {
list<ValueType> VTs = vts;
}
/// CCBitConvertToType - If applied, this bitconverts the specified current
/// value to the specified type.
class CCBitConvertToType<ValueType destTy> : CCAction {
ValueType DestTy = destTy;
}
CCIfType<[iPTR], CCBitConvertToType<i64>>
CCIfType<[v2f32], CCBitConvertToType<v2i32>>
CCIfType<[v2f64, v4f32], CCBitConvertToType<v2i64>>
CCBitConvertToType就是当做目的类型来处理,这三条的意思是对于地址类型当做i64来处理,对于v2f32向量类型当做v2i32向量类型来处理,第三条同理。
/// CCAssignToReg - This action matches if there is a register in the specified
/// list that is still available. If so, it assigns the value to the first
/// available register and succeeds.
class CCAssignToReg<list<Register> regList> : CCAction {
list<Register> RegList = regList;
}
/// CCIfSRet - If this argument is marked with the 'sret' attribute, apply
/// the specified action.
class CCIfSRet<CCAction A> : CCIf<"ArgFlags.isSRet()", A> {}
CCIfSRet<CCIfType<[i64], CCAssignToReg<[X8]>>>
这里的意思是,如果这是个sret的参数的话,又是个i64类型的话,如果X8寄存器目前可用的话我们就将其存到X8寄存器中,不可用的话一般会存到栈中。注意,这里不是严格的i64类型,因为前边我们对于指针类型的也是当做i64来处理的。
/// CCIfByVal - If the current argument has ByVal parameter attribute, apply
/// Action A.
class CCIfByVal<CCAction A> : CCIf<"ArgFlags.isByVal()", A> {
}
/// CCPassByVal - This action always matches: it assigns the value to a stack
/// slot to implement ByVal aggregate parameter passing. Size and alignment
/// specify the minimum size and alignment for the stack slot.
class CCPassByVal<int size, int align> : CCAction {
int Size = size;
int Align = align;
}
// Put ByVal arguments directly on the stack. Minimum size and alignment of a
// slot is 64-bit.
CCIfByVal<CCPassByVal<8, 8>>
这里的意思是,对于一个ByVal的参数我们将其存到栈上。
/// CCCustom - Calls a custom arg handling function.
class CCCustom<string fn> : CCAction {
string FuncName = fn;
}
/// CCIfConsecutiveRegs - If the current argument has InConsecutiveRegs
/// parameter attribute, apply Action A.
class CCIfConsecutiveRegs<CCAction A> : CCIf<"ArgFlags.isInConsecutiveRegs()", A> {
}
CCIfConsecutiveRegs<CCCustom<"CC_AArch64_Custom_Block">>
这里的意思是如果当前的参数满足这个条件的话,那么我们会调用CC_AArch64_Custom_Block这个参数处理函数,CC_AArch64_Custom_Block这个函数在AArch64CallingConvention.cpp文件内。
/// CCPromoteToType - If applied, this promotes the specified current value to
/// the specified type.
class CCPromoteToType<ValueType destTy> : CCAction {
ValueType DestTy = destTy;
}
CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,
CCIfType<[i32], CCAssignToReg<[W0, W1, W2, W3, W4, W5, W6, W7]>>,
这里的意思是将i1、i8、i16扩展成i32来处理,对于i32类型,将其按顺序存到可用的W0-W7寄存器中,W0-W7也就是AArch64中32位的整形参数寄存器。
CallingConvention.td文件中还有个比较常用的语法CCDelegateTo,我们也简单介绍下。
/// CCDelegateTo - This action invokes the specified sub-calling-convention. It
/// is successful if the specified CC matches.
class CCDelegateTo<CallingConv cc> : CCAction {
CallingConv CC = cc;
}
def CC_AArch64_Win64_VarArg : CallingConv<[
CCIfType<[f16, bf16], CCBitConvertToType<i16>>,
CCIfType<[f32], CCBitConvertToType<i32>>,
CCIfType<[f64], CCBitConvertToType<i64>>,
CCDelegateTo<CC_AArch64_AAPCS>
]>;
对于这个calling convention,将f16和bf16当做i16来处理,将f32当做i32来处理,将f64当做i64来处理,做完这些之后我们再使用CC_AArch64_AAPCS这个calling convention。
def RetCC_X86_32 : CallingConv<[
// If FastCC, use RetCC_X86_32_Fast.
CCIfCC<"CallingConv::Fast", CCDelegateTo<RetCC_X86_32_Fast>>,
CCIfCC<"CallingConv::Tail", CCDelegateTo<RetCC_X86_32_Fast>>,
// CFGuard_Check never returns a value so does not need a RetCC.
// If HiPE, use RetCC_X86_32_HiPE.
CCIfCC<"CallingConv::HiPE", CCDelegateTo<RetCC_X86_32_HiPE>>,
CCIfCC<"CallingConv::X86_VectorCall", CCDelegateTo<RetCC_X86_32_VectorCall>>,
CCIfCC<"CallingConv::X86_RegCall", CCDelegateTo<RetCC_X86_32_RegCall>>,
// Otherwise, use RetCC_X86_32_C.
CCDelegateTo<RetCC_X86_32_C>
]>;
这是X86CallingConvention.td文件内的,CCDelegateTo也可以这样使用。对于获取到的不同的calling convention我们使用对应的calling convention策略,都没匹配上的话,就使用最后这个策略,有点类似于switch case default。