本文是一篇译文,翻译自:
https://llvm.org/docs/CodeGenerator.html​llvm.org/docs/CodeGenerator.html
如有问题,敬请指出。
转载需注明出处,若需相关专业翻译服务,可联系我。


在高层设计中,LLVM 代码会翻译成由 MachineFunction、MachineBasicBlock 和 MachineInstr 实例组成的特定机器的表示。这种表示完全是目标无关的,以最抽象的方式描述指令:操作码和操作数。这种表示被设计用来同时支持机器码的 SSA 形式以及寄存器分配后的表示(即非 SSA 形式) 。

  1. LLVM 目标无关代码生成器 1:介绍
  2. LLVM 目标无关代码生成器 2:目标描述类
  3. LLVM 目标无关代码生成器 3:机器代码描述类
  4. LLVM 目标无关代码生成器 4:MC 层
  5. LLVM 目标无关代码生成器 5:目标无关代码生成算法
  6. LLVM 目标无关代码生成器 6:实现原生汇编器

3.1 MachineInstr 类

目标机器指令(Machine Instruction,LLVM 后端的一种中间表示,缩写为MI )通过 MachineInstr 的实例来描述。这个类以一种非常抽象的方式描述机器指令,特别是,它仅仅管理一个操作码和一些操作数。

操作码是一个简单的无符号整型数,只在特定后端下才有效。所有的指令都是在 *InstrInfo.td 文件中定义的,操作码的枚举值仅仅是依据这份描述自动生成。MachineInstr 类没有任何有关于指令意义的信息(比如这条指令的意义是什么),必须依赖于 TargetInstrInfo 类来了解。

操作数可以有多种不同类型,比如寄存器引用、常量值、basic block 引用等。另外,操作数应该被标记为 def 或 use (仅寄存器可以标记为 def )。

按照惯例,LLVM 代码生成器将指令操作数排了序,从而让寄存器先 def 再 use,即使在那些与该排序不符的架构上也是如此。比如,在 SPARC 架构中的加法指令为:add %i1, %i2, %i3,其中是将 %i1 和 %i2 相加,将结果放到 %i3 中,在 LLVM 的代码生成器中,操作数的顺序却是 %i3, %i1, %i2,依然是将目的操作数放在前边。

将目的操作数放在前边有一些优势,比如,在打印 debug 信息时,可以这么输出指令:

1
%r3 = add %i1, %i2

另外,如果第一个操作数是一个 def 操作数,这便很容易创建那些 def 作为第一个操作数的指令。

3.1.1 使用 MachineInstrBuilder.h 函数

机器指令(MI)由 BuildMI 函数来创建,这个函数定义在 include/llvm/CodeGen/MachineInstrBuilder.h 文件中。BuildMI 函数可以很容易的创建任意 MI。使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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);

如果需要新增操作数(非可选的目的寄存器),你必须显式的提出:

1
MI.addReg(Reg, RegState::Define);

3.1.2 固定寄存器(预分配寄存器)

一个很重要的议题,代码生成器需要注意那些固定寄存器。特别是在指令流中,寄存器分配器必须针对一些指令将特定的寄存器安排到特定的位置上。这可能发生于一些有限制的指令(比如,X86 中只允许 EAX / EDX 这两个寄存器完成 32 位除法操作)或者一些扩展功能,如调用约定。任何情况下,指令选择器都应该在指令需要时,发出将虚拟寄存器传入或传出物理寄存器的操作代码。

比如,在如下这段 LLVM 代码中:

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

X86 的指令选择器可能会生成以下代码:

1
2
3
4
5
6
7
8
9
10
;; Start of div
%EAX = mov %reg1024 ;; Copy X (in reg1024) into EAX
%reg1027 = sar %reg1024, 31 ;; Sign extend X
%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

寄存器分配器会合并寄存器,并删除冗余寄存器并产生以下代码:

1
2
3
4
5
;; X is in EAX, Y is in ECX
mov %EAX, %EDX
sar %EDX, 31
idiv %ECX
ret

这种操作非常通用,它允许任何目标平台实现这种将指令流中信息在指令选择器中独立出来的操作。需要注意对于物理寄存器来说,应当尽可能缩短其生命周期,并且,在进入和退出一个 basic blocks 时(寄存器分配前),所有物理寄存器都会被假设是 dead 状态。因此,如果你需要一个能跨越 basic block 边界的值,你应该使用虚拟寄存器。

