LLVM后端 td文件 tablegen 模式匹配 寄存器 指令集 calling convention

目录

一、寄存器

[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架构中,q0v0寄存器实际上是同一个寄存器的不同表示方式。在Aarch64中,寄存器q0v0都代表了128位的寄存器,用于存储128位的向量数据。q0Quad Register的缩写,表示一个128位的寄存器,而v0则表示一个向量寄存器。因此,q0v0在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方法。

在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。

相关推荐
谈谈叭26 分钟前
Javascript中的深浅拷贝以及实现方法
开发语言·javascript·ecmascript
lx学习27 分钟前
Python学习26天
开发语言·python·学习
大今野1 小时前
python习题练习
开发语言·python
爱编程的鱼1 小时前
javascript用来干嘛的?赋予网站灵魂的语言
开发语言·javascript·ecmascript
捕鲸叉2 小时前
怎样在软件设计中选择使用GOF设计模式
c++·设计模式
捕鲸叉2 小时前
C++设计模式和编程框架两种设计元素的比较与相互关系
开发语言·c++·设计模式
未知陨落3 小时前
数据结构——二叉搜索树
开发语言·数据结构·c++·二叉搜索树
大波V53 小时前
设计模式-参考的雷丰阳老师直播课
java·开发语言·设计模式
无敌最俊朗@3 小时前
unity3d————接口基础知识点
开发语言·c#
一丝晨光4 小时前
gcc 1.c和g++ 1.c编译阶段有什么区别?如何知道g++编译默认会定义_GNU_SOURCE?
c语言·开发语言·c++·gnu·clang·gcc·g++