机器 IR (MIR) 格式参考手册

警告

此文档尚未完善。

简介

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

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

概述

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

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

MIR 测试指南

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

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

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

测试单个代码生成过程

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

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

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

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

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

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

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

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

简化 MIR 文件

来自-stop-after/-stop-before的 MIR 代码非常冗长;简化后的测试更易于访问且更具前瞻性

  • 使用 llc 的-simplify-mir选项。

  • 机器函数属性通常具有默认值,或者测试在使用默认值时也能正常工作。典型的候选对象包括:alignment:exposesReturnsTwicelegalizedregBankSelectedselected。如果函数中没有特殊的帧使用,则整个frameInfo部分通常是不必要的。tracksRegLiveness另一方面,对于某些关心块活跃输入列表的过程来说,通常是必要的。

  • (全局)liveins:列表通常仅对早期指令选择过程感兴趣,并且在测试后期过程时可以删除。另一方面,如果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 部分仅包含虚拟函数(如上所示),则可以删除整个 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”命名寄存器表示,尽管建议使用前一种语法。

机器操作数

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

立即数操作数

立即数机器操作数是无类型的 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,它使用ConstantInt 中的APInt(LLVM 的任意精度整数)存储立即数。

寄存器操作数

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

[<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 表示。

标志

内部值

含义

隐式

RegState::Implicit

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

隐式定义

RegState::ImplicitDefine

隐式def

定义

RegState::Define

寄存器定义。

RegState::Dead

未使用的定义。

杀死

RegState::Kill

寄存器的最后一次使用。

未定义

RegState::Undef

寄存器的值无关紧要。

内部

RegState::InternalRead

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

提前覆盖

RegState::EarlyClobber

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

调试使用

RegState::Debug

寄存器的“使用”用于调试目的。

可重命名

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> 为真或假。

示例

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' ]

外部符号操作数

外部符号操作数使用带 & 前缀的标识符表示。标识符用“""”括起来,如果其中包含任何特殊的不可打印字符,则进行转义。

示例

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 操作数包含一个通用 intrinsic ID 或一个目标特定的 ID。

returnaddress intrinsic 的语法如下

$x0 = COPY intrinsic(@llvm.returnaddress)

谓词操作数

谓词操作数包含来自 CmpInst::Predicate 的 IR 谓词,例如 ICMP_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 对象表示。

固定变量位置

有多种方法可以指定变量位置。最简单的方法是描述永久位于堆栈上的变量。在机器函数的堆栈或 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 作者指南 也可能有用。