3.1.3 Call-clobbered 寄存器

对于一些机器指令,如 call,会影响到(clobber)大量的物理寄存器,我们使用 MO_RegisterMask 操作数来替代增加 <def, dead> 操作数。通过寄存器掩码来维持保留寄存器的状态,任何其他的寄存器都会被认为是会受到指令影响的。

3.1.4 SSA 形式下的机器码

MI 代码在指令选择时被创建为 SSA 形式,并直到寄存器分配发生。大多数情况下,这种形式都是非常简单的,因为 LLVM 代码也是 SSA 形式,LLVM 的 PHI 节点(译注:维持 SSA 形式的重要节点)会被转换为 MI 代码的 PHI 节点,虚拟寄存器也只需要通过简单的方法来描述。

在寄存器分配之后,MI 代码不再是 SSA 形式,因为虚拟寄存器不存在了。

3.2 MachineBasicBlock 类

MachineBasicBlock 类包含有一系列的 MI ( MachineInstr 的实例 ),它大致等效于输入到指令选择器的 LLVM 代码,但也可能存在一对多的映射(一个 LLVM basic block 对应多个 machine basic block )。MachineBasicBlock 类中有个 getBasicBlock 方法,可以返回对应的 LLVM basic block。

3.3 MachineFunction 类

MachineFunction 类包含有一系列的 machine basic block ( MachineBasicBlock 的实例 ),它与 输入指令选择器的 LLVM 函数是一对一的映射。除了一系列的 machine basic block 以外,它还包括一个 MachineConstantPool,一个 MachineFrameInfo,一个 MachineFunctionInfo,和一个 MachineRegisterInfo。这部分代码可参见 include/llvm/CodeGen/MachineFunction.h

3.4 MachineInstr Bundles

LLVM 代码生成器可以将一些指令序列看做是 MachineInstr bundles。 一个 MI bundle 可以模型化 VLIW 中包含能够并行化执行的 组/包 结构。它也可以被用来模型化无法合法分离的顺序指令序列(可能带有数据依赖)。

概念上讲,一个 MI bundle 就是将一个 MI 和其他一些 MIs 打包在一起:

1
2
3
4
5
6
7
8
9
10
11
Bundle ---
| \
| MI
| |
| MI
| |
| MI
|
Bundle ---
| \
... ...

MI bundle 可以不改变 MachineBasicBlock 和 MachineInstr 的物理表示,所有的 MIs (包括顶层的 MI 和其他打包的 MIs)通过序列化的列表存储。被打包的 MIs 会被标记为 bundled,每个 bundle 最顶层的 MI 用于表示为 bundle 的开始。将已打包的 MI 和单独的 MI (未参与打包)混合是合法的操作。

译注:下文部分将 bundle 译为打包。通常也将一个 bundle 中的所有 MIs 称为该 bundle 的operands。不同后端可能会在这里有不同的实现,但在 LLVM 看来,一个 bundle 会被当做一个 MI 一样的行为来处理。

MachineInstr 的 pass 应该将 MI bundle 当做一个单独的单元来处理。其成员方法已经知道如何处理 bundle 和 bundle 内部的 MIs。 MachineBasicBlock 迭代器也可以一次跳过整个 MI bundle (将 bundle 看做一个处理单元的效果),不过,MachineBasicBlock 也提供了一个额外的迭代器 instr_iterator,它能够忽视 bundle,对每个 MI 进行检查。bundle 中顶层的 MI 指令中必须有正确的寄存器 MachineOperand,它表示整个 bundle 中累计的输入和输出。

对支持 VLIW 架构机器中 MI 的打包操作需要在寄存器分配时完成。更明确点说,决定_哪些 MI 需要被 bundle 的工作_必须在 SSA 形式结束之后便完成(比如 two-address pass, phi 节点消除,copy coalescing ),最终的 bundle 任务应该在虚拟寄存器被物理寄存器重写之后结束(为 bundle 增加 MI,输入输出寄存器 MachineOperands)。这样就不需要为 bundle 指令添加虚拟寄存器的操作数了(避免了 use 和 def 列表翻倍 )。bundle 可能会以虚拟寄存器和 SSA 形式存在,但这并不常见。