LLVM目标无关代码生成器Target-Independent Code Generator

LLVM目标无关代码生成器Target-Independent Code Generator

介绍

LLVM目标无关代码生成器是一个框架,提供了一套可重用组件,用于将LLVM内部表示转换为指定目标的机器代码,无论是汇编形式(适用于静态编译器)还是二进制机器代码格式(适用于JIT编译器)。LLVM目标独立代码生成器由六个主要组件组成:

抽象的目标描述接口,可以捕获机器各个方面的重要属性,而与使用方式无关。这些接口在include/llvm/Target/中定义。

用于表示为目标生成的代码的类。这些类旨在足够抽象,以表示任何目标机器的机器代码。这些类在include/llvm/CodeGen/中定义。在这个级别上,“常量池条目”和“跳转表”等概念被显式公开。

用于表示对象文件级别(MC层)代码的类和算法。这些类表示组件级构造,如标签、节和指令。在这个级别上,“常量池条目”和“跳转表”等概念并不存在。

用于实现本机代码生成的各个阶段(寄存器分配、调度、堆栈帧表示等)的目标无关算法。此代码位于lib/CodeGen/中。

特定目标的抽象目标描述接口的实现。这些机器描述使用LLVM提供的组件,并可以选择提供定制的特定于目标的传递,为特定目标构建完整的代码生成器。目标描述位于lib/Target/中。

