Machine IR (MIR) 格式参考手册

警告

这是一个正在进行的工作。

简介

本文档是 Machine IR (MIR) 序列化格式的参考手册。MIR 是一种人类可读的序列化格式,用于表示 LLVM 的 机器特定的中间表示

MIR 序列化格式旨在用于测试 LLVM 中的代码生成 Pass。

概述

MIR 序列化格式使用 YAML 容器。YAML 是一种标准数据序列化语言,完整的 YAML 语言规范可以在 yaml.org 上阅读。

一个 MIR 文件被拆分成一系列 YAML 文档。第一个文档可以包含一个可选的嵌入式 LLVM IR 模块,其余文档包含序列化的机器函数。

MIR 测试指南

您可以使用 MIR 格式通过两种不同的方式进行测试

  • 您可以编写 MIR 测试,使用 llc 中的 -run-pass 选项调用单个代码生成 Pass。

  • 您可以将 llc 的 -stop-after 选项与现有或新的 LLVM 汇编测试一起使用,并检查特定代码生成 Pass 的 MIR 输出。

测试单个代码生成 Pass

llc 中的 -run-pass 选项允许您创建 MIR 测试,仅调用单个代码生成 Pass。当使用此选项时,llc 将解析输入 MIR 文件,运行指定的代码生成 Pass,并输出生成的 MIR 代码。

您可以使用 llc 中的 -stop-after-stop-before 选项生成测试的输入 MIR 文件。例如,如果您想为寄存器分配后伪指令展开 Pass 编写测试,您可以在 -stop-after 选项中指定机器副本传播 Pass,因为它在我们尝试测试的 Pass 之前运行

llc -stop-after=machine-cp bug-trigger.ll -o test.mir

如果同一个 Pass 运行多次,则可以在名称后用逗号包含运行索引。

llc -stop-after=dead-mi-elimination,1 bug-trigger.ll -o test.mir

生成输入 MIR 文件后,您必须添加一个运行行,该运行行使用 -run-pass 选项。为了在 X86-64 上测试寄存器分配后伪指令展开 Pass,可以使用如下所示的运行行

# RUN: llc -o - %s -mtriple=x86_64-- -run-pass=postrapseudos | FileCheck %s

MIR 文件是目标相关的,因此它们必须放置在目标特定的测试目录 (lib/CodeGen/TARGETNAME) 中。它们还需要在运行行或嵌入式 LLVM IR 模块中指定目标三元组或目标架构。

简化 MIR 文件

-stop-after/-stop-before 输出的 MIR 代码非常冗长;当简化时,测试更容易访问且面向未来

  • -simplify-mir 选项与 llc 一起使用。

  • 机器函数属性通常具有默认值,或者测试在默认值下也能很好地工作。典型的候选者是:alignment:exposesReturnsTwicelegalizedregBankSelectedselectedframeInfo 整个部分通常是不必要的,如果函数中没有特殊的帧使用情况。tracksRegLiveness 另一方面,对于某些关心块活跃输入列表的 Pass 来说,通常是必要的。

  • (全局)liveins: 列表通常仅对早期的指令选择 Pass 感兴趣,并且在测试后面的 Pass 时可以删除。另一方面,如果 tracksRegLiveness 为真,则每个块的 liveins: 是必要的。

  • 如果测试不依赖于分支概率数据,则可以删除块 successors: 列表中的分支概率数据。示例:successors: %bb.1(0x40000000), %bb.2(0x40000000) 可以替换为 successors: %bb.1, %bb.2

  • MIR 代码包含一个完整的 IR 模块。这是必要的,因为 MIR 中没有全局变量、对外部函数的引用、函数属性、元数据、调试信息的等价物。相反,一些 MIR 数据引用了 IR 构造。如果测试不依赖于它们,您通常可以删除它们。

  • 别名分析是在 IR 值上执行的。这些值通过 MIR 中的内存操作数引用。示例::: (load 8 from %ir.foobar, !alias.scope !9)。如果测试不依赖于(良好的)别名分析,则可以删除引用::: (load 8)

  • MIR 块可以引用 IR 块以进行调试打印、配置文件信息或调试位置。示例:MIR 中的 bb.42.myblock 引用了 IR 块 myblock。通常可以删除 .myblock 引用,而只需使用 bb.42

  • 如果没有内存操作数或块引用 IR,则 IR 函数可以替换为无参数的虚拟函数,如 define @func() { ret void }

  • 如果 MIR 文件的整个 IR 部分仅包含虚拟函数(见上文),则可以删除它。在这种情况下,.mir 加载器将自动创建 IR 函数。

