Inline assembly

内联汇编

behavior-considered-undefined.md
commit: 8eda943339b7033205604386472d3e6e1dfa28ed
本章译文最后维护日期:2024-02-03

Rust 通过 asm!global_asm! 这两个宏来提供了对内联汇编的支持。 它可用于在编译器生成的汇编程序输出中嵌入手写的汇编程序。

目前对内联汇编的支持在以下目标架构上是稳定的:

  • x86 和 x86-64
  • ARM
  • AArch64
  • RISC-V
  • LoongArch

如果在不支持的目标架构上使用 asm!,编译器会报错。

Example

示例

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

// 使用以为和相加运算来实现 x 乘6的效果
let mut x: u64 = 4;
unsafe {
    asm!(
        "mov {tmp}, {x}",
        "shl {tmp}, 1",
        "shl {x}, 2",
        "add {x}, {tmp}",
        x = inout(reg) x,
        tmp = out(reg) _,
    );
}
assert_eq!(x, 4 * 6);
}
}

Syntax

句法

下面的 ABNF语法规范指定了通用的内联汇编语法:

format_string := STRING_LITERAL / RAW_STRING_LITERAL
dir_spec := "in" / "out" / "lateout" / "inout" / "inlateout"
reg_spec := <register class> / "\"" <explicit register> "\""
operand_expr := expr / "_" / expr "=>" expr / expr "=>" "_"
reg_operand := [ident "="] dir_spec "(" reg_spec ")" operand_expr
clobber_abi := "clobber_abi(" <abi> *("," <abi>) [","] ")"
option := "pure" / "nomem" / "readonly" / "preserves_flags" / "noreturn" / "nostack" / "att_syntax" / "raw"
options := "options(" option *("," option) [","] ")"
operand := reg_operand / clobber_abi / options
asm := "asm!(" format_string *("," format_string) *("," operand) [","] ")"
global_asm := "global_asm!(" format_string *("," format_string) *("," operand) [","] ")"

Scope

作用域

内联汇编可以以下面两种方式来使用。

通过 asm!宏,汇编代码在函数作用域中被发射,且最终被集成到编译器生成的函数的汇编代码中。 此汇编代码必须遵守 严格规则以避免未定义行为。 注意,在某些情况下,编译器可能选择将汇编代码作为一个独立的函数发射,并生成对它的调用汇编。

通过 global_asm!宏,汇编代码在全局作用域中被发射。 这可以用来使用汇编代码编写完整的函数,并且通常提供更多的自由来使用任意寄存器和汇编指令。

Template string arguments

模板字符串参数

