内联汇编

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

以下体系结构上的内联汇编支持已经稳定:

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

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

示例

#![allow(unused)]
fn main() {
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);
}

语法

以下是常规语法的 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) [","] ")"

作用域

内联汇编有两种使用方式。

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

使用 global_asm! 宏时,汇编代码在全局作用域中生成,不在函数内。这可以用于使用汇编代码手写整个函数,并且通常提供更多使用任意寄存器和汇编指令的自由。

模板字符串参数

汇编模板使用与 格式化字符串 相同的语法 (即由大括号指定占位符) 。 相应的参数按顺序、索引或名称访问。但是,不支持隐式命名参数 (由 RFC #2795 引入) 。

asm! 调用可能有一个或多个模板字符串参数;具有多个模板字符串参数的 asm! 被视为所有字符串在它们之间用 \n 连接。 预期的用法是,每个模板字符串参数对应于一个汇编代码行。所有模板字符串参数必须出现在任何其他参数之前。

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

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

除了用于将操作数替换为传递给汇编器的代码的方式之外,确切的汇编代码语法是特定于目标的,并且对编译器不透明。

目前,所有受支持的目标都遵循LLVM内部汇编器使用的汇编代码语法,这通常对应于GNU汇编器 (GAS)的语法。 在x86上,默认情况下使用 GAS 的 .intel_syntax noprefix 模式。 在ARM上,使用 .syntax unified 模式。

这些目标对汇编代码施加了一个额外的限制: 必须在 asm 字符串的末尾将任何汇编器状态 (例如可以使用 .section 更改的当前部分) 恢复为其原始值。 不符合 GAS 语法的汇编代码将导致汇编器特定的行为。关于内联汇编使用的指令的进一步约束在 指令支持 中指示。

操作数类型

支持几种操作数类型:

  • in(<reg>) <expr>
    • <reg> 可以是寄存器类或显式寄存器。 分配的寄存器名称将替换为 asm 模板字符串中的名称。
    • 分配的寄存器将在 asm 代码开始时包含 <expr> 的值。
    • 在 asm 代码结束时,分配的寄存器必须包含相同的值 (除非另一个 lateout 被分配到相同的寄存器) 。
  • out(<reg>) <expr>
    • <reg> 可以是寄存器类或显式寄存器。 分配的寄存器名称将替换为 asm 模板字符串中的名称。
    • 分配的寄存器在 asm 代码开始时包含未定义的值。
    • <expr> 必须是一个 (可能未初始化的) place 表达式,分配的寄存器的内容在 asm 代码结束时写入该表达式。
    • 可以使用下划线 (_) 代替表达式,在 asm 代码结束时会丢弃寄存器的内容 (有效地作为 clobber) 。
  • lateout(<reg>) <expr>
    • out 相同,但是寄存器分配器可以重新使用分配给 in 的寄存器。
    • 应在读取所有输入之后再写入寄存器,否则可能破坏输入。
  • inout(<reg>) <expr>
    • <reg>可以引用寄存器类或显式寄存器。 分配的寄存器名称将替换为汇编模板字符串中。
    • 分配的寄存器将在汇编代码开始时包含 <expr> 的值。
    • <expr>必须是一个可变的初始化地点表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。
  • inout(<reg>) <in expr> => <out expr>
    • inout 相同,但是寄存器的初始值取自 <in expr> 的值。
    • <out expr> 必须是一个 (可能未初始化的) 地点表达式,分配的寄存器的内容将在汇编代码结束时写入该表达式。
    • 可以为 <out expr> 指定下划线 (_) ,这将导致在汇编代码结束时丢弃寄存器的内容 (实际上起到破坏作用) 。
    • <in expr><out expr>可能具有不同的类型。
  • inlateout(<reg>) <expr> / inlateout(<reg>) <in expr> => <out expr>
    • inout 相同,但是寄存器分配器可以重用分配给 in 的寄存器 (如果编译器知道 in 具有与 inlateout 相同的初始值,则可能会发生这种情况) 。
    • 只有在读取所有输入后才应写入寄存器,否则可能会破坏输入。
  • sym <path>
    • <path> 必须引用 fnstatic
    • 指向该条目的重命名符号名称将替换为汇编模板字符串中。
    • 替换的字符串不包括任何修改器 (例如 GOT 、 PLT 、重定位等) 。
    • <path> 允许指向 #[thread_local] 静态变量,在这种情况下,汇编代码可以将符号与重定位 (例如 @plt@TPOFF ) 组合以从线程本地数据读取。

操作数表达式从左到右依次评估,就像函数调用参数一样。 在 asm! 执行完成后,输出按照从左到右的顺序进行写入。 如果两个输出指向同一个位置,则该位置将包含最右边输出的值。

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

寄存器操作数

输入和输出操作数可以被指定为显式寄存器或者是寄存器类,从中寄存器分配器可以选择一个寄存器。 显式寄存器以字符串字面量表示 (例如 "eax" ) ,而寄存器类则以标识符表示 (例如 reg ) 。

需要注意的是,显式寄存器将寄存器别名 (例如 ARM 上的 r14 vs lr) 和寄存器的较小视图 (例如eax vs rax) 视为等效于基础寄存器。 在两个输入操作数或两个输出操作数中使用相同的显式寄存器是编译时错误。 此外,在输入操作数或输出操作数中使用重叠的寄存器 (例如 ARM VFP) 也是编译时错误。

只有以下类型的操作数被允许用于内联汇编:

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

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

ArchitectureRegister classRegistersLLVM constraint code
x86regax, bx, cx, dx, si, di, bp, r[8-15] (x86-64 only)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
x86kreg0k0Only clobbers
x86x87_regst([0-7])Only clobbers
x86mmx_regmm[0-7]Only clobbers
x86-64tmm_regtmm[0-7]Only clobbers
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffrOnly 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]Only clobbers

  • 在 x86 上,我们将 reg_bytereg 区别对待,因为编译器可以分别分配 alah,而 reg 保留整个寄存器。

  • 在 x86-64 上,高字节寄存器 (例如 ah) 不可用于 reg_byte 寄存器类。

  • 一些寄存器类被标记为 "仅占用" ,这意味着这些类中的寄存器不能用作输入或输出,只能用作类似 out(<explicit register>) _lateout(<explicit register>) _ 的占用。

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

