内联汇编

通过 asm!global_asm! 宏提供了对内联汇编的支持。它可以用于在编译器生成的汇编输出中嵌入手写的汇编代码。

在以下架构上,内联汇编是稳定的

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

如果在不支持的目标上使用 asm!,编译器将发出错误。

示例

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

// Multiply x by 6 using shifts and adds
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);
}
}

语法

以下 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 / sym <path> / const <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) [","] ")"

作用域

内联汇编可以通过两种方式使用。

使用 asm! 宏,汇编代码在函数作用域内发出,并集成到函数编译器生成的汇编代码中。此汇编代码必须遵守 严格的规则 以避免未定义行为。请注意,在某些情况下,编译器可能会选择将汇编代码作为单独的函数发出,并生成对它的调用。

使用 global_asm! 宏,汇编代码在全局作用域内发出,在函数之外。这可以用于手写整个函数使用汇编代码,并且通常提供更大的自由度来使用任意寄存器和汇编器指令。

模板字符串参数

汇编器模板使用与 格式化字符串 相同的语法(即,占位符由花括号指定)。

相应的参数按顺序、索引或名称访问。

但是,不支持隐式命名参数(由 RFC #2795 引入)。

asm! 调用可能有一个或多个模板字符串参数;具有多个模板字符串参数的 asm! 被视为所有字符串都用 \n 连接在一起。预期的用法是每个模板字符串参数对应于一行汇编代码。

所有模板字符串参数必须出现在任何其他参数之前。

与格式化字符串一样,位置参数必须出现在命名参数和显式 寄存器操作数 之前。

显式寄存器操作数不能被模板字符串中的占位符使用。

所有其他命名和位置操作数必须在模板字符串中至少出现一次,否则会生成编译器错误。

确切的汇编代码语法是特定于目标的,并且对编译器是不透明的,除了操作数被替换到模板字符串中以形成传递给汇编器的代码的方式之外。

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

操作数类型

支持几种类型的操作数

  • in(<reg>) <expr>
    • <reg> 可以引用寄存器类或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。
    • 分配的寄存器将在 asm 代码开始时包含 <expr> 的值。
    • 分配的寄存器必须在 asm 代码结束时包含相同的值(除非 lateout 分配给同一寄存器)。
  • out(<reg>) <expr>
    • <reg> 可以引用寄存器类或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。
    • 分配的寄存器将在 asm 代码开始时包含未定义的值。
    • <expr> 必须是(可能未初始化的)位置表达式,分配的寄存器的内容在 asm 代码结束时写入到该表达式。
    • 可以指定下划线 (_) 而不是表达式,这将导致寄存器的内容在 asm 代码结束时被丢弃(有效地充当一个 clobber)。
  • lateout(<reg>) <expr>
    • out 相同,不同之处在于寄存器分配器可以重用分配给 in 的寄存器。
    • 您应该只在读取所有输入后才写入寄存器,否则您可能会 clobber 一个输入。
  • inout(<reg>) <expr>
    • <reg> 可以引用寄存器类或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。
    • 分配的寄存器将在 asm 代码开始时包含 <expr> 的值。
    • <expr> 必须是可变初始化的位置表达式,分配的寄存器的内容在 asm 代码结束时写入到该表达式。
  • inout(<reg>) <in expr> => <out expr>
    • inout 相同,不同之处在于寄存器的初始值取自 <in expr> 的值。
    • <out expr> 必须是(可能未初始化的)位置表达式,分配的寄存器的内容在 asm 代码结束时写入到该表达式。
    • 可以为 <out expr> 指定下划线 (_) 而不是表达式,这将导致寄存器的内容在 asm 代码结束时被丢弃(有效地充当一个 clobber)。
    • <in expr><out expr> 可以具有不同的类型。
  • inlateout(<reg>) <expr> / inlateout(<reg>) <in expr> => <out expr>
    • inout 相同,不同之处在于寄存器分配器可以重用分配给 in 的寄存器(如果编译器知道 in 具有与 inlateout 相同的初始值,则可能发生这种情况)。
    • 您应该只在读取所有输入后才写入寄存器,否则您可能会 clobber 一个输入。
  • sym <path>
    • <path> 必须引用 fnstatic
    • 引用该项的 mangled 符号名称被替换到 asm 模板字符串中。
    • 替换的字符串不包含任何修饰符(例如,GOT、PLT、重定位等)。
    • <path> 允许指向 #[thread_local] static,在这种情况下,asm 代码可以将符号与重定位(例如 @plt, @TPOFF)结合使用,以从线程本地数据读取。
  • const <expr>
    • <expr> 必须是整数常量表达式。此表达式遵循与内联 const 代码块相同的规则。
    • 表达式的类型可以是任何整数类型,但默认值与整数文字一样,为 i32
    • 表达式的值被格式化为字符串并直接替换到 asm 模板字符串中。

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

由于 global_asm! 存在于函数之外,它只能使用 symconst 操作数。

寄存器操作数

输入和输出操作数可以指定为显式寄存器或寄存器类,寄存器分配器可以从中选择寄存器。显式寄存器指定为字符串文字(例如 "eax"),而寄存器类指定为标识符(例如 reg)。

请注意,显式寄存器将寄存器别名(例如 ARM 上的 r14lr)和寄存器的较小视图(例如 eaxrax)视为等同于基寄存器。

为两个输入操作数或两个输出操作数使用相同的显式寄存器是编译时错误。

此外,在输入操作数或输出操作数中使用重叠寄存器(例如 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仅限 clobber
x86x87_regst([0-7])仅限 clobber
x86mmx_regmm[0-7]仅限 clobber
x86-64tmm_regtmm[0-7]仅限 clobber
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffr仅限 clobber
Arm64ECregx[0-12], x[15-22], x[25-27], x30r
Arm64ECvregv[0-15]w
Arm64ECvreg_low16v[0-15]x
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] (非 RV32E)r
RISC-Vfregf[0-31]f
RISC-Vvregv[0-31]仅限 clobber
LoongArchreg$r1, $r[4-20], $r[23,30]r
LoongArchfreg$f[0-31]f
s390xregr[0-10], r[12-14]r
s390xreg_addrr[1-10], r[12-14]a
s390xfregf[0-15]f
s390xvregv[0-31]仅限 clobber
s390xarega[2-15]仅限 clobber

注释:

  • 在 x86 上,我们将 reg_bytereg 不同对待,因为编译器可以分别分配 alah,而 reg 保留整个寄存器。
  • 在 x86-64 上,高字节寄存器(例如 ah)在 reg_byte 寄存器类中不可用。
  • 某些寄存器类被标记为“仅限 clobber”,这意味着这些类中的寄存器不能用于输入或输出,只能用于 out(<explicit register>) _lateout(<explicit register>) _ 形式的 clobber。

每个寄存器类都对其可以使用的值类型有限制。这是必要的,因为将值加载到寄存器中的方式取决于其类型。例如,在 big-endian 系统上,即使 i32x4i8x16 的字节级内存表示形式相同,将它们加载到 SIMD 寄存器中也可能导致不同的寄存器内容。特定寄存器类支持的类型的可用性可能取决于当前启用的目标特性。

架构寄存器类目标特性允许的类型
x86-32regi16, i32, f32
x86-64regi16, i32, f32, i64, f64
x86reg_bytei8
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仅限 clobber
x86x87_regN/A仅限 clobber
x86tmm_regN/A仅限 clobber
AArch64regi8, 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仅限 clobber
Arm64ECregi8, i16, i32, f32, i64, f64
Arm64ECvregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
ARMregi8, i16, i32, f32
ARMsregvfp2i32, f32
ARMdregvfp2i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2
ARMqregneoni8x16, i16x8, i32x4, i64x2, f32x4
RISC-V32regi8, i16, i32, f32
RISC-V64regi8, i16, i32, f32, i64, f64
RISC-Vfregff32
RISC-Vfregdf64
RISC-VvregN/A仅限 clobber
LoongArch64regi8, i16, i32, i64, f32, f64
LoongArch64fregff32
LoongArch64fregdf64
s390xreg, reg_addri8, i16, i32, i64
s390xfregf32, f64
s390xvregN/A仅限 clobber
s390xaregN/A仅限 clobber

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

如果值的大小小于分配给它的寄存器,则该寄存器的高位对于输入将具有未定义的值,并且对于输出将被忽略。唯一的例外是 RISC-V 上的 freg 寄存器类,其中 f32 值按照 RISC-V 架构的要求在 f64 中进行 NaN-boxed。

当为 inout 操作数指定单独的输入和输出表达式时,两个表达式必须具有相同的类型。唯一的例外是如果两个操作数都是指针或整数,在这种情况下,它们只需要具有相同的大小。存在此限制是因为 LLVM 和 GCC 中的寄存器分配器有时无法处理具有不同类型的绑定操作数。

寄存器名称

某些寄存器有多个名称。编译器将所有这些名称都视为与基寄存器名称相同。以下是所有受支持的寄存器别名列表

架构基寄存器别名
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]
Arm64ECx[0-30]w[0-30]
Arm64ECx29fp
Arm64ECx30lr
Arm64ECspwsp
Arm64ECxzrwzr
Arm64ECv[0-15]b[0-15], h[0-15], s[0-15], d[0-15], q[0-15]
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]

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