局限性

目前,MIR 格式在可以序列化的状态方面存在一些局限性

  • 目标特定的 MachineFunctionInfo 子类中的目标特定状态目前未序列化。

  • MachineConstantPoolValue 子类(在 ARM 和 SystemZ 后端)目前未序列化。

  • MCSymbol 机器操作数不支持临时或本地符号。

  • MachineModuleInfo 中的许多状态未序列化 - 目前仅序列化来自 MMI 的 CFI 指令和变量调试信息。

这些局限性对您可以使用 MIR 格式测试的内容施加了限制。目前,想要测试某些行为的测试,这些行为依赖于临时或本地 MCSymbol 操作数的状态或 MMI 中的异常处理状态,不能使用 MIR 格式。同样,测试某些行为的测试,这些行为依赖于目标特定的 MachineFunctionInfoMachineConstantPoolValue 子类的状态,目前也不能使用 MIR 格式。

高层结构

嵌入式模块

当第一个 YAML 文档包含一个 YAML 块字面量字符串 时,MIR 解析器会将此字符串视为表示嵌入式 LLVM IR 模块的 LLVM 汇编语言字符串。这是一个包含 LLVM 模块的 YAML 文档示例

define i32 @inc(ptr %x) {
entry:
  %0 = load i32, ptr %x
  %1 = add i32 %0, 1
  store i32 %1, ptr %x
  ret i32 %1
}

机器函数

其余的 YAML 文档包含机器函数。这是一个这样的 YAML 文档的示例

---
name:            inc
tracksRegLiveness: true
liveins:
  - { reg: '$rdi' }
callSites:
  - { bb: 0, offset: 3, fwdArgRegs:
      - { arg: 0, reg: '$edi' } }
body: |
  bb.0.entry:
    liveins: $rdi

    $eax = MOV32rm $rdi, 1, _, 0, _
    $eax = INC32r killed $eax, implicit-def dead $eflags
    MOV32mr killed $rdi, 1, _, 0, _, $eax
    CALL64pcrel32 @foo <regmask...>
    RETQ $eax
...

上面的文档由属性组成,这些属性表示机器函数中的各种属性和数据结构。

属性 name 是必需的,其值应与此机器函数所基于的函数的名称相同。

属性 body 是一个 YAML 块字面量字符串。它的值表示函数机器基本块及其机器指令。

属性 callSites 是调用点信息的表示,它跟踪调用指令和用于传输调用参数的寄存器。

机器指令格式参考

机器基本块及其指令使用自定义的人类可读序列化语言表示。此语言用于对应于机器函数主体的 YAML 块字面量字符串 中。

使用此语言的源字符串包含机器基本块的列表,这些块在下面的部分中描述。

机器基本块

机器基本块在单个块定义源构造中定义,该构造包含块的 ID。下面的示例定义了两个 ID 为零和一的块

bb.0:
  <instructions>
bb.1:
  <instructions>

机器基本块也可以有一个名称。它应该在块定义中的 ID 之后指定

bb.0.entry:       ; This block's name is "entry"
   <instructions>

块的名称应与此机器块所基于的 IR 块的名称相同。

块引用

机器基本块通过其 ID 号标识。单个块使用以下语法引用

%bb.<id>

示例

%bb.0

也支持以下语法,但前一种语法是块引用的首选语法

%bb.<id>[.<name>]

示例

%bb.1.then

后继块

机器基本块的后继块必须在任何指令之前指定

bb.0.entry:
  successors: %bb.1.then, %bb.2.else
  <instructions>
bb.1.then:
  <instructions>
bb.2.else:
  <instructions>

分支权重可以在后继块之后的括号中指定。下面的示例定义了一个具有两个后继块的块,分支权重分别为 32 和 16

bb.0.entry:
  successors: %bb.1.then(32), %bb.2.else(16)

活跃输入寄存器

机器基本块的活跃输入寄存器必须在任何指令之前指定

bb.0.entry:
  liveins: $edi, $esi

活跃输入寄存器和后继块的列表可以为空。该语言还允许多个活跃输入寄存器和后继块列表 - 它们由解析器组合成一个列表。

杂项属性

属性 IsAddressTakenIsLandingPadIsInlineAsmBrIndirectTargetAlignment 可以在块定义后的括号中指定

bb.0.entry (address-taken):
  <instructions>
bb.2.else (align 4):
  <instructions>
bb.3(landing-pad, align 4):
  <instructions>
bb.4 (inlineasm-br-indirect-target):
  <instructions>

Alignment 以字节为单位指定,并且必须是 2 的幂。

机器指令

