内联汇编
通过 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>
必须引用fn
或static
。- 引用该项的 mangled 符号名称被替换到 asm 模板字符串中。
- 替换的字符串不包含任何修饰符(例如,GOT、PLT、重定位等)。
<path>
允许指向#[thread_local]
static,在这种情况下,asm 代码可以将符号与重定位(例如@plt
,@TPOFF
)结合使用,以从线程本地数据读取。
const <expr>
<expr>
必须是整数常量表达式。此表达式遵循与内联const
代码块相同的规则。- 表达式的类型可以是任何整数类型,但默认值与整数文字一样,为
i32
。 - 表达式的值被格式化为字符串并直接替换到 asm 模板字符串中。
操作数表达式从左到右求值,就像函数调用参数一样。在 asm!
执行后,输出以从左到右的顺序写入。如果两个输出指向同一位置,这将很重要:该位置将包含最右侧输出的值。
由于 global_asm!
存在于函数之外,它只能使用 sym
和 const
操作数。
寄存器操作数
输入和输出操作数可以指定为显式寄存器或寄存器类,寄存器分配器可以从中选择寄存器。显式寄存器指定为字符串文字(例如 "eax"
),而寄存器类指定为标识符(例如 reg
)。
请注意,显式寄存器将寄存器别名(例如 ARM 上的 r14
与 lr
)和寄存器的较小视图(例如 eax
与 rax
)视为等同于基寄存器。
为两个输入操作数或两个输出操作数使用相同的显式寄存器是编译时错误。
此外,在输入操作数或输出操作数中使用重叠寄存器(例如 ARM VFP)也是编译时错误。
只有以下类型允许作为内联汇编的操作数
- 整数(有符号和无符号)
- 浮点数
- 指针(仅限瘦指针)
- 函数指针
- SIMD 向量(使用
#[repr(simd)]
定义并实现Copy
的结构体)。这包括std::arch
中定义的特定于架构的向量类型,例如__m128
(x86) 或int8x16_t
(ARM)。
以下是当前支持的寄存器类列表
架构 | 寄存器类 | 寄存器 | LLVM 约束代码 |
---|---|---|---|
x86 | reg | ax , bx , cx , dx , si , di , bp , r[8-15] (仅限 x86-64) | r |
x86 | reg_abcd | ax , bx , cx , dx | Q |
x86-32 | reg_byte | al , bl , cl , dl , ah , bh , ch , dh | q |
x86-64 | reg_byte * | al , bl , cl , dl , sil , dil , bpl , r[8-15]b | q |
x86 | xmm_reg | xmm[0-7] (x86) xmm[0-15] (x86-64) | x |
x86 | ymm_reg | ymm[0-7] (x86) ymm[0-15] (x86-64) | x |
x86 | zmm_reg | zmm[0-7] (x86) zmm[0-31] (x86-64) | v |
x86 | kreg | k[1-7] | Yk |
x86 | kreg0 | k0 | 仅限 clobber |
x86 | x87_reg | st([0-7]) | 仅限 clobber |
x86 | mmx_reg | mm[0-7] | 仅限 clobber |
x86-64 | tmm_reg | tmm[0-7] | 仅限 clobber |
AArch64 | reg | x[0-30] | r |
AArch64 | vreg | v[0-31] | w |
AArch64 | vreg_low16 | v[0-15] | x |
AArch64 | preg | p[0-15] , ffr | 仅限 clobber |
Arm64EC | reg | x[0-12] , x[15-22] , x[25-27] , x30 | r |
Arm64EC | vreg | v[0-15] | w |
Arm64EC | vreg_low16 | v[0-15] | x |
ARM (ARM/Thumb2) | reg | r[0-12] , r14 | r |
ARM (Thumb1) | reg | r[0-7] | r |
ARM | sreg | s[0-31] | t |
ARM | sreg_low16 | s[0-15] | x |
ARM | dreg | d[0-31] | w |
ARM | dreg_low16 | d[0-15] | t |
ARM | dreg_low8 | d[0-8] | x |
ARM | qreg | q[0-15] | w |
ARM | qreg_low8 | q[0-7] | t |
ARM | qreg_low4 | q[0-3] | x |
RISC-V | reg | x1 , x[5-7] , x[9-15] , x[16-31] (非 RV32E) | r |
RISC-V | freg | f[0-31] | f |
RISC-V | vreg | v[0-31] | 仅限 clobber |
LoongArch | reg | $r1 , $r[4-20] , $r[23,30] | r |
LoongArch | freg | $f[0-31] | f |
s390x | reg | r[0-10] , r[12-14] | r |
s390x | reg_addr | r[1-10] , r[12-14] | a |
s390x | freg | f[0-15] | f |
s390x | vreg | v[0-31] | 仅限 clobber |
s390x | areg | a[2-15] | 仅限 clobber |
注释:
- 在 x86 上,我们将
reg_byte
与reg
不同对待,因为编译器可以分别分配al
和ah
,而reg
保留整个寄存器。- 在 x86-64 上,高字节寄存器(例如
ah
)在reg_byte
寄存器类中不可用。- 某些寄存器类被标记为“仅限 clobber”,这意味着这些类中的寄存器不能用于输入或输出,只能用于
out(<explicit register>) _
或lateout(<explicit register>) _
形式的 clobber。
每个寄存器类都对其可以使用的值类型有限制。这是必要的,因为将值加载到寄存器中的方式取决于其类型。例如,在 big-endian 系统上,即使 i32x4
和 i8x16
的字节级内存表示形式相同,将它们加载到 SIMD 寄存器中也可能导致不同的寄存器内容。特定寄存器类支持的类型的可用性可能取决于当前启用的目标特性。
架构 | 寄存器类 | 目标特性 | 允许的类型 |
---|---|---|---|
x86-32 | reg | 无 | i16 , i32 , f32 |
x86-64 | reg | 无 | i16 , i32 , f32 , i64 , f64 |
x86 | reg_byte | 无 | i8 |
x86 | xmm_reg | sse | i32 , f32 , i64 , f64 ,i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 |
x86 | ymm_reg | avx | i32 , f32 , i64 , f64 ,i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 i8x32 , i16x16 , i32x8 , i64x4 , f32x8 , f64x4 |
x86 | zmm_reg | avx512f | i32 , f32 , i64 , f64 ,i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 i8x32 , i16x16 , i32x8 , i64x4 , f32x8 , f64x4 i8x64 , i16x32 , i32x16 , i64x8 , f32x16 , f64x8 |
x86 | kreg | avx512f | i8 , i16 |
x86 | kreg | avx512bw | i32 , i64 |
x86 | mmx_reg | N/A | 仅限 clobber |
x86 | x87_reg | N/A | 仅限 clobber |
x86 | tmm_reg | N/A | 仅限 clobber |
AArch64 | reg | 无 | i8 , i16 , i32 , f32 , i64 , f64 |
AArch64 | vreg | neon | i8 , i16 , i32 , f32 , i64 , f64 ,i8x8 , i16x4 , i32x2 , i64x1 , f32x2 , f64x1 ,i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 |
AArch64 | preg | N/A | 仅限 clobber |
Arm64EC | reg | 无 | i8 , i16 , i32 , f32 , i64 , f64 |
Arm64EC | vreg | neon | i8 , i16 , i32 , f32 , i64 , f64 ,i8x8 , i16x4 , i32x2 , i64x1 , f32x2 , f64x1 ,i8x16 , i16x8 , i32x4 , i64x2 , f32x4 , f64x2 |
ARM | reg | 无 | i8 , i16 , i32 , f32 |
ARM | sreg | vfp2 | i32 , f32 |
ARM | dreg | vfp2 | i64 , f64 , i8x8 , i16x4 , i32x2 , i64x1 , f32x2 |
ARM | qreg | neon | i8x16 , i16x8 , i32x4 , i64x2 , f32x4 |
RISC-V32 | reg | 无 | i8 , i16 , i32 , f32 |
RISC-V64 | reg | 无 | i8 , i16 , i32 , f32 , i64 , f64 |
RISC-V | freg | f | f32 |
RISC-V | freg | d | f64 |
RISC-V | vreg | N/A | 仅限 clobber |
LoongArch64 | reg | 无 | i8 , i16 , i32 , i64 , f32 , f64 |
LoongArch64 | freg | f | f32 |
LoongArch64 | freg | d | f64 |
s390x | reg , reg_addr | 无 | i8 , i16 , i32 , i64 |
s390x | freg | 无 | f32 , f64 |
s390x | vreg | N/A | 仅限 clobber |
s390x | areg | N/A | 仅限 clobber |
注意:为了上表的目的,指针、函数指针和
isize
/usize
被视为等效的整数类型(i16
/i32
/i64
取决于目标)。
如果值的大小小于分配给它的寄存器,则该寄存器的高位对于输入将具有未定义的值,并且对于输出将被忽略。唯一的例外是 RISC-V 上的 freg
寄存器类,其中 f32
值按照 RISC-V 架构的要求在 f64
中进行 NaN-boxed。
当为 inout
操作数指定单独的输入和输出表达式时,两个表达式必须具有相同的类型。唯一的例外是如果两个操作数都是指针或整数,在这种情况下,它们只需要具有相同的大小。存在此限制是因为 LLVM 和 GCC 中的寄存器分配器有时无法处理具有不同类型的绑定操作数。
寄存器名称
某些寄存器有多个名称。编译器将所有这些名称都视为与基寄存器名称相同。以下是所有受支持的寄存器别名列表
架构 | 基寄存器 | 别名 |
---|---|---|
x86 | ax | eax , rax |
x86 | bx | ebx , rbx |
x86 | cx | ecx , rcx |
x86 | dx | edx , rdx |
x86 | si | esi , rsi |
x86 | di | edi , rdi |
x86 | bp | bpl , ebp , rbp |
x86 | sp | spl , esp , rsp |
x86 | ip | eip , rip |
x86 | st(0) | st |
x86 | r[8-15] | r[8-15]b , r[8-15]w , r[8-15]d |
x86 | xmm[0-31] | ymm[0-31] , zmm[0-31] |
AArch64 | x[0-30] | w[0-30] |
AArch64 | x29 | fp |
AArch64 | x30 | lr |
AArch64 | sp | wsp |
AArch64 | xzr | wzr |
AArch64 | v[0-31] | b[0-31] , h[0-31] , s[0-31] , d[0-31] , q[0-31] |
Arm64EC | x[0-30] | w[0-30] |
Arm64EC | x29 | fp |
Arm64EC | x30 | lr |
Arm64EC | sp | wsp |
Arm64EC | xzr | wzr |
Arm64EC | v[0-15] | b[0-15] , h[0-15] , s[0-15] , d[0-15] , q[0-15] |
ARM | r[0-3] | a[1-4] |
ARM | r[4-9] | v[1-6] |
ARM | r9 | rfp |
ARM | r10 | sl |
ARM | r11 | fp |
ARM | r12 | ip |
ARM | r13 | sp |
ARM | r14 | lr |
ARM | r15 | pc |
RISC-V | x0 | zero |
RISC-V | x1 | ra |
RISC-V | x2 | sp |
RISC-V | x3 | gp |
RISC-V | x4 | tp |
RISC-V | x[5-7] | t[0-2] |
RISC-V | x8 | fp , s0 |
RISC-V | x9 | s1 |
RISC-V | x[10-17] | a[0-7] |
RISC-V | x[18-27] | s[2-11] |
RISC-V | x[28-31] | t[3-6] |
RISC-V | f[0-7] | ft[0-7] |
RISC-V | f[8-9] | fs[0-1] |
RISC-V | f[10-17] | fa[0-7] |
RISC-V | f[18-27] | fs[2-11] |
RISC-V | f[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) | 帧指针不能用作输入或输出。 |
ARM | r7 或 r11 | 在 ARM 上,帧指针可以是 r7 或 r11 ,具体取决于目标。帧指针不能用作输入或输出。 |
所有 | si (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64 和 Arm64EC), x9 (RISC-V), $s8 (LoongArch) | 这在内部被 LLVM 用作具有复杂堆栈帧的函数的“基指针”。 |
x86 | ip | 这是程序计数器,而不是真正的寄存器。 |
AArch64 | xzr | 这是一个常量零寄存器,无法修改。 |
AArch64 | x18 | 这是某些 AArch64 目标上的操作系统保留寄存器。 |
Arm64EC | xzr | 这是一个常量零寄存器,无法修改。 |
Arm64EC | x18 | 这是一个操作系统保留寄存器。 |
Arm64EC | x13 , x14 , x23 , x24 , x28 , v[16-31] , p[0-15] , ffr | 这些是 Arm64EC 不支持的 AArch64 寄存器。 |
ARM | pc | 这是程序计数器,而不是真正的寄存器。 |
ARM | r9 | 这是某些 ARM 目标上的操作系统保留寄存器。 |
RISC-V | x0 | 这是一个常量零寄存器,无法修改。 |
RISC-V | gp , tp | 这些寄存器是保留的,不能用作输入或输出。 |
LoongArch | $r0 或 $zero | 这是一个常量零寄存器,无法修改。 |
LoongArch | $r2 或 $tp | 这是为 TLS 保留的。 |
LoongArch | $r21 | 这是 ABI 保留的。 |
s390x | c[0-15] | 由内核保留。 |
s390x | a[0-1] | 保留供系统使用。 |
帧指针和基指针寄存器保留供 LLVM 内部使用。虽然 asm!
语句不能显式指定保留寄存器的使用,但在某些情况下,LLVM 会为 reg
操作数分配这些保留寄存器之一。使用保留寄存器的汇编代码应小心,因为 reg
操作数可能使用相同的寄存器。
模板修饰符
占位符可以通过修饰符来增强,修饰符在花括号中的 :
之后指定。这些修饰符不影响寄存器分配,但会更改操作数在插入到模板字符串时的格式。
每个模板占位符只允许一个修饰符。
支持的修饰符是 LLVM(和 GCC)的 asm 模板参数修饰符 的子集,但不使用相同的字母代码。
架构 | 寄存器类 | 修饰符 | 示例输出 | LLVM 修饰符 |
---|---|---|---|---|
x86-32 | reg | 无 | eax | k |
x86-64 | reg | 无 | rax | q |
x86-32 | reg_abcd | l | al | b |
x86-64 | reg | l | al | b |
x86 | reg_abcd | h | ah | h |
x86 | reg | x | ax | w |
x86 | reg | e | eax | k |
x86-64 | reg | r | rax | q |
x86 | reg_byte | 无 | al / ah | 无 |
x86 | xmm_reg | 无 | xmm0 | x |
x86 | ymm_reg | 无 | ymm0 | t |
x86 | zmm_reg | 无 | zmm0 | g |
x86 | *mm_reg | x | xmm0 | x |
x86 | *mm_reg | y | ymm0 | t |
x86 | *mm_reg | z | zmm0 | g |
x86 | kreg | 无 | k1 | 无 |
AArch64/Arm64EC | reg | 无 | x0 | x |
AArch64/Arm64EC | reg | w | w0 | w |
AArch64/Arm64EC | reg | x | x0 | x |
AArch64/Arm64EC | vreg | 无 | v0 | 无 |
AArch64/Arm64EC | vreg | v | v0 | 无 |
AArch64/Arm64EC | vreg | b | b0 | b |
AArch64/Arm64EC | vreg | h | h0 | h |
AArch64/Arm64EC | vreg | s | s0 | s |
AArch64/Arm64EC | vreg | d | d0 | d |
AArch64/Arm64EC | vreg | q | q0 | q |
ARM | reg | 无 | r0 | 无 |
ARM | sreg | 无 | s0 | 无 |
ARM | dreg | 无 | d0 | P |
ARM | qreg | 无 | q0 | q |
ARM | qreg | e / f | d0 / d1 | e / f |
RISC-V | reg | 无 | x1 | 无 |
RISC-V | freg | 无 | f0 | 无 |
LoongArch | reg | 无 | $r1 | 无 |
LoongArch | freg | 无 | $f0 | 无 |
s390x | reg | 无 | %r0 | 无 |
s390x | reg_addr | 无 | %r1 | 无 |
s390x | freg | 无 | %f0 | 无 |
注释:
- 在 ARM 上
e
/f
:这会打印 NEON quad(128 位)寄存器的低位或高位双字寄存器名称。- 在 x86 上:我们对于没有修饰符的
reg
的行为与 GCC 的行为不同。GCC 将根据操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。- 在 x86
xmm_reg
上:x
、t
和g
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 将继续是正确的。
选项
标志用于进一步影响内联汇编代码块的行为。当前定义了以下选项
pure
:asm!
代码块没有副作用,必须最终返回,并且其输出仅取决于其直接输入(即值本身,而不是它们指向的内容)或从内存读取的值(除非还设置了nomem
选项)。这允许编译器执行asm!
代码块的次数少于程序中指定的次数(例如,通过将其提升出循环)甚至完全消除它(如果未使用输出)。pure
选项必须与nomem
或readonly
选项结合使用,否则会发出编译时错误。
nomem
:asm!
代码块不从asm!
代码块外部可访问的任何内存读取或写入。这允许编译器在asm!
代码块之间将修改后的全局变量的值缓存在寄存器中,因为它知道asm!
不会读取或写入它们。编译器还假定此asm!
代码块不执行与其他线程的任何类型的同步,例如通过 fences。
readonly
:asm!
代码块不写入asm!
代码块外部可访问的任何内存。这允许编译器在asm!
代码块之间将未修改的全局变量的值缓存在寄存器中,因为它知道asm!
不会写入它们。编译器还假定此asm!
代码块不执行与其他线程的任何类型的同步,例如通过 fences。
preserves_flags
:asm!
代码块不修改标志寄存器(在下面的规则中定义)。这允许编译器避免在asm!
代码块之后重新计算条件标志。
noreturn
:asm!
代码块永远不会返回,并且其返回类型定义为!
(never)。如果执行流过了 asm 代码的末尾,则行为未定义。noreturn
asm 代码块的行为就像一个不返回的函数;值得注意的是,作用域内的局部变量在调用它之前不会被 drop。
nostack
:asm!
代码块不会将数据推送到堆栈,也不会写入堆栈 red-zone(如果目标支持)。如果不使用此选项,则保证堆栈指针已适当对齐(根据目标 ABI)以进行函数调用。
att_syntax
:此选项仅在 x86 上有效,并使汇编器使用 GNU 汇编器的.att_syntax prefix
模式。寄存器操作数在替换时带有前导%
。
raw
:这会导致模板字符串被解析为原始汇编字符串,对{
和}
没有特殊处理。当使用include_str!
从外部文件包含原始汇编代码时,这主要很有用。
编译器对选项执行一些额外的检查
nomem
和readonly
选项是互斥的:同时指定两者是编译时错误。
- 在没有输出或只有丢弃输出 (
_
) 的 asm 代码块上指定pure
是编译时错误。
- 在具有输出的 asm 代码块上指定
noreturn
是编译时错误。
global_asm!
仅支持 att_syntax
和 raw
选项。其余选项对于全局作用域内联汇编没有意义
内联汇编的规则
为避免未定义行为,使用函数作用域内联汇编 (asm!
) 时必须遵循这些规则
- 任何未指定为输入的寄存器在进入 asm 代码块时都将包含未定义的值。
- 在内联汇编的上下文中,“未定义的值”意味着寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM
undef
不同,后者每次读取时都可以具有不同的值(因为汇编代码中不存在这样的概念)。
- 在内联汇编的上下文中,“未定义的值”意味着寄存器可以(非确定性地)具有架构允许的任何可能值之一。值得注意的是,它与 LLVM
- 任何未指定为输出的寄存器在退出 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
- 在 x86 上,方向标志(
EFLAGS
中的 DF)在进入汇编代码块时被清除,并且在退出时必须被清除。- 如果在退出汇编代码块时设置了方向标志,则行为是未定义的。
- 在 x86 上,x87 浮点寄存器堆栈必须保持不变,除非所有
st([0-7])
寄存器都已标记为被破坏,例如out("st(0)") _, out("st(1)") _, ...
。- 如果所有 x87 寄存器都被破坏,则保证 x87 寄存器堆栈在进入
asm
代码块时为空。汇编代码必须确保 x87 寄存器堆栈在退出汇编代码块时也为空。
- 如果所有 x87 寄存器都被破坏,则保证 x87 寄存器堆栈在进入
- 在 arm64ec 上,当调用函数时,必须使用 带有适当 thunk 的调用检查器。
- 将堆栈指针和非输出寄存器恢复为其原始值的要求仅在退出
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