架构不支持的寄存器原因
所有sp, r15 (s390x)堆栈指针必须在 asm 代码块的末尾恢复到其原始值。
所有bp (x86), x29 (AArch64 和 Arm64EC), x8 (RISC-V), $fp (LoongArch), r11 (s390x)帧指针不能用作输入或输出。
ARMr7r11在 ARM 上,帧指针可以是 r7r11,具体取决于目标。帧指针不能用作输入或输出。
所有si (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64 和 Arm64EC), x9 (RISC-V), $s8 (LoongArch)这在内部被 LLVM 用作具有复杂堆栈帧的函数的“基指针”。
x86ip这是程序计数器,而不是真正的寄存器。
AArch64xzr这是一个常量零寄存器,无法修改。
AArch64x18这是某些 AArch64 目标上的操作系统保留寄存器。
Arm64ECxzr这是一个常量零寄存器,无法修改。
Arm64ECx18这是一个操作系统保留寄存器。
Arm64ECx13, x14, x23, x24, x28, v[16-31], p[0-15], ffr这些是 Arm64EC 不支持的 AArch64 寄存器。
ARMpc这是程序计数器,而不是真正的寄存器。
ARMr9这是某些 ARM 目标上的操作系统保留寄存器。
RISC-Vx0这是一个常量零寄存器,无法修改。
RISC-Vgp, tp这些寄存器是保留的,不能用作输入或输出。
LoongArch$r0$zero这是一个常量零寄存器,无法修改。
LoongArch$r2$tp这是为 TLS 保留的。
LoongArch$r21这是 ABI 保留的。
s390xc[0-15]由内核保留。
s390xa[0-1]保留供系统使用。