机器指令由名称、机器操作数指令标志 和机器内存操作数组成。

指令的名称通常在操作数之前指定。下面的示例显示了 X86 RETQ 指令的实例,该指令具有单个机器操作数

RETQ $eax

但是,如果机器指令具有一个或多个显式定义的寄存器操作数,则指令的名称必须在它们之后指定。下面的示例显示了 AArch64 LDPXpost 指令的实例,该指令具有三个定义的寄存器操作数

$sp, $fp, $lr = LDPXpost $sp, 2

指令名称使用目标 *InstrInfo.td 文件中的确切定义进行序列化,并且区分大小写。这意味着类似的指令名称(如 TSTritSTRi)表示不同的机器指令。

指令标志

标志 frame-setupframe-destroy 可以在指令名称之前指定

$fp = frame-setup ADDXri $sp, 0, 0
$x21, $x20 = frame-destroy LDPXi $sp

捆绑指令

捆绑指令的语法如下

BUNDLE implicit-def $r0, implicit-def $r1, implicit $r2 {
  $r0 = SOME_OP $r2
  $r1 = ANOTHER_OP internal $r0
}

第一个指令通常是捆绑头。 {} 之间的指令与第一个指令捆绑在一起。

寄存器

寄存器是机器指令序列化语言中的关键原语之一。它们主要用于 寄存器机器操作数,但它们也可以用于许多其他地方,例如 基本块的活跃输入列表

物理寄存器通过其名称和 ‘$’ 前缀符号标识。它们使用以下语法

$<name>

下面的示例显示了三个 X86 物理寄存器

$eax
$r15
$eflags

虚拟寄存器通过其 ID 号和 ‘%’ 符号标识。它们使用以下语法

%<id>

示例

%0

空寄存器使用下划线(‘_’)表示。它们也可以使用 ‘$noreg’ 命名寄存器表示,尽管前一种语法是首选语法。

机器操作数

有十八种不同类型的机器操作数,所有这些操作数都可以序列化。

立即数操作数

立即数机器操作数是无类型的 64 位有符号整数。下面的示例显示了 X86 MOV32ri 指令的实例,该指令具有一个立即数机器操作数 -42

$eax = MOV32ri -42

当机器指令具有以下操作码之一时,立即数操作数也用于表示子寄存器索引

  • EXTRACT_SUBREG

  • INSERT_SUBREG

  • REG_SEQUENCE

  • SUBREG_TO_REG

如果为真,则根据目标打印机器操作数。

例如

在 AArch64RegisterInfo.td 中

def sub_32 : SubRegIndex<32>;

如果第三个操作数是一个值为 15(目标相关值)的立即数,则根据指令的操作码和操作数的索引,该操作数将打印为 %subreg.sub_32

%1:gpr64 = SUBREG_TO_REG 0, %0, %subreg.sub_32

对于 > 64 位的整数,我们使用特殊的机器操作数 MO_CImmediate,它使用 APInt(LLVM 的任意精度整数)将立即数存储在 ConstantInt 中。

寄存器操作数

寄存器 原语用于表示寄存器机器操作数。寄存器操作数还可以具有可选的 寄存器标志子寄存器索引 和对绑定寄存器操作数的引用。寄存器操作数的完整语法如下所示

[<flags>] <register> [ :<subregister-idx-name> ] [ (tied-def <tied-op>) ]

此示例显示了 X86 XOR32rr 指令的实例,该指令具有 5 个带有不同寄存器标志的寄存器操作数

dead $eax = XOR32rr undef $eax, undef $eax, implicit-def dead $eflags, implicit-def $al
寄存器标志

下表显示了所有可能的寄存器标志以及相应的内部 llvm::RegState 表示

标志

内部值

含义

implicit

RegState::Implicit

未发出的寄存器(例如,进位或临时结果)。

implicit-def

RegState::ImplicitDefine

implicitdef

def

RegState::Define

寄存器定义。

dead

RegState::Dead

未使用的定义。

killed

RegState::Kill

寄存器的最后一次使用。

undef

RegState::Undef

寄存器的值无关紧要。

internal

RegState::InternalRead

寄存器读取在同一指令或捆绑包内定义的值。

early-clobber

RegState::EarlyClobber

寄存器定义发生在用途之前。

debug-use

RegState::Debug

寄存器 ‘use’ 用于调试目的。

renamable

RegState::Renamable

可以重命名的寄存器。

子寄存器索引

寄存器机器操作数可以通过使用子寄存器索引来引用寄存器的部分。下面的示例显示了 COPY 伪指令的实例,该指令使用 X86 sub_8bit 子寄存器索引将 32 位虚拟寄存器 0 的较低 8 位复制到 8 位虚拟寄存器 1

