目录
[1.1 寄存器定义](#1.1 寄存器定义)
[1.2 寄存器分类](#1.2 寄存器分类)
[2.1 指令集定义](#2.1 指令集定义)
[2.2 模式匹配](#2.2 模式匹配)
[2.2.1 PatFrags与PatFrag](#2.2.1 PatFrags与PatFrag)
[2.2.2 OutPatFrag](#2.2.2 OutPatFrag)
[2.2.3 PatLeaf](#2.2.3 PatLeaf)
[2.2.4 ImmLeaf](#2.2.4 ImmLeaf)
[2.2.5 IntImmLeaf和FPImmLeaf](#2.2.5 IntImmLeaf和FPImmLeaf)
[2.2.6 Pat](#2.2.6 Pat)
[2.2.7 ComplexPattern](#2.2.7 ComplexPattern)
[2.3 指令合法化](#2.3 指令合法化)
[2.3.1 Promote](#2.3.1 Promote)
[2.3.2 Expand](#2.3.2 Expand)
[2.3.3 Custom](#2.3.3 Custom)
[2.3.4 Legal](#2.3.4 Legal)
[2.4 模式匹配的底层实现(先记录,待扩展)](#2.4 模式匹配的底层实现(先记录,待扩展))
[2.5 总结](#2.5 总结)
[三、calling convention](#三、calling convention)
一、寄存器
1.1 寄存器定义
cpp
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表示寄存器的备用名称索引,这么解释可能不太好理解,我们看了例子就明白了。
cpp
// 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寄存器的例子。
cpp
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 寄存器分类
cpp
// 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, ...)),还可以使用更高级的集合运算符。
cpp
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是指令集相关接口的父类所在的文件,我们的工作是继承所需的功能然后实现我们要定制化的指令。
cpp
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来模拟这种情况。
cpp
// 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
cpp
//===----------------------------------------------------------------------===//
// 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代码里边生成的一些谓词条件,第四个参数主要是用于表示立即数的。比较重要的是前两个参数,也是较常用的两个参数。
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
cpp
// 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
cpp
// 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
cpp
// 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
cpp
// 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
cpp
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
cpp
// 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架构所支持的更大的类型。
cpp
setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
2.3.2 Expand
对于没有本地支持的类型,一个值可能需要进一步分解,而不是提升。对于没有本地支持的操作,可能需要使用其他操作的组合来达到类似的效果。在SPARC中,浮点正弦和余弦三角函数操作通过扩展到其他操作来实现支持,这由setOperationAction的第三个参数Expand指示。
cpp
setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);
2.3.3 Custom
对于某些操作,简单的类型提升或操作扩展可能不足够。在某些情况下,必须实现特殊的内部函数。
例如,常量值可能需要特殊处理,或者某个操作可能需要在堆栈中溢出和恢复寄存器,并与寄存器分配器一起工作。
如下所示,在X86ISelLowering.cpp代码中,要将处理绝对值运算,首先应该调用setOperationAction,将Custom作为第三个参数:
cpp
// 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方法:
cpp
case ISD::ABS: return LowerABS(Op, Subtarget, DAG);
最后,实现LowerABS方法。
2.3.4 Legal
在SparcISelLowering.cpp中,LegalizeAction枚举值Legal表示操作具有本地支持的默认条件,因此很少使用。对于CTPOP(用于计算整数中设置的位数的操作),仅在SPARC v9中本地支持。以下代码为非v9 SPARC实现启用了Expand转换技术。
cpp
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
cpp
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的定义,我们简单来分析下。
cpp
/// 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向量类型来处理,第三条同理。
cpp
/// 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来处理的。
cpp
/// 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的参数我们将其存到栈上。
cpp
/// 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文件内。
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,我们也简单介绍下。
cpp
/// 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。
cpp
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。