帧指针和基指针寄存器保留供 LLVM 内部使用。虽然 asm! 语句不能显式指定保留寄存器的使用,但在某些情况下,LLVM 会为 reg 操作数分配这些保留寄存器之一。使用保留寄存器的汇编代码应小心,因为 reg 操作数可能使用相同的寄存器。

模板修饰符

占位符可以通过修饰符来增强,修饰符在花括号中的 : 之后指定。这些修饰符不影响寄存器分配,但会更改操作数在插入到模板字符串时的格式。

每个模板占位符只允许一个修饰符。

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

架构寄存器类修饰符示例输出LLVM 修饰符
x86-32regeaxk
x86-64regraxq
x86-32reg_abcdlalb
x86-64reglalb
x86reg_abcdhahh
x86regxaxw
x86regeeaxk
x86-64regrraxq
x86reg_byteal / ah
x86xmm_regxmm0x
x86ymm_regymm0t
x86zmm_regzmm0g
x86*mm_regxxmm0x
x86*mm_regyymm0t
x86*mm_regzzmm0g
x86kregk1
AArch64/Arm64ECregx0x
AArch64/Arm64ECregww0w
AArch64/Arm64ECregxx0x
AArch64/Arm64ECvregv0
AArch64/Arm64ECvregvv0
AArch64/Arm64ECvregbb0b
AArch64/Arm64ECvreghh0h
AArch64/Arm64ECvregss0s
AArch64/Arm64ECvregdd0d
AArch64/Arm64ECvregqq0q
ARMregr0
ARMsregs0
ARMdregd0P
ARMqregq0q
ARMqrege / fd0 / d1e / f
RISC-Vregx1
RISC-Vfregf0
LoongArchreg$r1
LoongArchfreg$f0
s390xreg%r0
s390xreg_addr%r1
s390xfreg%f0