ArchitectureRegister classTarget featureAllowed types
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/AOnly clobbers
x86x87_regN/AOnly clobbers
x86tmm_regN/AOnly 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/AOnly 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/AOnly clobbers

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

如果一个值的大小比分配给它的寄存器小,那么对于输入,该寄存器的高位将具有未定义的值,对于输出,将被忽略。 唯一的例外是 RISC-V 上的 freg 寄存器类,其中 f32 值作为 RISC-V 架构所需的 NaN 被封装在 f64 中。

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

寄存器名称

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

ArchitectureBase registerAliases
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]

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

架构不支持的寄存器原因
Allsp栈指针必须在 asm 代码块结束时恢复为其原始值。
Allbp (x86), x29 (AArch64), x8 (RISC-V)帧指针不能用作输入或输出。
ARMr7r11在 ARM 上,帧指针可以是 r7r11,具体取决于目标。帧指针不能用作输入或输出。
Allsi (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64), x9 (RISC-V)LLVM 在函数具有复杂堆栈帧的情况下将其用作“基指针”。
x86ip这是程序计数器,不是实际的寄存器。
AArch64xzr这是一个无法修改的常量零寄存器。
AArch64x18这是某些 AArch64 目标上保留的操作系统寄存器。
ARMpc这是程序计数器,不是实际的寄存器。
ARMr9这是某些 ARM 目标上保留的操作系统寄存器。
RISC-Vx0这是一个无法修改的常量零寄存器。
RISC-Vgptp这些寄存器是保留的,不能用作输入或输出。

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

模板修改器

在花括号中的 : 后可以加上修改器,来改变插入模板字符串时操作数的格式,这些修改器不影响寄存器分配。每个模板占位符只能有一个修改器。

这些修改器是 LLVM 和 GCC 的汇编模板参数修改器的子集,但使用的字母代码不同。

ArchitectureRegister classModifierExample outputLLVM modifier
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

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

如前一节所述,如果内联汇编传递的输入值比寄存器宽度小,则寄存器的上位比特将包含未定义的值。 如果内联汇编只访问寄存器的低位,这不是一个问题,可以通过使用模板修饰符在汇编代码中使用子寄存器名称 (例如,使用 ax 代替 rax) 来实现。 由于这是一个简单的错误,编译器将根据输入类型在适当的地方建议使用模板修饰符。 如果对一个操作数的所有引用已经有修饰符,则该警告将对该操作数进行抑制。

ABI破坏标记

clobber_abi 关键字可用于将默认一组破坏标记应用于 asm! 块。这将根据特定调用约定自动插入必要的破坏标记:如果调用约定在函数调用时不能完全保留寄存器的值,则会在操作数列表中隐式添加 lateout("...") _ (其中 ... 替换为寄存器名称) 。

