内联汇编
内联汇编支持通过 asm!
和 global_asm!
宏提供。它可以用来将手写的汇编代码嵌入到编译器生成的汇编代码输出中。
以下架构上的内联汇编支持是稳定的
- x86 和 x86-64
- ARM
- AArch64
- RISC-V
- LoongArch
如果在不支持的目标上使用 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
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
的寄存器。 - 您应该只在所有输入读取后写入寄存器,否则可能会覆盖输入。
- 与
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
具有相同的初始值,则可能会发生这种情况)。 - 您应该只在所有输入读取后写入寄存器,否则可能会覆盖输入。
- 与
sym <path>
<path>
必须引用一个fn
或static
。- 一个引用该项目的混淆符号名称将被替换到 asm 模板字符串中。
- 替换后的字符串不包含任何修饰符(例如 GOT、PLT、重定位等)。
<path>
允许指向一个#[thread_local]
静态变量,在这种情况下,asm 代码可以将符号与重定位(例如@plt
、@TPOFF
)组合起来以从线程局部数据中读取。
操作数表达式从左到右进行计算,就像函数调用参数一样。在 asm!
执行后,输出将从左到右的顺序写入。如果两个输出指向同一个位置,则这一点很重要:该位置将包含最右侧输出的值。
由于 global_asm!
存在于函数之外,因此它只能使用 sym
操作数。
寄存器操作数
输入和输出操作数可以指定为显式寄存器,也可以指定为寄存器类,寄存器分配器可以从中选择一个寄存器。显式寄存器指定为字符串字面量(例如 "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 | 仅 clobbers |
x86 | x87_reg | st([0-7]) | 仅 clobbers |
x86 | mmx_reg | mm[0-7] | 仅 clobbers |
x86-64 | tmm_reg | tmm[0-7] | 仅 clobbers |
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 | 仅 clobbers |
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] | 仅 clobbers |
LoongArch | reg | $r1 , $r[4-20] , $r[23,30] | r |
LoongArch | freg | $f[0-31] | f |
注释:
在 x86 上,我们对
reg_byte
的处理方式不同于reg
,因为编译器可以分别分配al
和ah
,而reg
会保留整个寄存器。在 x86-64 上,高字节寄存器(例如
ah
)在reg_byte
寄存器类中不可用。一些寄存器类被标记为“仅 clobbers”,这意味着这些类中的寄存器不能用于输入或输出,只能用于
out(<显式寄存器>) _
或lateout(<显式寄存器>) _
形式的 clobbers。
每个寄存器类都对可以使用的值类型有约束。这是必要的,因为将值加载到寄存器的方式取决于其类型。例如,在大端系统上,将 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 | 仅 clobbers |
x86 | x87_reg | N/A | 仅 clobbers |
x86 | tmm_reg | N/A | 仅 clobbers |
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 | 仅 clobbers |
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 | 仅 clobbers |
LoongArch64 | reg | 无 | i8 , i16 , i32 , i64 , f32 , f64 |
LoongArch64 | freg | 无 | f32 , f64 |
注意:对于上表,指针、函数指针和
isize
/usize
被视为等效的整数类型(i16
/i32
/i64
,具体取决于目标)。
如果一个值的大小小于分配给它的寄存器,那么该寄存器的较高位对于输入将具有未定义的值,对于输出将被忽略。唯一的例外是 RISC-V 上的 freg
寄存器类,其中 f32
值在 f64
中进行 NaN 装箱,这是 RISC-V 体系结构的要求。
当为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] |
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 | 在 asm 代码块结束时,必须将堆栈指针恢复到其原始值。 |
所有 | bp (x86), x29 (AArch64), x8 (RISC-V), $fp (LoongArch) | 帧指针不能用作输入或输出。 |
ARM | r7 或 r11 | 在 ARM 上,帧指针可以是r7 或r11 ,具体取决于目标。帧指针不能用作输入或输出。 |
所有 | si (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64), x9 (RISC-V), $s8 (LoongArch) | LLVM 在内部使用它作为具有复杂堆栈帧的函数的“基指针”。 |
x86 | ip | 这是程序计数器,而不是真正的寄存器。 |
AArch64 | xzr | 这是一个常量零寄存器,不能修改。 |
AArch64 | x18 | 这是某些 AArch64 目标上的操作系统保留寄存器。 |
ARM | pc | 这是程序计数器,而不是真正的寄存器。 |
ARM | r9 | 这是某些 ARM 目标上的操作系统保留寄存器。 |
RISC-V | x0 | 这是一个常量零寄存器,不能修改。 |
RISC-V | gp , tp | 这些寄存器是保留的,不能用作输入或输出。 |
LoongArch | $r0 或 $zero | 这是一个常量零寄存器,不能修改。 |
LoongArch | $r2 或 $tp | 这是为 TLS 保留的。 |
LoongArch | $r21 | 这是 ABI 保留的。 |
帧指针和基指针寄存器是为 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 | reg | 无 | x0 | x |
AArch64 | reg | w | w0 | w |
AArch64 | reg | x | x0 | x |
AArch64 | vreg | 无 | v0 | 无 |
AArch64 | vreg | v | v0 | 无 |
AArch64 | vreg | b | b0 | b |
AArch64 | vreg | h | h0 | h |
AArch64 | vreg | s | s0 | s |
AArch64 | vreg | d | d0 | d |
AArch64 | 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 | 无 |
注释:
- 在 ARM 上
e
/f
:这将打印 NEON 四元组(128 位)寄存器的低位或高位双字寄存器名称。- 在 x86 上:我们对没有修饰符的
reg
的行为与 GCC 不同。GCC 会根据操作数的值类型推断修饰符,而我们默认使用完整的寄存器大小。- 在 x86
xmm_reg
上:x
、t
和g
LLVM 修饰符尚未在 LLVM 中实现(它们仅受 GCC 支持),但这应该是一个简单的更改。
如上一节所述,传递小于寄存器宽度的输入值会导致寄存器的较高位包含未定义的值。如果内联汇编只访问寄存器的较低位,则这不是问题,这可以通过使用模板修饰符在汇编代码中使用子寄存器名称来完成(例如,使用ax
而不是rax
)。由于这是一个容易出现的陷阱,因此编译器会在适当的情况下建议使用模板修饰符。如果对操作数的所有引用都已具有修饰符,则会抑制针对该操作数的警告。
ABI 覆盖
clobber_abi
关键字可用于将默认覆盖集应用于asm!
块。这将根据需要自动插入必要的覆盖约束,以使用特定调用约定调用函数:如果调用约定在调用期间没有完全保留寄存器的值,则lateout("...") _
会隐式添加到操作数列表中(其中...
被替换为寄存器的名称)。
clobber_abi
可以指定任意次数。它将为所有指定调用约定的并集中的所有唯一寄存器插入覆盖。
当使用clobber_abi
时,编译器不允许使用通用寄存器类输出:所有输出都必须指定显式寄存器。显式寄存器输出优先于clobber_abi
插入的隐式覆盖:只有当寄存器不用作输出时,才会为该寄存器插入覆盖。以下 ABI 可以与clobber_abi
一起使用
体系结构 | ABI 名称 | 覆盖的寄存器 |
---|---|---|
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 |
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
仅在目标上不被视为保留寄存器时才包含在覆盖列表中。
每个 ABI 的覆盖寄存器列表在 rustc 中更新,因为架构获得了新的寄存器:这确保了当 LLVM 开始在其生成的代码中使用这些新寄存器时,asm!
覆盖将继续保持正确。
选项
标志用于进一步影响内联汇编块的行为。目前定义了以下选项
pure
:asm!
块没有副作用,必须最终返回,其输出仅取决于其直接输入(即值本身,而不是它们指向的内容)或从内存中读取的值(除非还设置了nomem
选项)。这允许编译器执行asm!
块的次数少于程序中指定的次数(例如,通过将其提升到循环之外),或者如果输出未被使用,则完全消除它。pure
选项必须与nomem
或readonly
选项结合使用,否则会发出编译时错误。nomem
:asm!
代码块不读取或写入任何内存。这允许编译器在asm!
代码块中将修改后的全局变量的值缓存到寄存器中,因为它知道asm!
代码块不会读取或写入这些变量。编译器还假设此asm!
代码块不会执行任何与其他线程同步的操作,例如通过栅栏。readonly
:asm!
代码块不写入任何内存。这允许编译器在asm!
代码块中将未修改的全局变量的值缓存到寄存器中,因为它知道asm!
代码块不会写入这些变量。编译器还假设此asm!
代码块不会执行任何与其他线程同步的操作,例如通过栅栏。preserves_flags
:asm!
代码块不会修改标志寄存器(在下面的规则中定义)。这允许编译器避免在asm!
代码块之后重新计算条件标志。noreturn
:asm!
代码块永远不会返回,其返回类型定义为!
(永不)。如果执行从 asm 代码的末尾继续执行,则行为未定义。noreturn
asm 代码块的行为就像一个不返回的函数;值得注意的是,在调用它之前不会丢弃作用域内的局部变量。nostack
:asm!
代码块不会将数据推送到堆栈,也不会写入堆栈红色区域(如果目标支持)。如果不使用此选项,则堆栈指针保证适合函数调用的对齐方式(根据目标 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 代码块中展开,则行为未定义。
- 这也适用于汇编代码调用一个函数,然后该函数展开。
- 汇编代码允许读取和写入的内存位置集与 FFI 函数允许的内存位置集相同。
- 有关确切规则,请参阅不安全代码指南。
- 如果设置了
readonly
选项,则仅允许内存读取。 - 如果设置了
nomem
选项,则不允许读取或写入内存。 - 这些规则不适用于 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
- 在 x86 上,方向标志(
EFLAGS
中的 DF)在进入 asm 代码块时为清除状态,并且在退出时必须为清除状态。- 如果在退出 asm 代码块时方向标志被设置,则行为未定义。
- 在 x86 上,x87 浮点寄存器堆栈必须保持不变,除非所有
st([0-7])
寄存器都被标记为使用out("st(0)") _, out("st(1)") _, ...
覆盖。- 如果所有 x87 寄存器都被覆盖,则保证在进入
asm
代码块时 x87 寄存器堆栈为空。汇编代码必须确保在退出 asm 代码块时 x87 寄存器堆栈也为空。
- 如果所有 x87 寄存器都被覆盖,则保证在进入
- 将堆栈指针和非输出寄存器恢复到其原始值的 requirement 仅适用于退出
asm!
代码块时。- 这意味着永远不会返回的
asm!
代码块(即使没有标记为noreturn
)也不需要保留这些寄存器。 - 在返回到与您进入的
asm!
代码块不同的asm!
代码块时(例如,用于上下文切换),这些寄存器必须包含它们在您退出的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
.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