目标独立JIT组件。LLVM JIT完全独立于目标(使用TargetJITInfo结构来处理特定于目标的问题。独立于目标的JIT的代码位于lib/ExecutionEngine/JIT中。

根据对代码生成器的哪一部分感兴趣,其中的不同部分将对您有用。在任何情况下,都应该熟悉目标描述和机器代码表示类。如果要为新目标添加后端,则需要为新目标实现目标描述类,并了解LLVM代码表示。如果对实现新的代码生成算法感兴趣,那么应该只依赖于目标描述和机器代码表示类,确保是可移植的。

代码生成器中所需的组件

LLVM代码生成器的两部分是代码生成器的高级接口和可用于构建目标特定后端的一组可重用组件。两个最重要的接口(TargetMachine和DataLayout)是唯一需要为后端定义以适应LLVM系统的接口,但如果要使用可重用代码生成器组件,则必须定义其他接口。

这种设计有两个重要的含义。首先,LLVM可以支持完全非传统的代码生成目标。例如,C后端不需要寄存器分配、指令选择或系统提供的任何其他标准组件。因此,只实现这两个接口,并做自己的事情。注意,自LLVM 3.1发布以来,C后端已从主干中删除。类似这样的代码生成器的另一个示例是一个(纯假设的)后端,它将LLVM转换为GCC RTL形式,并使用GCC为目标发出机器代码。

这种设计还意味着可以在LLVM系统中设计和实现完全不同的代码生成器,而不使用任何内置组件。根本不建议这样做,但对于根本不符合LLVM机器描述模型(例如FPGA)的不同目标,可能需要这样做。

代码生成器的高级设计

LLVM目标独立代码生成器旨在支持基于寄存器的标准微处理器的高效和高质量代码生成。此模型中的代码生成分为以下阶段:

指令选择—此阶段确定在目标指令集中表达输入LLVM代码的有效方式。此阶段为目标指令集中的程序生成初始代码,然后使用SSA形式的虚拟寄存器和物理寄存器,这些寄存器表示由于目标约束或调用约定而需要的任何寄存器分配。此步骤将LLVM代码转换为目标指令的DAG。

调度和形成-此阶段获取指令选择阶段生成的目标指令的DAG,确定指令的顺序,然后将指令作为具有该顺序的MachineInstrs发出。注意,在指令选择部分对此进行了描述,因为在SelectionDAG上运行。

基于SSA的机器C

%r3 = add %i1, %i2

此外,如果第一个操作数是def,则创建仅def是第一个操作的指令更容易。

使用MachineInstrBuilder.h函数

机器指令是通过使用位于include/llvm/CodeGen/MachineInstrBuilder.h文件中的BuildMI函数创建的。BuildMI函数使构建任意机器指令变得容易。BuildMI函数的用法如下:

// Create a 'DestReg = mov 42' (rendered in X86 assembly as 'mov DestReg, 42')
// instruction and insert it at the end of the given MachineBasicBlock.
const TargetInstrInfo &TII = ...
MachineBasicBlock &MBB = ...
DebugLoc DL;
MachineInstr *MI = BuildMI(MBB, DL, TII.get(X86::MOV32ri), DestReg).addImm(42);
 
// Create the same instr, but insert it before a specified iterator point.
MachineBasicBlock::iterator MBBI = ...
BuildMI(MBB, MBBI, DL, TII.get(X86::MOV32ri), DestReg).addImm(42);
 
// Create a 'cmp Reg, 0' instruction, no destination reg.
MI = BuildMI(MBB, DL, TII.get(X86::CMP32ri8)).addReg(Reg).addImm(42);
 
// Create an 'sahf' instruction which takes no operands and stores nothing.
MI = BuildMI(MBB, DL, TII.get(X86::SAHF));
 
// Create a self looping branch instruction.
BuildMI(MBB, DL, TII.get(X86::JNE)).addMBB(&MBB);

如果需要添加定义操作数(可选目标寄存器除外),则必须将其显式标记为:

固定(预分配)寄存器

代码生成器需要注意的一个重要问题是固定寄存器的存在。特别是,在指令流中,寄存器分配器必须安排特定值在特定寄存器中的位置。这可能是由于指令集的限制(例如,X86只能与EAX/EDX寄存器进行32位除法),或外部因素(如调用约定)。在任何情况下,指令选择器都应该发出代码,在需要时将虚拟寄存器复制到物理寄存器中或从物理寄存器中复制出来。

例如,考虑这个简单的LLVM示例:

define i32 @test(i32 %X, i32 %Y) {
  %Z = sdiv i32 %X, %Y
  ret i32 %Z
}

X86指令选择器可能会为div和ret生成以下机器代码:

;; Start of div
%EAX = mov %reg1024           ;; Copy X (in reg1024) into EAX
%reg1027 = sar %reg1024, 31
%EDX = mov %reg1027           ;; Sign extend X into EDX
idiv %reg1025                 ;; Divide by Y (in reg1025)
%reg1026 = mov %EAX           ;; Read the result (Z) out of EAX
 
;; Start of ret
%EAX = mov %reg1026           ;; 32-bit return value goes in EAX
ret

在代码生成结束时,寄存器分配器将合并寄存器并删除生成的标识移动,生成以下代码:

;; X is in EAX, Y is in ECX
mov %EAX, %EDX
sar %EDX, 31
idiv %ECX
ret

这种方法非常通用(如果它可以处理X86体系结构,那么它可以处理任何事情!),并且允许在指令选择器中隔离关于指令流的所有目标特定知识。请注意,物理寄存器的生存期应该很短,以便生成好的代码,并且所有物理寄存器在进入和退出基本块时(在寄存器分配之前)都被认为是死的。因此,如果需要一个值跨越基本块边界,必须存在于虚拟寄存器中。

调用已损坏的寄存器

一些机器指令,如调用,会大量物理寄存器。可以使用MO_RegisterMask操作数,而不是为所有操作数添加<def,dead>操作数。寄存器掩码操作数保存一个保留寄存器的位掩码,其他所有内容都被视为被指令清除。

SSA形式的机器代码

MachineInstr最初以SSA形式选择,并以SSA格式维护,直到发生寄存器分配。在大多数情况下,这很简单,因为LLVM已经是SSA形式;LLVM PHI节点变成机器代码PHI节点,虚拟寄存器只能有一个定义。

寄存器分配后,机器代码不再是SSA形式,因为代码中没有剩余的虚拟寄存器。

MachineBasicBlock类

MachineBasicBlock类包含机器指令列表(MachineInstr实例)。大致对应于输入到指令选择器的LLVM代码,但可以有一对多映射(即一个LLVM基本块可以映射到多个机器基本块)。MachineBasicBlock类有一个“getBasicBlock”方法,返回LLVM基本块。

MachineFunction类

MachineFunction类包含机器基本块(MachineBasicBlock实例)的列表。它与输入到指令选择器的LLVM函数一一对应。除了基本块列表之外,MachineFunction还包含MachineConstantPool、MachineFrameInfo、MachineFunctionInfo和MachineRegisterInfo。有关详细信息,请参阅include/llvm/CodeGen/MachineFunction.h。

MachineInstr捆绑

LLVM代码生成器可以将指令序列建模为MachineInstr包。MI包可以对包含任意数量的并行指令的VLIW组/包进行建模。还可以用于对无法合法分离的指令(可能具有数据依赖性)的顺序列表进行建模(例如ARM Thumb2 It块)。

从概念上讲,MI束是一个MI,其中嵌套了许多其他MI:

--------------
|   Bundle   | ---------
--------------          \
       |           ----------------
       |           |      MI      |
       |           ----------------
       |                   |
       |           ----------------
       |           |      MI      |
       |           ----------------
       |                   |
       |           ----------------
       |           |      MI      |
       |           ----------------
       |
--------------
|   Bundle   | --------
--------------         \
       |           ----------------
       |           |      MI      |
       |           ----------------
       |                   |
       |           ----------------
       |           |      MI      |
       |           ----------------
       |                   |
       |                  ...
       |
--------------
|   Bundle   | --------
--------------         \
       |
      ...

选择DAG选择阶段

Select阶段是用于指令选择的目标特定代码的主体。此阶段将合法的SelectionDAG作为输入,模式将目标支持的指令与此DAG匹配,并生成目标代码的新DAG。例如,考虑以下LLVM片段:

%t1 = fadd float %W, %X
%t2 = fmul float %t1, %Y
%t3 = fadd float %t2, %Z

此LLVM代码对应于一个SelectionDAG,基本上如下所示:

如果目标支持浮点乘法和加法(FMA)操作,则可以将其中一个加法与乘法合并。例如,在PowerPC上,指令选择器的输出可能看起来像以下DAG:

(FMADDS (FADDS W, X), Y, Z)

FMADDS指令是一个三进制指令,将前两个操作数相乘,并将第三个操作数相加(作为单精度浮点数)。FADDS指令是一个简单的二进制单精度加法指令。为了执行此模式匹配,PowerPC后端包括以下指令定义:

def FMADDS : AForm_1<59, 29,
                    (ops F4RC:$FRT, F4RC:$FRA, F4RC:$FRC, F4RC:$FRB),
                    "fmadds $FRT, $FRA, $FRC, $FRB",
                    [(set F4RC:$FRT, (fadd (fmul F4RC:$FRA, F4RC:$FRC),
                                           F4RC:$FRB))]>;
def FADDS : AForm_2<59, 21,
                    (ops F4RC:$FRT, F4RC:$FRA, F4RC:$FRB),
                    "fadds $FRT, $FRA, $FRB",
                    [(set F4RC:$FRT, (fadd F4RC:$FRA, F4RC:$FRB))]>;

除了指令,目标还可以使用“Pat”类指定映射到一个或多个指令的任意模式。例如,PowerPC无法在一条指令中将任意整数立即数加载到寄存器中。要告诉tblgen如何做到这一点,定义了:

// Arbitrary immediate support.  Implement in terms of LIS/ORI.
def : Pat<(i32 imm:$imm),
          (ORI (LIS (HI16 imm:$imm)), (LO16 imm:$imm))>;

当使用“Pat”类将模式映射到具有一个或多个复杂操作数的指令(例如X86寻址模式)时,该模式可以使用ComplexPattern将操作数指定为一个整体,也可以单独指定复杂操作数组成部分。后者由PowerPC后端完成,例如用于预增量指令:

def STWU  : DForm_1<37, (outs ptr_rc:$ea_res), (ins GPRC:$rS, memri:$dst),
                "stwu $rS, $dst", LdStStoreUpd, []>,
                RegConstraint<"$dst.reg = $ea_res">, NoEncode<"$ea_res">;
 
def : Pat<(pre_store GPRC:$rS, ptr_rc:$ptrreg, iaddroff:$ptroff),
          (STWU GPRC:$rS, iaddroff:$ptroff, ptr_rc:$ptrreg)>;

LLVM中的物理寄存器按寄存器类分组。同一寄存器类中的元素在功能上是等效的,可以互换使用。每个虚拟寄存器只能映射到特定类的物理寄存器。例如,在X86体系结构中,某些虚拟机只能分配给8位寄存器。寄存器类由TargetRegisterClass对象描述。要发现虚拟寄存器是否与给定的物理寄存器兼容,可以使用以下代码:

bool RegMapping_Fer::compatible_class(MachineFunction &mf,
                                      unsigned v_reg,
                                      unsigned p_reg) {
  assert(TargetRegisterInfo::isPhysicalRegister(p_reg) &&
         "Target register must be physical");
  const TargetRegisterClass *trc = mf.getRegInfo().getRegClass(v_reg);
  return trc->contains(p_reg);
}

虚拟寄存器也用整数表示。与物理寄存器相反,不同的虚拟寄存器从不共享相同的数字。物理寄存器是在TargetRegisterInfo.td文件中静态定义的,应用程序开发人员无法创建,而虚拟寄存器则不是这样。为了创建新的虚拟寄存器,请使用方法MachineRegisterInfo::createVirtualRegister()。此方法将返回一个新的虚拟寄存器。使用IndexedMap<Foo,VirtReg2IndexFunctor>保存每个虚拟寄存器的信息。如果需要枚举所有虚拟寄存器,请使用函数TargetRegisterInfo::index2VirtualReg()查找虚拟寄存器编号:

for (unsigned i = 0, e = MRI->getNumVirtRegs(); i != e; ++i) {
  unsigned VirtReg = TargetRegisterInfo::index2VirtReg(i);
  stuff(VirtReg);
}

为了产生正确的代码,LLVM必须将表示两个地址指令的三个地址指令转换为真正的两个地址的指令。LLVM为此特定目的提供传递TwoAddressInstructionPass。它必须在寄存器分配之前运行。执行后,生成的代码可能不再是SSA形式。例如,当一条指令(如%a=ADD%b%c)转换为两条指令(例如:

%a = MOVE %b
%a = ADD %a %c

例子:

调用llc-tailcallpt test.ll。

declare fastcc i32 @tailcallee(i32 inreg %a1, i32 inreg %a2, i32 %a3, i32 %a4)
 
define fastcc i32 @tailcaller(i32 %in1, i32 %in2) {
  %l1 = add i32 %in1, %in2
  %tmp = tail call fastcc i32 @tailcallee(i32 inreg %in1, i32 inreg %in2, i32 %in1, i32 %l1)
  ret i32 %tmp
}

Example:

declare i32 @bar(i32, i32)
 
define i32 @foo(i32 %a, i32 %b, i32 %c) {
entry:
  %0 = tail call i32 @bar(i32 %a, i32 %b)
  ret i32 %0
}

支持X86目标三元组

以下是X86后端支持的已知目标三元组。这并不是一个详尽的列表,添加人们测试过的内容会很有用。

  • i686-pc-linux-gnu — Linux
  • i386-unknown-freebsd5.3 — FreeBSD 5.3
  • i686-pc-cygwin — Cygwin on Win32
  • i686-pc-mingw32 — MingW on Win32
  • i386-pc-mingw32msvc — MingW crosscompiler on Linux
  • i686-apple-darwin* — Apple Darwin on X86
  • x86_64-unknown-linux-gnu — Linux

支持X86调用约定

后端已知以下特定于目标的调用约定:

  • x86_StdCall — stdcall calling convention seen on Microsoft Windows platform (CC ID = 64).
  • x86_FastCall — fastcall calling convention seen on Microsoft Windows platform (CC ID = 65).
  • x86_ThisCall — Similar to X86_StdCall. Passes first argument in ECX, others via stack. Callee is responsible for stack cleaning. This convention is used by MSVC by default for methods in its ABI (CC ID = 70).
·        SegmentReg: Base + [1,2,4,8] * IndexReg + Disp32
·        Index:        0     |    1        2       3           4          5
·        Meaning:   DestReg, | BaseReg,  Scale, IndexReg, Displacement Segment
·        OperandTy: VirtReg, | VirtReg, UnsImm, VirtReg,   SignExtImm  PhysReg

 

ADD8rr      -> add, 8-bit register, 8-bit register
IMUL16rmi   -> imul, 16-bit register, 16-bit memory, 16-bit immediate
IMUL16rmi8  -> imul, 16-bit register, 16-bit memory, 8-bit immediate
MOVSX32rm16 -> movsx, 32-bit register, 16-bit memory

 

R0        return value from in-kernel functions; exit value for eBPF program
R1 - R5   function call arguments to in-kernel functions
R6 - R9   callee-saved registers preserved by in-kernel functions
R10       stack frame pointer (read only)

 

+----------------+--------+--------------------+
|   4 bits       |  1 bit |   3 bits           |
| operation code | source | instruction class  |
+----------------+--------+--------------------+
(MSB)                                      (LSB)

 

BPF_LD     0x0
BPF_LDX    0x1
BPF_ST     0x2
BPF_STX    0x3
BPF_ALU    0x4
BPF_JMP    0x5
(unused)   0x6
BPF_ALU64  0x7

 

BPF_X     0x1  use src_reg register as source operand
BPF_K     0x0  use 32 bit immediate as source operand

 

BPF_ADD   0x0  add
BPF_SUB   0x1  subtract
BPF_MUL   0x2  multiply
BPF_DIV   0x3  divide
BPF_OR    0x4  bitwise logical OR
BPF_AND   0x5  bitwise logical AND
BPF_LSH   0x6  left shift
BPF_RSH   0x7  right shift (zero extended)
BPF_NEG   0x8  arithmetic negation
BPF_MOD   0x9  modulo
BPF_XOR   0xa  bitwise logical XOR
BPF_MOV   0xb  move register to register
BPF_ARSH  0xc  right shift (sign extended)
BPF_END   0xd  endianness conversion

 

BPF_JA    0x0  unconditional jump
BPF_JEQ   0x1  jump ==
BPF_JGT   0x2  jump >
BPF_JGE   0x3  jump >=
BPF_JSET  0x4  jump if (DST & SRC)
BPF_JNE   0x5  jump !=
BPF_JSGT  0x6  jump signed >
BPF_JSGE  0x7  jump signed >=
BPF_CALL  0x8  function call
BPF_EXIT  0x9  function return

 

+--------+--------+-------------------+
| 3 bits | 2 bits |   3 bits          |
|  mode  |  size  | instruction class |
+--------+--------+-------------------+
(MSB)                             (LSB)

 

BPF_W       0x0  word
BPF_H       0x1  half word
BPF_B       0x2  byte
BPF_DW      0x3  double word

 

BPF_IMM     0x0  immediate
BPF_ABS     0x1  used to access packet data
BPF_IND     0x2  used to access packet data
BPF_MEM     0x3  memory
(reserved)  0x4
(reserved)  0x5
BPF_XADD    0x6  exclusive add

 

R0 - rax
R1 - rdi
R2 - rsi
R3 - rdx
R4 - rcx
R5 - r8
R6 - rbx
R7 - r13
R8 - r14
R9 - r15
R10 - rbp

 

参考文献了解

https://llvm.org/docs/CodeGenerator.html

posted @ 2023-01-25 04:46  吴建明wujianming  阅读(260)  评论(0编辑  收藏  举报