clobber_abi 可以指定任意次数。它将为所有指定的调用约定的并集中的所有唯一寄存器插入一个破坏标记。

当使用 clobber_abi 时,编译器禁止使用通用寄存器类输出:所有输出都必须指定显式寄存器。显式寄存器输出优先于由 clobber_abi 插入的隐式破坏标记:仅当该寄存器未用作输出时,才会为该寄存器插入破坏标记。

以下 ABIs 可与 clobber_abi 一起使用:

ArchitectureABI nameClobbered registers
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" (on 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" (on non-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
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]

注意:

  • 在 AArch64 上,仅当 x18 在目标上不被视为保留寄存器时,才将其包括在破坏列表中。

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

选项

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

  • pure: asm! 块没有副作用,其输出仅取决于其直接输入 (即值本身,而不是它们所指向的内容) 或从内存读取的值 (除非也设置了 nomem 选项) 。这使得编译器可以执行比程序中指定的更少的 asm! 块次数 (例如将其提升出循环) 或甚至完全消除它 (如果输出没有使用) 。
  • nomem: asm! 块不读取或写入任何内存。这使得编译器可以在 asm! 块之间缓存修改后的全局变量的值,因为它知道它们不会被 asm! 读取或写入。
  • readonly: asm! 块不写入任何内存。这使得编译器可以在 asm! 块之间缓存未修改的全局变量的值,因为它知道它们不会被 asm! 写入。
  • preserves_flags: asm! 块不修改标志寄存器 (在下面的规则中定义) 。这使得编译器可以在 asm! 块之后避免重新计算条件标志。
  • noreturn: asm! 块永远不会返回,其返回类型定义为 ! (永远不会) 。如果执行超过 asm 代码的结尾,行为是未定义的。 noreturn asm 块的行为就像不返回的函数一样;特别是,作用域中的局部变量在调用之前不会被丢弃。
  • nostack: asm! 块不将数据推入栈,也不写入堆栈保护区域 (如果目标支持) 。如果未使用此选项,则栈指针保证根据目标 ABI 适当对齐,以用于函数调用。
  • att_syntax: 此选项仅在 x86 上有效,导致汇编程序使用 GNU 汇编程序的 .att_syntax 前缀模式。寄存器操作数将替换为带有前导 % 的操作数。
  • raw: 这将导致模板字符串被解析为原始汇编字符串,没有对 {} 进行特殊处理。这在使用 include_str! 包含来自外部文件的原始汇编代码时非常有用。

编译器对选项进行了一些额外的检查:

  • nomemreadonly 选项是互斥的:指定两者都会导致编译时错误。
  • pure 选项必须与 nomemreadonly 选项结合使用,否则会发出编译时错误。
  • 在没有输出或只有被丢弃的输出 (_) 的 asm! 块上指定 pure 是编译时错误。
  • 在带有输出的 asm! 块上指定 noreturn 是编译时错误。

global_asm! 只支持 att_syntaxraw 选项。 其他选项在全局作用域内的内联汇编中没有意义。