汇编器模板使用与格式字符串相同的语法(即占位符是由一对花括号来标定)。 相应的参数通过顺序、索引或名称来获取。 但是不支持隐式的命名参数(RFC #2795引入)。

asm!宏调用可以有一个或多个模板字符串参数;带有多个模板字符串参数的 asm! 的所有字符串参数都会被视为用 \n 连接在了一起的大的字符串。 预期的用法是每个模板字符串参数对应一行汇编代码。 所有的模板字符串参数必须放在其他类型的参数之前。

和格式字符串一样,命名参数必须在位置参数之后出现。 显式的寄存器操作必须出现在操作(operand)列表的末尾(译者注:不懂操作列表的,请回头看看上面内联汇编句法中 asm 的参数表达式),如果有命名参数,还要在命名参数之后。

在模板字符串中,占位符不能使用(带有)显式寄存器的操作。 所有其他命名操作和位置操作必须在模板字符串中至少出现一次,否则将生成编译器错误。

确切的汇编代码语法是特定于目标架构的,对于编译器来说是不透明的,当然除了编译器可以将各种操作类型去替换模板字符串以形成能传递给汇编器的汇编代码。

目前,所有支持的目标架构都遵循 LLVM 内部汇编器使用的汇编代码语法,这通常对应于 GNU汇编器(GAS)。 x86目标架构上,默认使用 GAS的 .intel_syntax noprefix模式。 ARM目标架构上,使用 .syntax unified模式。 这些目标架构对汇编代码做了一些额外的限制:任何汇编器状态(例如,可以使用 .section 更改的当前节)必须在 asm字符串末尾恢复为其原始值。 不符合 GAS语法的汇编代码将导致特定于汇编器的行为。 对内联汇编使用的指令的进一步的约束在本章后面的指令支持章节有详细说明。

Operand type

操作类型

目前支持以下几种操作:

  • in(<reg>) <expr>
    • <reg> 可以指向某类寄存器或一个显式的寄存器。 此已分配的寄存器名会被替换到 asm模板字符串中。
    • 在这段 asm代码的开头,此已分配的寄存器将包含 <expr> 的值。
    • 在这段 asm代码的结尾,该寄存器内的值必须恢复如初(除了另有 lateout操作也被分配使用了此寄存器)。
  • out(<reg>) <expr>
    • <reg> 可以指向某类寄存器或一个显式的寄存器。 此已分配的寄存器名会被替换到 asm模板字符串中。
    • 在这段 asm代码的开头,此已分配的寄存器将包含一个未定义的值。
    • <expr> 必须是一个(可能未被初始化的)位置表达式,在这段 asm代码的结尾,此寄存器的内容会被写到此位置表达式里。
    • 可以使用下划线(_)替代这个位置表达式,这将导致在这段 asm代码的结尾,此寄存器的内容被丢弃(等效于一个 clobber寄存器(译者注:clobber寄存器会被该asm语句中的汇编代码隐性修改,也因此,编译器在为输入操作数和输出操作数挑选寄存器时,就不会使用这类寄存器,这样就避免了发生数据覆盖等逻辑错误)。
  • lateout(<reg>) <expr>
    • 除了寄存器分配器可以重用分配给 in 的寄存器外,其他的同 out
    • 应该只在读取所有输入后才写入此寄存器,否则可能会毁坏真实的输入。
  • inout(<reg>) <expr>
    • <reg> 可以指向某类寄存器或一个显式的寄存器。 此已分配的寄存器名会被替换到 asm模板字符串中。
    • 在这段 asm代码的开头,此已分配的寄存器将包含 <expr> 的值。
    • <expr> 必须是一个可变的已初始化的位置表达式,在这段 asm代码的结尾,此已分配的寄存器内的内容将会被写入此表达式。
  • inout(<reg>) <in expr> => <out expr>
    • 除了从 <in expr> 里取值来初始化此寄存器外,其他的同 inout
    • <out expr> 必须是一个(可能未被初始化的)位置表达式,在这段 asm代码的结尾,此寄存器的内容会被写到此位置表达式里。
    • 可以使用下划线(_)替代这个 <out expr>表达式,这将导致在这段 asm代码的结尾,此寄存器的内容被丢弃(等效于一个 clobber寄存器)。
    • <in expr><out expr> 可以有不同的类型。
  • inlateout(<reg>) <expr> / inlateout(<reg>) <in expr> => <out expr>
    • 除了寄存器分配器可以重用分配给 in 的寄存器外(如果编译器知道 ininlateout 具有相同的初始值,则可能发生这种情况),其他的同 inout
    • 应该只在读取所有输入后才写入此寄存器,否则可能会毁坏真实的输入。
  • sym <path>
    • <path> 必须指向一个 fn程序项 或 static程序项。
    • 引用该程序项的混淆符号名(mangled symbol)称被替换为 asm模板字符串。
    • 替换的字符串不包含任何修饰符(例如GOT、PLT、重定位等)。
    • <path>允许指向#[thread_local]静态项,在这种情况下,asm代码可以将符号与重定位(例如@plt@TPOFF)结合起来,来从线程内的本地变量中读取数据。

操作表达式从左到右求值,就像函数调用参数一样。 在 asm!执行后,会按从左到右的顺序写出输出。 这一点很重要,如果两个输出指向同一个位置:该位置(表达式)将包含最右侧输出的值。

因为 global_asm! 存在于函数外部,它只能使用 sym操作。

Register operands

寄存器操作

输入和输出操作可以被用来指定一个显式寄存器或某一类寄存器(可以被寄存器分配器选择和分配的一类寄存器,每次可以从中选择和分配其中的一个)。 显式寄存器是被字符串文本(例如 "eax")指定的,而寄存器类是通过标识符(例如 reg)来指定的。

请注意,显式寄存器将寄存器别名(例如ARM上的 r14lr)和寄存器的较小视图(例如 eaxrax)视为与其基寄存器(base register)等效。 对两个输入操作或两个输出操作使用相同的显式寄存器会在编译期报错。 此外,在输入操作或输出操作中发生寄存器重叠(如ARM VFP)也会在编译期报错。

仅允许以下类型的值作为的内联汇编操作:

  • 整型 (有符号的和无符号的)
  • 浮点型数值
  • 指针 (仅廋指针)
  • 函数指针
  • SIMD向量 (使用 #[repr(simd)] 定义的并且实现了 Copy 的结构体)。 这包括在 std::arch 中定义的特定于体系架构的向量类型,如 __m128 (x86) 或 int8x16_t (ARM)。

以下是当前支持的寄存器类列表:

体系架构寄存器类寄存器LLVM约束代码
x86regax, bx, cx, dx, si, di, bp, r[8-15] (仅 x86-64)r
x86reg_abcdax, bx, cx, dxQ
x86-32reg_byteal, bl, cl, dl, ah, bh, ch, dhq
x86-64reg_byte*al, bl, cl, dl, sil, dil, bpl, r[8-15]bq
x86xmm_regxmm[0-7] (x86) xmm[0-15] (x86-64)x
x86ymm_regymm[0-7] (x86) ymm[0-15] (x86-64)x
x86zmm_regzmm[0-7] (x86) zmm[0-31] (x86-64)v
x86kregk[1-7]Yk
x86kreg0k0仅 clobbers
x86x87_regst([0-7])仅 clobbers
x86mmx_regmm[0-7]仅 clobbers
x86-64tmm_regtmm[0-7]仅 clobbers
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffr仅 clobbers
ARM (ARM/Thumb2)regr[0-12], r14r
ARM (Thumb1)regr[0-7]r
ARMsregs[0-31]t
ARMsreg_low16s[0-15]x
ARMdregd[0-31]w
ARMdreg_low16d[0-15]t
ARMdreg_low8d[0-8]x
ARMqregq[0-15]w
ARMqreg_low8q[0-7]t
ARMqreg_low4q[0-3]x
RISC-Vregx1, x[5-7], x[9-15], x[16-31] (non-RV32E)r
RISC-Vfregf[0-31]f
RISC-Vvregv[0-31]仅 clobbers
LoongArchreg$r1, $r[4-20], $r[23,30]r
LoongArchfreg$f[0-31]f

注意:

  • 在x86上,我们将 reg_byte 与 reg区别对待,因为编译器可以为reg_byte分别分配低位的 'al' 和高位的 'ah',而reg` 却只能持有整个寄存器。

  • 在x86-64上,reg_byte寄存器类中没有高位寄存器(例如没有 ah)。

  • 一些寄存器类被标记为 "仅 clobbers" 意味着此类中的寄存器不能被用于输入或输出,只能用于 out(<explicit register>) _lateout(<explicit register>) _ 这样的表达形式的。

每个寄存器类都有可以与之一起使用的值类型的约束。 这是必要的,因为值被加载到寄存器的方式取决于它的类型。 例如,在大端机系统上,将 i32x4i8x16 加载到 SIMD寄存器可能会导致不同的寄存器内容,即便这两个值的字节内存表示形式相同。 特定寄存器类支持的类型的可用性可能取决于当前启用的目标特性。

体系架构寄存器类目标特性允许的类型
x86-32regNonei16, i32, f32
x86-64regNonei16, i32, f32, i64, f64
x86reg_byteNonei8
x86xmm_regssei32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
x86ymm_regavxi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
x86zmm_regavx512fi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
i8x64, i16x32, i32x16, i64x8, f32x16, f64x8
x86kregavx512fi8, i16
x86kregavx512bwi32, i64
x86mmx_regN/A仅 clobbers
x86x87_regN/A仅 clobbers
x86tmm_regN/A仅 clobbers
AArch64regNonei8, i16, i32, f32, i64, f64
AArch64vregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
AArch64pregN/A仅 clobbers
ARMregNonei8, i16, i32, f32
ARMsregvfp2i32, f32
ARMdregvfp2i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2
ARMqregneoni8x16, i16x8, i32x4, i64x2, f32x4
RISC-V32regNonei8, i16, i32, f32
RISC-V64regNonei8, i16, i32, f32, i64, f64
RISC-Vfregff32
RISC-Vfregdf64
RISC-VvregN/A仅 clobbers
LoongArch64regNonei8, i16, i32, i64, f32, f64
LoongArch64fregNonef32, f64

注意: 对于上述表里,指针、函数指针和 isize/usize 被视为等效的整数类型(i16/i32/i64 具体取决于目标架构)。

如果某个值的内存宽度小于分配给它的寄存器,则在输入时该寄存器的高位将有一个未定义的输入值,输出时将忽略该寄存器的高位值。 唯一的例外是 RISC-V 上的 freg寄存器类,其中 f32 值按照 RISC-V 体系架构的要求被以 NaN-boxed的形式表达为 f64

当为 inout操作分别指定了输入和输出表达式时,这两个表达式必须具有相同的数据类型。 唯一的例外是两个操作(所操作的表达式)都是指针或整数时,它们只需要具有相同的类型内存宽度。 存在此约束是因为 LLVM 和 GCC 中的寄存器分配器有时无法让既定的操作(operand)和不同的数据类型进行有效绑定。

Register names

寄存器名称

一些寄存器有多个名称。 编译器会将它们视为与其基寄存器名称相同。 以下是所有受支持的寄存器别名的列表:

体系架构基寄存器别名
x86axeax, rax
x86bxebx, rbx
x86cxecx, rcx
x86dxedx, rdx
x86siesi, rsi
x86diedi, rdi
x86bpbpl, ebp, rbp
x86spspl, esp, rsp
x86ipeip, rip
x86st(0)st
x86r[8-15]r[8-15]b, r[8-15]w, r[8-15]d
x86xmm[0-31]ymm[0-31], zmm[0-31]
AArch64x[0-30]w[0-30]
AArch64x29fp
AArch64x30lr
AArch64spwsp
AArch64xzrwzr
AArch64v[0-31]b[0-31], h[0-31], s[0-31], d[0-31], q[0-31]
ARMr[0-3]a[1-4]
ARMr[4-9]v[1-6]
ARMr9rfp
ARMr10sl
ARMr11fp
ARMr12ip
ARMr13sp
ARMr14lr
ARMr15pc
RISC-Vx0zero
RISC-Vx1ra
RISC-Vx2sp
RISC-Vx3gp
RISC-Vx4tp
RISC-Vx[5-7]t[0-2]
RISC-Vx8fp, s0
RISC-Vx9s1
RISC-Vx[10-17]a[0-7]
RISC-Vx[18-27]s[2-11]
RISC-Vx[28-31]t[3-6]
RISC-Vf[0-7]ft[0-7]
RISC-Vf[8-9]fs[0-1]
RISC-Vf[10-17]fa[0-7]
RISC-Vf[18-27]fs[2-11]
RISC-Vf[28-31]ft[8-11]
LoongArch$r0$zero
LoongArch$r1$ra
LoongArch$r2$tp
LoongArch$r3$sp
LoongArch$r[4-11]$a[0-7]
LoongArch$r[12-20]$t[0-8]
LoongArch$r21
LoongArch$r22$fp, $s9
LoongArch$r[23-31]$s[0-8]
LoongArch$f[0-7]$fa[0-7]
LoongArch$f[8-23]$ft[0-15]
LoongArch$f[24-31]$fs[0-7]

某些寄存器不能用于输入或输出操作:

体系架构不支持的寄存器原因
Allsp栈指针必须在 asm代码块末尾恢复为其原始值。
Allbp (x86), x29 (AArch64), x8 (RISC-V), $fp (LoongArch)帧指针不能用作输入或输出。
ARMr7 or r11在 ARM 上,帧指针可以是 r7r11,具体取决于目标架构。帧指针不能用作输入或输出。
Allsi (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64), x9 (RISC-V), $s8 (LoongArch)LLVM 在内部将其用作具有复杂栈帧的函数的“基指针”。
x86ip这是程序计数器,不是真正的寄存器。
AArch64xzr这是一个不能修改的常量零寄存器。
AArch64x18这是一些 AArch64目标架构上的操作系统预留寄存器。
ARMpc这是程序计数器,不是真正的寄存器。
ARMr9这是一些 ARM目标架构上的操作系统预留寄存器。
RISC-Vx0这是一个不能修改的常量零寄存器。
RISC-Vgp, tp这些寄存器是预留的,不能用作输入或输出。
LoongArch$r0 or $zero这是一个不能修改的常量零寄存器。
LoongArch$r2 or $tp这是为TLS保留的。
LoongArch$r21这是 ABI 所保留的。

帧指针和基指针寄存器(base pointer register)预留供 LLVM 内部使用。而 asm!语句不能显式去指定使用预留寄存器,但在某些情况下,LLVM 将为 reg操作分配其中一个预留寄存器。使用预留寄存器的汇编代码应该小心,因为 reg操作可能在使用相同的寄存器。

Template modifiers

模板修饰符

占位符可以通过在大括号中的 : 之后指定的修饰符进行扩充。 这些修饰符不影响寄存器分配,但会更改插入模板字符串时操作(operand)的格式化方式。 每个模板占位符只允许一个修饰符。

支持的修饰符是 LLVM(和 GCC)asm模板参数修饰符的子集,但不使用相同的字母代码。

体系架构寄存器类修饰符输出示例LLVM修饰符
x86-32regNoneeaxk
x86-64regNoneraxq
x86-32reg_abcdlalb
x86-64reglalb
x86reg_abcdhahh
x86regxaxw
x86regeeaxk
x86-64regrraxq
x86reg_byteNoneal / ahNone
x86xmm_regNonexmm0x
x86ymm_regNoneymm0t
x86zmm_regNonezmm0g
x86*mm_regxxmm0x
x86*mm_regyymm0t
x86*mm_regzzmm0g
x86kregNonek1None
AArch64regNonex0x
AArch64regww0w
AArch64regxx0x
AArch64vregNonev0None
AArch64vregvv0None
AArch64vregbb0b
AArch64vreghh0h
AArch64vregss0s
AArch64vregdd0d
AArch64vregqq0q
ARMregNoner0None
ARMsregNones0None
ARMdregNoned0P
ARMqregNoneq0q
ARMqrege / fd0 / d1e / f
RISC-VregNonex1None
RISC-VfregNonef0None
LoongArchregNone$r1None
LoongArchfregNone$f0None

注意:

  • 对于 ARM架构 e / f: 这将打印出 NEON quad(128位)寄存器的低双字或高双字寄存器名称。
  • 对于 x86架构: 对于没有修饰符的 reg,Rust 编译器与 GCC 的编译行为不同。 GCC 将根据操作值的类型推断修饰符,而 Rust 默认为完整寄存器宽度。
  • 对于 x86架构的 xmm_reg: LLVM修饰符 xtg 目前还未在 LLVM 中真正实现(目前它们仅被 GCC 支持),但这应该将只是一个小变动。

如前一节开头所述,传递小于寄存器宽度的输入值将导致寄存器的高位包含未定义的值。 如果内联asm 仅访问寄存器的低位,则这不是问题,这可以通过使用模板修饰符在 asm代码中使用子寄存器名称(例如,使用 ax 替代 rax)来实现。 由于这是一个容易犯的错误,编译器将建议在适当的情况下使用模板修饰符来给出输入值的类型。 但如果某个操作的所有引用都已包含了修饰符,则对该操作的警告将会被抑制。

ABI clobbers

关键字clobber_abi 可用于将与之对应的一组默认的 clobber寄存器应用于 asm!块内。 这在调用具有特定调用约定的函数时,会根据需要自动插入必要的 clobber约束:如果调用约定没有在调用过程中完整保持寄存器的值,则会将 lateout("...") _ 隐式添加到操作列表中 (其中 ... 要用具体的寄存器名字替换掉)。

clobber_abi 可以指定任意多次。它将为所有指定了调用约定的寄存器集合中的所有单一寄存器都分配一个 clobber寄存器。

当使用 clobber_abi 时,不允许把通用寄存器类作为输出寄存器类来指定:此时所有输出寄存器必须显式指定。 有 clobber_abi 时,把显式输出寄存器标记为输出寄存器的工作优先于给寄存器隐式插入 clobber标志:此时只有当该寄存器明确未用作输出时,才会为寄存器插入 clobber标记。 以下的 ABI 可与 clobber_abi 一起使用:

体系架构ABI名称Clobbered寄存器
x86-32"C", "system", "efiapi", "cdecl", "stdcall", "fastcall"ax, cx, dx, xmm[0-7], mm[0-7], k[1-7], st([0-7])
x86-64"C", "system" (Windows平台), "efiapi", "win64"ax, cx, dx, r[8-11], xmm[0-31], mm[0-7], k[1-7], st([0-7]), tmm[0-7]
x86-64"C", "system" (在非Windows平台上), "sysv64"ax, cx, dx, si, di, r[8-11], xmm[0-31], mm[0-7], k[1-7], st([0-7]), tmm[0-7]
AArch64"C", "system", "efiapi"x[0-17], x18*, x30, v[0-31], p[0-15], ffr
ARM"C", "system", "efiapi", "aapcs"r[0-3], r12, r14, s[0-15], d[0-7], d[16-31]
RISC-V"C", "system", "efiapi"x1, x[5-7], x[10-17], x[28-31], f[0-7], f[10-17], f[28-31], v[0-31]
LoongArch"C", "system", "efiapi"$r1, $r[4-20], $f[0-23]

注意:

  • 在 AArch64架构上,如果 x18 不是目标架构上的预留寄存器,则它仅被包括在 clobber寄存器列表中。

随着体系架构中新的寄存器的加入,在rustc中,每个 ABI 对应的 clobbered寄存器列表也会一并更新:这确保了当 LLVM 开始在其生成的代码中使用这些新寄存器时,asm! 里的 clobber寄存器的正确性也能得到保持。

Options

可选项

我们使用标志(Flag)技术来进一步影响内联汇编块的行为。 目前定义了以下可选项(标志):

  • pure:此 asm!块没有副作用,且最终必须返回,其输出仅取决于其直接输入(比如,输入的值本身,而不是它们指向的对象)或从内存读取的值(除非还设置了 nomem可选项)。 这允许编译器执行此 asm!块的次数少于程序中指定的次数(例如,通过将其从循环中提出来),或者如果 asm!块没有输出的情况下,编译器甚至可能完全消除此段 asm代码。 pure可选项必须与 nomemreadonly 可选项组合使用,否则会导致编译期错误。
  • nomem:此 asm!块不读取或写入任何内存。 这允许编译器将修改过的全局变量的值缓存在跨当前 asm!块的寄存器中,因为它知道当前 asm!不会读取或写入它们。 编译器还假定此 asm!块不执行与其他线程的任何类型的同步,例如通过 fence。
  • readonly:此 asm!块不写入任何内存。 这允许编译器将未修改的全局变量的值缓存在跨当前 asm!块的寄存器中,因为它知道它们不是由当前 asm!写入的。 编译器还假定此 asm!块不执行与其他线程的任何类型的同步,例如通过 fence。
  • preserves_flags:此 asm!块不修改标志寄存器(在后面章节的规则表中有定义)。 这可以避免编译器在此 asm!块之后重新计算条件标志。
  • noreturn:此 asm!块没有具体返回值,其返回类型被定义为 !(never)。 如果执行超过 asm代码的末尾,则为未定义行为。(译者注:asm代码末尾应该有跳转,否则执行程序会顺序执行二进制指令,从而执行超出 asm代码块) 有 noreturn可选项的 asm块的行为就像一个不返回的函数;需要注意的是,其作用域中的局部变量在被调用之前不会被销毁。
  • nostack:此 asm!块不会将数据推送到栈上,也不会写入栈的红色区域(red-zone)(有些目标架构会支持red-zone)。 如果此可选项使用,则保证栈指针为函数调用会(根据目标ABI)适当对齐。
  • att_syntax:此可选项仅在 x86架构上有效,并导致汇编器使用 GNU汇编器的 .att_syntax prefix模式。 寄存器操作在被替换时会自动加上前导%
  • raw:这将导致模板字符串被解析为裸汇编字符串,对 {} 也不做特殊处理。 这在使用 include_str! 从外部文件中包含裸汇编代码时非常有用。

编译器对可选项执行一些附加检查:

  • nomemreadonly 是互斥的:同时指定这两个选项会导致编译期错误。
  • 在没有输出或只有丢弃的输出(_)的 asm块上指定 pure 会导致编译期错误。
  • 在带有输出的 asm块上指定 noreturn 会导致编译期错误。

global_asm! 仅支持 att_syntaxraw 可选项。 其余可选项对于全局作用域类型的内联汇编没有意义。

Rules for inline assembly

内联汇编规则

为了避免未定义行为,在使用函数作用域类型的内联汇编(asm!)时必须遵循以下规则:

  • 任何未指定为输入的寄存器在进入 asm块时都会包含未定义的值。
    • 内联汇编上下文中的“未定义值”意味着寄存器可以(非确定性地)具有体系架构允许的任何一个可能值。 值得注意的是,它与 LLVM 的 undef 不同,LLVM 的 undef 每次读取时都会有不同的值(因为在汇编代码中不存在这样的概念)。
  • 任何未指定为输出的寄存器在退出 asm块时必须具有与进入时相同的值,否则为未定义行为。
    • 这仅适用于可指定为输入或输出的寄存器。 其他寄存器遵循特定于目标架构的规则。
    • 请注意,lateout 可以分配给与 in 相同的寄存器,在这种情况下,此规则不适用。 然而,代码不应该依赖于此,因为它依赖于寄存器分配的结果。
  • 如果在 asm块中执行展开(unwind),则为未定义行为。
    • 如果汇编代码调用一个函数,然后该函数展开,则这也适用此规则。
  • 汇编代码允许读取和写入的内存位置集与 FFI函数允许读取和写入的内存位置集相同。
    • 有关确切规则,请参阅不安全代码指南。
    • 如果设置了 readonly可选项,则只允许内存读取。
    • 如果设置了 nomem可选项,则不允许对内存进行读取或写入。
    • 这些规则不适用于 asm代码私有的内存空间,例如 asm块内分配的堆栈空间。
  • 编译器不能假定 asm块中的指令是最终实际执行的指令。
    • 这实际上意味着编译器必须将 asm! 作为一个黑盒对待,只考虑接口规范,而不考虑 asm块内的指令本身。
    • 允许通过特定于目标架构的机制来执行运行时代码补丁。
  • 除非设置了 nostack可选项,否则可以允许 asm代码使用栈指针下方的栈空间。
    • 当进入 asm块后,为保证栈指针为函数调用,此指针会(根据目标ABI)适当对齐。
    • 你有责任确保不会溢出堆栈(例如,使用堆栈探测以确保命中保护页)。
    • 根据目标ABI的要求分配堆栈内存时,应调整堆栈指针。
    • 在离开 asm块之前,必须将堆栈指针还原为其原始值。
  • 如果设置了 noreturn可选项,则如果执行超过 asm块的末尾,则此行为未定义。
  • 如果设置了 pure可选项,那么如果 asm!代码除了有直接输出外,还有其他副作用,则此行为未定义。 如果两次执行 asm!代码,它们具有相同输入,但输出不同,此行为也未定义。
    • 当与 nomem可选项一起使用时,所谓的“输入”只能是 asm!的直接输入。
    • 当与 readonly可选项一起使用时,所谓的“输入”包含 asm!的直接输入,还包含允许 asm!块来读取的全部内存。
  • 如果设置了 preserves_flags可选项,则在退出 asm块时必须恢复这些标志寄存器:
    • x86
      • EFLAGS (CF, PF, AF, ZF, SF, OF)中的状态标志寄存器。
      • 浮点状态字 (全部)。
      • MXCSR (PE, UE, OE, ZE, DE, IE)中的浮点异常标志寄存器。
    • ARM
      • CPSR (N, Z, C, V)中的条件标志寄存器。
      • CPSR (Q)中的饱和标志寄存器。
      • CPSR (GE)中的大于或等于标志寄存器。
      • FPSCR (N, Z, C, V)中的条件标志寄存器。
      • FPSCR (QC)中的饱和标志寄存器。
      • FPSCR (IDC, IXC, UFC, OFC, DZC, IOC)中的浮点异常标志寄存器。
    • AArch64
      • 条件标志寄存器(NZCV)。
      • 浮点状态寄存器(FPSR).
    • RISC-V
      • fcsr (fflags)中的浮点异常标志寄存器。
      • 向量扩展状态寄存器(vtype, vl, vcsr)。
    • LoongArch
      • $fcc[0-7] 中的浮点条件标志寄存器。
  • 在 x86 上,方向标志寄存器(EFLAGS 中的 DF)在进入 asm块时清除,在退出时也必须清除。
    • 如果在退出 asm块时设置了方向标志,则此行为未定义。
  • 在 x86 上,x87浮点寄存器栈必须保持不变,除非所有的 st([0-7])寄存器都被用 out("st(0)") _, out("st(1)") _, ...标记标记为 clobber寄存器。
    • 如果所有 x87寄存器都被标记为 clobber寄存器,则在进入 asm块时,x87寄存器栈被保证为空。并且汇编代码必须确保退出 asm块时 x87寄存器栈也为空。
  • 将堆栈指针和非输出寄存器恢复为其原始值的要求仅在退出 asm!块时适用。
    • 这意味着永不返回的 asm!块(即使未标记为 noreturn)不需要恢复这些寄存器。
    • 当返回到其他的 asm!块(例如上下文切换)时,这些寄存器必须包含它们在进入当前(你正在退出)的 asm!块的值。
      • 你不能退出尚未进入的 asm!块。 你也不能退出已退出的 asm!块(也可以理解为不能未进入而退出)。
      • 你负责切换任何特定于目标架构的状态(例如线程级的本地存储和堆栈边界)。
      • 你不能从一个 asm!块中的地址跳转块到另一个 asm!块中的地址,即使在同一个函数或块中也不行。应将两个 asm!块的上下文视为潜在不同,跳转时会进行上下文切换。 你不能假设这些上下文中的任何特定值(例如当前堆栈指针或堆栈指针下的临时值)在两个 asm!块之间保持不变。
      • 你可以访问的内存位置集是你进入的 asm!块所允许的内存位置和你刚退出的 asm!块所允许的内存位置的交集。
  • 即使两个 asm!块在源代码中相邻,并且它们之间没有任何其他代码,你仍不能假设它们也将在二进制中的地址连续,不能保证它们之间没有任何其他指令。
  • 你不能假设 asm!块在输出的二进制文件中只出现一次。 允许编译器实例化 asm!块的多个副本块,例如当包含它的函数在多个位置内联时。
  • 在 x86 上,内联汇编不能以指令前缀(如 LOCK)结尾(这些指令前缀被用于编译器生成的指令)。
    • 由于内联汇编的编译方式,编译器当前还无法检测到此问题,但将来可能会捕获并拒绝此问题。

注意:作为一个一般性原则,preserves_flags 包含的标志是在执行函数调用时保留的标志。

Correctness and Validity

正确性和有效性

除了前面的所有规则外,asm! 的字符串参数(在计算完所有其他参数后,执行格式化并转换其操作数)必须最终转换成为(对于目标体系结构而言)语法正确且语义有效的汇编代码。 其中,格式化规则允许编译器生成具有正确语法的汇编代码。 有关操作数的规则允许将 Rust操作数有效转换为asm!,以及从 asm! 转换出。 为使最终汇编代码既正确又有效,遵守这些规则是必要的,但还不够。例如:

  • 格式化后,参数可能被放置在语法不正确的位置
  • 一条指令可能被正确写入,但该操作数在给定的体系结构上却是无效的
  • 对应体系结构未支持的指令可以汇编成不确定的代码
  • 一组指令,每一条都正确有效,如果连续放置,仍有可能会导致未定义的行为

因此,这些规则是 非穷尽 的。编译器不需要检查初始字符串的正确性和有效性,也不需要检查生成的最终汇编代码。 汇编器可以检查这些代码的正确性和有效性,但不需要这样做。 使用 asm! 时,键入错误就足以使程序变得不健壮。完全排除这类错误太难了,汇编规则那是包含在数千页的体系结构参考手册中的呀。 程序员应该谨慎行事,调用这种 unsafe 的功能需要承担不违反编译器或体系结构规则的责任。

Directives Support

伪指令支持

内联汇编支持 GNU AS 和 LLVM 的内部汇编器支持的伪指令集的一个子集,具体如下所示。 也有部分伪指令的效果是特定于汇编器的(可能会导致错误,或者可能会被接受)。

如果内联汇编包含任何修改后续汇编程序的“有状态(stateful)”伪指令,则块必须在内联汇编结束之前撤消任何此类伪指令的执行效果。

下面这些伪指令被汇编器确保支持:

  • .2byte
  • .4byte
  • .8byte
  • .align
  • .alt_entry
  • .ascii
  • .asciz
  • .balign
  • .balignl
  • .balignw
  • .bss
  • .byte
  • .comm
  • .data
  • .def
  • .double
  • .endef
  • .equ
  • .equiv
  • .eqv
  • .fill
  • .float
  • .global
  • .globl
  • .inst
  • .lcomm
  • .long
  • .octa
  • .option
  • .p2align
  • .popsection
  • .private_extern
  • .pushsection
  • .quad
  • .scl
  • .section
  • .set
  • .short
  • .size
  • .skip
  • .sleb128
  • .space
  • .string
  • .text
  • .type
  • .uleb128
  • .word

Target Specific Directive Support

基于特定目标规范的指令支持

Dwarf Unwinding
Dwarf展开

支持 DWARF展开信息的 ELF目标平台支持以下指令:

  • .cfi_adjust_cfa_offset
  • .cfi_def_cfa
  • .cfi_def_cfa_offset
  • .cfi_def_cfa_register
  • .cfi_endproc
  • .cfi_escape
  • .cfi_lsda
  • .cfi_offset
  • .cfi_personality
  • .cfi_register
  • .cfi_rel_offset
  • .cfi_remember_state
  • .cfi_restore
  • .cfi_restore_state
  • .cfi_return_column
  • .cfi_same_value
  • .cfi_sections
  • .cfi_signal_frame
  • .cfi_startproc
  • .cfi_undefined
  • .cfi_window_save
Structured Exception Handling
结构化异常处理

在带有结构化异常处理机制的目标平台上,可以确保下面的这些附加指令得到支持:

  • .seh_endproc
  • .seh_endprologue
  • .seh_proc
  • .seh_pushreg
  • .seh_savereg
  • .seh_setframe
  • .seh_stackalloc
x86 (32-bit and 64-bit)
x86 (32位 and 64位)

无论是 32位还是 64位 x86目标平台,可以确保下面的这些附加指令得到支持:

  • .nops
  • .code16
  • .code32
  • .code64

只有在退出汇编块之前将状态重置为默认状态时,才支持使用 .code16.code32.code64 这些指令。 默认情况下,32位 x86平台使用 .code32,64位 x86平台使用 .code64

ARM (32-bit)
ARM (32位)

ARM目标平台下,可以确保下面的这些附加指令得到支持:

  • .even
  • .fnstart
  • .fnend
  • .save
  • .movsp
  • .code
  • .thumb
  • .thumb_func