%1 = COPY %0:sub_8bit

子寄存器索引的名称是特定于目标的,并且通常在目标的 *RegisterInfo.td 文件中定义。

常量池索引

常量池索引 (CPI) 操作数使用其在函数的 MachineConstantPool 中的索引和偏移量打印。

例如,索引为 1 且偏移量为 8 的 CPI

%1:gr64 = MOV64ri %const.1 + 8

对于索引为 0 且偏移量为 -12 的 CPI

%1:gr64 = MOV64ri %const.0 - 12

常量池条目绑定到 LLVM IR Constant 或目标特定的 MachineConstantPoolValue。当序列化所有函数的常量时,使用以下格式

constants:
  - id:               <index>
    value:            <value>
    alignment:        <alignment>
    isTargetSpecific: <target-specific>
其中
  • <index> 是一个 32 位无符号整数;

  • <value> 是一个 LLVM IR 常量

  • <alignment> 是一个以字节为单位指定的 32 位无符号整数,并且必须是 2 的幂;

  • <target-specific> 是 true 或 false。

示例

constants:
  - id:               0
    value:            'double 3.250000e+00'
    alignment:        8
  - id:               1
    value:            'g-(LPC0+8)'
    alignment:        4
    isTargetSpecific: true

全局值操作数

全局值机器操作数引用 嵌入式 LLVM IR 模块 中的全局值。下面的示例显示了 X86 MOV64rm 指令的实例,该指令具有一个名为 G 的全局值操作数

$rax = MOV64rm $rip, 1, _, @G, _

命名的全局值使用带有 ‘@’ 前缀的标识符表示。如果标识符与正则表达式 [-a-zA-Z$._][-a-zA-Z$._0-9]* 不匹配,则必须引用此标识符。

未命名的全局值使用带有 ‘@’ 前缀的无符号数值表示,如以下示例所示:@0@989

目标相关的索引操作数

目标索引操作数是目标特定的索引和偏移量。目标特定的索引使用目标特定的名称和正或负偏移量打印。

例如,amdgpu-constdata-start 与 AMDGPU 后端中的索引 0 相关联。因此,如果我们有一个索引为 0 且偏移量为 8 的目标索引操作数

$sgpr2 = S_ADD_U32 _, target-index(amdgpu-constdata-start) + 8, implicit-def _, implicit-def _

跳转表索引操作数

索引为 0 的跳转表索引操作数打印如下

tBR_JTr killed $r0, %jump-table.0

机器跳转表条目包含 MachineBasicBlocks 列表。当序列化所有函数的跳转表条目时,使用以下格式

jumpTable:
  kind:             <kind>
  entries:
    - id:             <index>
      blocks:         [ <bbreference>, <bbreference>, ... ]

其中 <kind> 描述了跳转表的表示和发出方式(纯地址、重定位、PIC 等),每个 <index> 是一个 32 位无符号整数,blocks 包含 机器基本块引用 列表。

示例

jumpTable:
  kind:             inline
  entries:
    - id:             0
      blocks:         [ '%bb.3', '%bb.9', '%bb.4.d3' ]
    - id:             1
      blocks:         [ '%bb.7', '%bb.7', '%bb.4.d3', '%bb.5' ]

外部符号操作数

外部符号操作数使用带有 & 前缀的标识符表示。如果标识符中包含任何特殊的不可打印字符,则该标识符用 ““‘s 包围并转义。

示例

CALL64pcrel32 &__stack_chk_fail, csr_64, implicit $rsp, implicit-def $rsp

MCSymbol 操作数

MCSymbol 操作数保存指向 MCSymbol 的指针。有关此操作数在 MIR 中的局限性,请参见 局限性

语法是

EH_LABEL <mcsymbol Ltmp1>

调试指令引用操作数

调试指令引用操作数是一对索引,分别引用指令和该指令内的操作数;请参阅 指令引用位置

下面的示例使用了对指令 1、操作数 0 的引用

DBG_INSTR_REF !123, !DIExpression(DW_OP_LLVM_arg, 0), dbg-instr-ref(1, 0), debug-location !456

CFIIndex 操作数

CFI Index 操作数保存到每个函数的边表 MachineFunction::getFrameInstructions() 的索引,该边表引用 MachineFunction 中的所有帧指令。 CFI_INSTRUCTION 可能看起来包含多个操作数,但它唯一包含的操作数是 CFI Index。其他操作数由 MCCFIInstruction 对象跟踪。

语法是

CFI_INSTRUCTION offset $w30, -16