函数内联汇编规则

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

  • 任何未在输入中指定的寄存器,在进入汇编块时将包含未定义的值。
    • 在内联汇编的上下文中, "未定义的值" 意味着该寄存器可以 (非确定性地) 具有体系结构允许的任何可能值。 需要注意的是,它与 LLVM 中的 undef 不同,后者每次读取时都可以具有不同的值 (因为在汇编代码中不存在这样的概念) 。
  • 任何未指定为输出的寄存器,在离开汇编块时必须具有与进入时相同的值,否则行为是未定义的。
    • 这仅适用于可以指定为输入或输出的寄存器。 其他寄存器遵循特定于目标的规则。
    • 需要注意的是,lateout 可能会分配到与 in 相同的寄存器中,此时此规则不适用。 不过代码不应该依赖此规则,因为它取决于寄存器分配的结果。
  • 如果执行从汇编块中退出,则行为是未定义的。
    • 如果汇编代码调用一个函数,然后函数抛出异常也同样适用。
  • 汇编代码被允许读取和写入的内存位置集合与 FFI 函数允许的位置集合相同。
    • 参考非安全代码指南了解确切的规则。
    • 如果设置了 readonly 选项,则只允许内存读取。
    • 如果设置了 nomem 选项,则不允许读取或写入内存。
    • 这些规则不适用于汇编代码私有的内存,例如在汇编块中分配的栈空间。
  • 编译器不能假设汇编中的指令实际上就是将要执行的指令。
    • 这实际上意味着编译器必须将 asm! 视为黑匣子,并仅考虑接口规范,而不考虑指令本身。
    • 可以通过特定于目标的机制进行运行时代码修补。
  • 除非设置了 nostack 选项,汇编代码可以使用堆栈指针下面的堆栈空间。
    • 进入汇编块时,堆栈指针保证对于函数调用具有适当的对齐方式 (根据目标 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) 。
      • 向量扩展状态 (vtypevlvcsr) 。
  • 在 x86 上,进入 asm 块时方向标志 (EFLAGS 中的 DF) 被清除,并且在退出时必须保持清除状态。
    • 如果在退出 asm 块时设置了方向标志,则行为未定义。
  • 在 x86 上,除非所有的 st([0-7]) 寄存器都被标记为被污染的,否则 x87 浮点寄存器堆栈必须保持不变,方法是使用 out("st(0)") _,out("st(1)") _,...
    • 如果所有 x87 寄存器都被污染,则在进入 asm 块时保证 x87 寄存器堆栈为空。汇编代码必须确保在退出 asm 块时 x87 寄存器堆栈也为空。
  • 将栈指针和非输出寄存器恢复到其原始值的要求仅适用于退出 asm! 块时。
    • 这意味着从不返回的 asm! 块 (即使没有标记为 noreturn) 不需要保留这些寄存器。
    • 当返回到与你进入的 asm! 块不同的 asm! 块时 (例如用于上下文切换) ,这些寄存器必须包含你正在 退出asm! 块进入时的值。
      • 你不能退出未进入的 asm! 块。也不能退出已经退出的 asm! 块 (除非首先再次进入它) 。
      • 你负责切换任何特定于目标的状态 (例如线程本地存储,堆栈边界) 。
      • 你不能从一个 asm! 块中的地址跳转到另一个 asm! 块中的地址,即使在同一个函数或块中,也必须将它们的上下文视为可能不同并要求进行上下文切换。你不能假设这些上下文中的任何特定值 (例如当前堆栈指针或堆栈指针下方的临时值) 在两个 asm! 块之间保持不变。
      • 你可以访问的内存位置集是你输入和退出的 asm! 块所允许的位置集的交集。
  • 你不能假设两个在源代码中相邻的 asm! 块,即使它们之间没有其他代码,也会在二进制代码中以连续的地址出现,而没有其他指令在它们之间。
  • 你不能假设 asm! 块将在输出二进制代码中出现一次。 编译器可以实例化多个 asm! 块,例如当包含它的函数在多个位置内联时。
  • 在 x86 平台上,内联汇编不得以指令前缀 (如 LOCK ) 作为结尾,因为这将应用于编译器生成的指令。
    • 目前,编译器无法检测到这种情况,但将来可能会检测并拒绝此类代码。

注意: 通常情况下,preserves_flags 覆盖的标志位是在函数调用时 不会 保留的标志位。

正确性和有效性

除了之前的所有规则之外, asm! 的字符串参数在所有其他参数求值、格式化和操作数翻译后,必须最终成为对于目标架构既具有语法正确性又具有语义有效性的汇编代码。 格式化规则允许编译器生成正确语法的汇编代码。有关操作数的规则允许 Rust 操作数与 asm! 中的操作数之间有效的转换。 遵循这些规则是必要的,但不足以保证最终扩展的汇编代码是正确且有效的。例如:

  • 参数可能被放置在格式化后语法上不正确的位置。
  • 一个指令可能被正确书写,但给定不符合架构的操作数。
  • 一个架构未指定的指令可能会被汇编成未指定的代码。
  • 一组指令,每个指令都正确且有效,如果放置在紧接着的位置,可能会导致未定义行为。

因此,这些规则是 "非穷尽" 的。编译器不需要检查最终汇编代码的正确性和有效性。汇编器可以检查正确性和有效性,但并不是必需的。 当使用 asm! 时,一个排版错误就足以使程序非安全,并且汇编规则可能包括数千页的架构参考手册。 程序员应该谨慎使用,因为使用这种 "非安全" 功能意味着承担不违反编译器或架构规则的责任。

指令支持

内联汇编支持一些 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
  • .lcomm
  • .long
  • .octa
  • .option
  • .p2align
  • .popsection
  • .private_extern
  • .pushsection
  • .quad
  • .scl
  • .section
  • .set
  • .short
  • .size
  • .skip
  • .sleb128
  • .space
  • .string
  • .text
  • .type
  • .uleb128
  • .word

目标特定指令支持

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
结构化异常处理

对于具有结构化异常处理的目标,以下附加指令被保证受支持:

  • .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