注释:

  • 在 ARM 上 e / f:这会打印 NEON quad(128 位)寄存器的低位或高位双字寄存器名称。
  • 在 x86 上:我们对于没有修饰符的 reg 的行为与 GCC 的行为不同。GCC 将根据操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。
  • 在 x86 xmm_reg 上:xtg LLVM 修饰符尚未在 LLVM 中实现(它们仅由 GCC 支持),但这应该是一个简单的更改。

如上一节所述,传递小于寄存器宽度的输入值将导致寄存器的高位包含未定义的值。如果内联汇编仅访问寄存器的低位,则这不是问题,这可以通过使用模板修饰符在汇编代码中使用子寄存器名称来完成(例如 ax 而不是 rax)。由于这是一个容易出错的陷阱,编译器将建议在适当的情况下使用模板修饰符,具体取决于输入类型。如果对操作数的所有引用都已经有修饰符,则会抑制该操作数的警告。

ABI clobber

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[0-7], st([0-7])
x86-64"C", "system" (在 Windows 上), "efiapi", "win64"ax, cx, dx, r[8-11], xmm[0-31], mm[0-7], k[0-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[0-7], st([0-7]), tmm[0-7]
AArch64"C", "system", "efiapi"x[0-17], x18*, x30, v[0-31], p[0-15], ffr
Arm64EC"C", "system"x[0-12], x[15-17], x30, v[0-15]
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"$r1, $r[4-20], $f[0-23]
s390x"C", "system"r[0-5], r14, f[0-7], v[0-31], a[2-15]

注释

  • 在 AArch64 上,只有当 x18 不被视为目标上的保留寄存器时,才包含在 clobber 列表中。
  • 在 RISC-V 上,只有当 x[16-17]x[28-31] 不被视为目标上的保留寄存器时,才包含在 clobber 列表中。

每个 ABI 的 clobbered 寄存器列表在 rustc 中会随着架构获得新的寄存器而更新:这确保了当 LLVM 开始在其生成的代码中使用这些新寄存器时,asm! clobber 将继续是正确的。

选项

标志用于进一步影响内联汇编代码块的行为。当前定义了以下选项

  • pureasm! 代码块没有副作用,必须最终返回,并且其输出仅取决于其直接输入(即值本身,而不是它们指向的内容)或从内存读取的值(除非还设置了 nomem 选项)。这允许编译器执行 asm! 代码块的次数少于程序中指定的次数(例如,通过将其提升出循环)甚至完全消除它(如果未使用输出)。pure 选项必须与 nomemreadonly 选项结合使用,否则会发出编译时错误。
  • nomemasm! 代码块不从 asm! 代码块外部可访问的任何内存读取或写入。这允许编译器在 asm! 代码块之间将修改后的全局变量的值缓存在寄存器中,因为它知道 asm! 不会读取或写入它们。编译器还假定此 asm! 代码块不执行与其他线程的任何类型的同步,例如通过 fences。
  • readonlyasm! 代码块不写入 asm! 代码块外部可访问的任何内存。这允许编译器在 asm! 代码块之间将未修改的全局变量的值缓存在寄存器中,因为它知道 asm! 不会写入它们。编译器还假定此 asm! 代码块不执行与其他线程的任何类型的同步,例如通过 fences。
  • preserves_flagsasm! 代码块不修改标志寄存器(在下面的规则中定义)。这允许编译器避免在 asm! 代码块之后重新计算条件标志。
  • noreturnasm! 代码块永远不会返回,并且其返回类型定义为 ! (never)。如果执行流过了 asm 代码的末尾,则行为未定义。noreturn asm 代码块的行为就像一个不返回的函数;值得注意的是,作用域内的局部变量在调用它之前不会被 drop。
  • nostackasm! 代码块不会将数据推送到堆栈,也不会写入堆栈 red-zone(如果目标支持)。如果使用此选项,则保证堆栈指针已适当对齐(根据目标 ABI)以进行函数调用。
  • att_syntax:此选项仅在 x86 上有效,并使汇编器使用 GNU 汇编器的 .att_syntax prefix 模式。寄存器操作数在替换时带有前导 %
  • raw:这会导致模板字符串被解析为原始汇编字符串,对 {} 没有特殊处理。当使用 include_str! 从外部文件包含原始汇编代码时,这主要很有用。

编译器对选项执行一些额外的检查

  • nomemreadonly 选项是互斥的:同时指定两者是编译时错误。
  • 在没有输出或只有丢弃输出 (_) 的 asm 代码块上指定 pure 是编译时错误。
  • 在具有输出的 asm 代码块上指定 noreturn 是编译时错误。

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

内联汇编的规则

为避免未定义行为,使用函数作用域内联汇编 (asm!) 时必须遵循这些规则

  • 任何未指定为输入的寄存器在进入 asm 代码块时都将包含未定义的值。
    • 在内联汇编的上下文中,“未定义的值”意味着寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM undef 不同,后者每次读取时都可以具有不同的值(因为汇编代码中不存在这样的概念)。
  • 任何未指定为输出的寄存器在退出 asm 代码块时必须具有与进入时相同的值,否则行为未定义。
    • 这仅适用于可以指定为输入或输出的寄存器。其他寄存器遵循特定于目标的规则。
    • 请注意,lateout 可能分配给与 in 相同的寄存器,在这种情况下,此规则不适用。但是,代码不应依赖于此,因为它取决于寄存器分配的结果。
  • 如果执行从 asm 代码块中 unwind 出来,则行为未定义。
    • 如果汇编代码调用一个函数,然后该函数 unwind,这也适用。
  • 汇编代码允许读取和写入的内存位置集与 FFI 函数允许的内存位置集相同。
    • 有关确切规则,请参阅不安全代码指南。
    • 如果设置了 readonly 选项,则只允许内存读取。
    • 如果设置了 nomem 选项,则不允许读取或写入内存。
    • 这些规则不适用于 asm 代码私有的内存,例如在 asm 代码块内分配的堆栈空间。
  • 编译器不能假设 asm 中的指令是最终实际执行的指令。
    • 这实际上意味着编译器必须将 asm! 视为黑盒,并且只考虑接口规范,而不是指令本身。
    • 允许通过特定于目标的机制进行运行时代码修补。
    • 但是,不能保证每个 asm! 都直接对应于目标文件中的单个指令实例:编译器可以自由地复制或删除重复的 asm! 代码块。
  • 除非设置了 nostack 选项,否则允许汇编代码使用堆栈指针以下的堆栈空间。
    • 在进入汇编代码块时,保证堆栈指针已适当对齐(根据目标 ABI)以进行函数调用。
    • 您有责任确保您不会使堆栈溢出(例如,使用堆栈探测来确保您触及保护页)。
    • 您应该根据目标 ABI 的要求调整堆栈指针以分配堆栈内存。
    • 在离开汇编代码块之前,必须将堆栈指针恢复为其原始值。
  • 如果设置了 noreturn 选项,则当执行流程落到汇编代码块末尾时,行为是未定义的。
  • 如果设置了 pure 选项,则当 asm! 具有除其直接输出之外的副作用时,行为是未定义的。如果使用相同输入对 asm! 代码进行两次执行导致不同的输出,行为也是未定义的。
    • 当与 nomem 选项一起使用时,“输入”仅指 asm! 的直接输入。
    • 当与 readonly 选项一起使用时,“输入”包括 asm! 的直接输入以及 asm! 代码块允许读取的任何内存。
  • 如果设置了 preserves_flags 选项,则在退出汇编代码块时必须恢复这些标志寄存器
    • 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 和 Arm64EC
      • 条件标志 (NZCV 寄存器)。
      • 浮点状态 (FPSR 寄存器)。
    • RISC-V
      • fcsr (fflags) 中的浮点异常标志。
      • 向量扩展状态 (vtype, vl, vcsr)。
    • LoongArch
      • $fcc[0-7] 中的浮点条件标志。
    • s390x
      • 条件代码寄存器 cc
  • 在 x86 上,方向标志(EFLAGS 中的 DF)在进入汇编代码块时被清除,并且在退出时必须被清除。
    • 如果在退出汇编代码块时设置了方向标志,则行为是未定义的。
  • 在 x86 上,x87 浮点寄存器堆栈必须保持不变,除非所有 st([0-7]) 寄存器都已标记为被破坏,例如 out("st(0)") _, out("st(1)") _, ...
    • 如果所有 x87 寄存器都被破坏,则保证 x87 寄存器堆栈在进入 asm 代码块时为空。汇编代码必须确保 x87 寄存器堆栈在退出汇编代码块时也为空。
  • 将堆栈指针和非输出寄存器恢复为其原始值的要求仅在退出 asm! 代码块时适用。
    • 这意味着永不返回的 asm! 代码块(即使未标记为 noreturn)也不需要保留这些寄存器。
    • 当返回到与您进入的 asm! 代码块不同的代码块时(例如,对于上下文切换),这些寄存器必须包含您退出asm! 代码块的进入时的值。
      • 您不能退出尚未进入的 asm! 代码块。您也不能退出已经退出的 asm! 代码块(除非先再次进入它)。
      • 您有责任切换任何特定于目标的状态(例如,线程本地存储,堆栈边界)。
      • 您不能从一个 asm! 代码块中的地址跳转到另一个代码块中的地址,即使在同一函数或代码块内也是如此,除非将它们的上下文视为可能不同并需要上下文切换。您不能假设这些上下文中的任何特定值(例如,当前堆栈指针或堆栈指针以下的临时值)在两个 asm! 代码块之间保持不变。
      • 您可以访问的内存位置集合是您进入和退出的 asm! 代码块允许访问的内存位置的交集。
  • 您不能假设源代码中相邻的两个 asm! 代码块,即使它们之间没有任何其他代码,也会最终在二进制文件中以连续的地址出现,并且它们之间没有任何其他指令。
  • 您不能假设 asm! 代码块将在输出二进制文件中只出现一次。编译器可以实例化 asm! 代码块的多个副本,例如,当包含它的函数在多个位置内联时。
  • 在 x86 上,内联汇编代码不得以指令前缀(例如 LOCK)结尾,该前缀将应用于编译器生成的指令。
    • 由于内联汇编代码的编译方式,编译器目前无法检测到这一点,但将来可能会捕获并拒绝这种情况。

注意:作为一般规则,preserves_flags 涵盖的标志是那些在执行函数调用时保留的标志。

正确性和有效性

除了之前的所有规则之外,asm! 的字符串参数最终必须成为——在所有其他参数被评估、执行格式化和操作数被转换之后——对于目标架构而言,既符合语法规则又语义有效的汇编代码。格式化规则允许编译器生成具有正确语法的汇编代码。关于操作数的规则允许将 Rust 操作数有效转换进出 asm!。遵守这些规则是必要的,但对于最终扩展的汇编代码既正确又有效而言,这还不够充分。例如

  • 参数可能放置在格式化后语法上不正确的位置
  • 指令可能被正确编写,但给定了架构上无效的操作数
  • 架构上未指定的指令可能会被汇编成未指定的代码
  • 一组指令,每条指令都正确且有效,如果立即连续放置,可能会导致未定义的行为

因此,这些规则是非详尽的。编译器不需要检查初始字符串或生成的最终汇编代码的正确性和有效性。汇编器可能会检查正确性和有效性,但不是必须这样做。当使用 asm! 时,一个排版错误可能足以使程序变得不健全,并且汇编规则可能包含数千页的架构参考手册。程序员应谨慎行事,因为调用此 unsafe 功能意味着承担不违反编译器或架构规则的责任。

指令支持

内联汇编代码支持 GNU AS 和 LLVM 内部汇编器支持的指令子集,如下所示。使用其他指令的结果是特定于汇编器的(并且可能导致错误,或者可能按原样接受)。

如果内联汇编代码包含任何“有状态”指令,该指令会修改后续汇编代码的处理方式,则代码块必须在内联汇编代码结束之前撤消任何此类指令的效果。

以下指令保证被汇编器支持

  • .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
  • .insn
  • .lcomm
  • .long
  • .octa
  • .option
  • .p2align
  • .popsection
  • .private_extern
  • .pushsection
  • .quad
  • .scl
  • .section
  • .set
  • .short
  • .size
  • .skip
  • .sleb128
  • .space
  • .string
  • .text
  • .type
  • .uleb128
  • .word

目标特定的指令支持

Dwarf Unwinding

以下指令在支持 DWARF unwind 信息的 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
结构化异常处理

在具有结构化异常处理的目标上,保证支持以下附加指令

  • .seh_endproc
  • .seh_endprologue
  • .seh_proc
  • .seh_pushreg
  • .seh_savereg
  • .seh_setframe
  • .seh_stackalloc
x86(32 位和 64 位)

在 x86 目标上,包括 32 位和 64 位,保证支持以下附加指令

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

只有在状态在退出汇编代码块之前重置为默认值的情况下,才支持使用 .code16.code32.code64 指令。32 位 x86 默认使用 .code32,而 x86_64 默认使用 .code64

ARM(32 位)

在 ARM 上,保证支持以下附加指令

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