稍后可能会在 MC 层中发出为

.cfi_offset w30, -16

IntrinsicID 操作数

Intrinsic ID 操作数包含通用内在 ID 或目标特定的 ID。

returnaddress 内在函数的语法是

$x0 = COPY intrinsic(@llvm.returnaddress)

谓词操作数

谓词操作数包含来自 CmpInst::Predicate 的 IR 谓词,如 ICMP_EQ 等。

对于 int eq 谓词 ICMP_EQ,语法是

%2:gpr(s32) = G_ICMP intpred(eq), %0, %1

注释

机器操作数可以具有 C/C++ 样式的注释,这些注释用 /**/ 括起来,以提高例如立即数操作数的可读性。在下面的示例中,ARM 指令 EOR 和 BCC 以及立即数操作数 140 已用其条件代码 (CC) 定义进行注释,即 alwayseq 条件代码

dead renamable $r2, $cpsr = tEOR killed renamable $r2, renamable $r1, 14 /* CC::always */, $noreg
t2Bcc %bb.4, 0 /* CC:eq */, killed $cpsr

由于这些注释是注释,因此 MI 解析器会忽略它们。可以通过覆盖 InstrInfo 的 hook createMIROperandComment() 来添加或自定义注释。

调试信息构造

MIR 文件中的大多数调试信息都可以在嵌入式模块的元数据中找到。在机器函数中,该元数据通过各种构造引用,以描述源位置和变量位置。

源位置

每个 MIR 指令都可以选择性地在所有操作数和符号之后,但在内存操作数之前,具有对 DILocation 元数据节点的尾随引用

$rbp = MOV64rr $rdi, debug-location !12

源位置附件与 LLVM-IR 中的 !dbg 元数据附件同义。源位置附件的缺失将由机器指令中的空 DebugLoc 对象表示。

固定变量位置

指定变量位置有几种方法。最简单的方法是描述一个永久位于堆栈上的变量。在机器函数的 stack 或 fixedStack 属性中,提供了变量、作用域和任何限定位置修饰符

- { id: 0, name: offset.addr, offset: -24, size: 8, alignment: 8, stack-id: default,
 4  debug-info-variable: '!1', debug-info-expression: '!DIExpression()',
    debug-info-location: '!2' }

其中

  • debug-info-variable 标识一个 DILocalVariable 元数据节点,

  • debug-info-expression 为变量位置添加限定符,

  • debug-info-location 标识一个 DILocation 元数据节点。

这些元数据属性对应于 #dbg_declare IR 调试记录的操作数,请参阅源代码级调试文档。

可变变量位置

不总是位于堆栈上或位置会发生变化的变量使用 DBG_VALUE 元机器指令指定。它与 #dbg_value IR 记录同义,其写法为

DBG_VALUE $rax, $noreg, !123, !DIExpression(), debug-location !456

分别对应于以下操作数

  1. 标识一个机器位置,例如寄存器、立即数或帧索引,

  2. 如果是 $noreg,或者如果要在第一个操作数中添加额外的间接级别,则为立即数值零,

  3. 标识一个 DILocalVariable 元数据节点,

  4. 指定限定变量位置的表达式,可以是内联的,也可以作为元数据节点引用,

而源位置标识变量作用域的 DILocation。第二个操作数 (IsIndirect) 已弃用,将被删除。变量位置的所有其他限定符都应通过表达式元数据进行。

指令引用位置

此实验性功能旨在将变量的规范与变量获取该值的程序点分离。变量值的更改以与 DBG_VALUE 元指令相同的方式发生,但使用 DBG_INSTR_REF。变量值由指令编号和操作数编号对标识。考虑以下示例

$rbp = MOV64ri 0, debug-instr-number 1, debug-location !12
DBG_INSTR_REF !123, !DIExpression(DW_OP_LLVM_arg, 0), dbg-instr-ref(1, 0), debug-location !456

指令编号直接附加到机器指令,带有可选的 debug-instr-number 附件,在可选的 debug-location 附件之前。上述代码中 $rbp 中定义的值将由 <1, 0> 对标识。

上面 DBG_INSTR_REF 的第三个操作数记录了指令和操作数编号 <1, 0>,标识了 MOV64ri 定义的值。DBG_INSTR_REF 的前两个操作数与 DBG_VALUE_LIST 相同,并且 DBG_INSTR_REF 的位置以相同的方式记录变量获取指定值的位置。

有关如何使用这些构造的更多信息,请参见 指令引用调试信息。相关文档 使用 LLVM 进行源代码级调试如何更新调试信息:LLVM Pass 作者指南 也可能很有用。