内联汇编
通过 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> / label <block>
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! 宏时,汇编代码会在函数作用域内发出,并集成到编译器生成的函数汇编代码中。这段汇编代码必须遵守严格的规则,以避免未定义行为。请注意,在某些情况下,编译器可能会选择将汇编代码作为单独的函数发出并生成对其的调用。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { unsafe { core::arch::asm!("/* {} */", in(reg) 0); } } }
使用 global_asm! 宏时,汇编代码会在全局作用域内发出,位于函数之外。这可以用来完全手写汇编函数,并且通常在使用任意寄存器和汇编器指令方面提供更多自由。
fn main() {} #[cfg(target_arch = "x86_64")] core::arch::global_asm!("/* {} */", const 0);
模板字符串参数
汇编器模板使用与格式字符串相同的语法(即占位符由花括号指定)。
相应的参数按顺序、按索引或按名称访问。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64; let y: i64; let z: i64; // This unsafe { core::arch::asm!("mov {}, {}", out(reg) x, in(reg) 5); } // ... this unsafe { core::arch::asm!("mov {0}, {1}", out(reg) y, in(reg) 5); } // ... and this unsafe { core::arch::asm!("mov {out}, {in}", out = out(reg) z, in = in(reg) 5); } // all have the same behavior assert_eq!(x, y); assert_eq!(y, z); } }
然而,不支持隐式命名参数(由 RFC #2795 引入)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5; // We can't refer to `x` from the scope directly, we need an operand like `in(reg) x` unsafe { core::arch::asm!("/* {x} */"); } // ERROR: no argument named x } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
一个 asm! 调用可以有一个或多个模板字符串参数;包含多个模板字符串参数的 asm! 被视为所有字符串之间用 \n 连接起来。预期的用法是每个模板字符串参数对应一行汇编代码。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64; let y: i64; // We can separate multiple strings as if they were written together unsafe { core::arch::asm!("mov eax, 5", "mov ecx, eax", out("rax") x, out("rcx") y); } assert_eq!(x, y); } }
所有模板字符串参数必须出现在任何其他参数之前。
#![allow(unused)] fn main() { let x = 5; #[cfg(target_arch = "x86_64")] { // The template strings need to appear first in the asm invocation unsafe { core::arch::asm!("/* {x} */", x = const 5, "ud2"); } // ERROR: unexpected token } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
与格式字符串一样,位置参数必须出现在命名参数和显式寄存器操作数之前。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5; // Named operands need to come after positional ones unsafe { core::arch::asm!("/* {x} {} */", x = const 5, in(reg) 5); } // ERROR: positional arguments cannot follow named arguments or explicit register arguments } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5; // We also can't put explicit registers before positional operands unsafe { core::arch::asm!("/* {} */", in("eax") 0, in(reg) 5); } // ERROR: positional arguments cannot follow named arguments or explicit register arguments } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
显式寄存器操作数不能在模板字符串中用作占位符。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5; // Explicit register operands don't get substituted, use `eax` explicitly in the string unsafe { core::arch::asm!("/* {} */", in("eax") 5); } // ERROR: invalid reference to argument at index 0 } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
所有其他命名和位置操作数必须在模板字符串中至少出现一次,否则会生成编译器错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5; // We have to name all of the operands in the format string unsafe { core::arch::asm!("", in(reg) 5, x = const 5); } // ERROR: multiple unused asm arguments } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
确切的汇编代码语法是目标特定的,对于编译器来说是不透明的,只有操作数如何替换到模板字符串中以形成传递给汇编器的代码这一方式除外。
当前,所有支持的目标都遵循 LLVM 内部汇编器使用的汇编代码语法,该语法通常与 GNU 汇编器 (GAS) 的语法一致。在 x86 上,默认使用 GAS 的 .intel_syntax noprefix 模式。在 ARM 上,使用 .syntax unified 模式。这些目标对汇编代码施加了额外的限制:任何汇编器状态(例如,可以通过 .section 更改的当前节)必须在汇编字符串结束时恢复到其原始值。不符合 GAS 语法的汇编代码将导致特定于汇编器的行为。内联汇编使用的指令的进一步约束由指令支持指示。
操作数类型
支持几种类型的操作数
in(<reg>) <expr><reg>可以指代寄存器类或显式寄存器。分配的寄存器名称会被替换到汇编模板字符串中。- 分配的寄存器在汇编代码开始时将包含
<expr>的值。 - 分配的寄存器在汇编代码结束时必须包含相同的值(除非同一个寄存器被分配为
lateout)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // ``in` can be used to pass values into inline assembly... unsafe { core::arch::asm!("/* {} */", in(reg) 5); } } }
out(<reg>) <expr><reg>可以指代寄存器类或显式寄存器。分配的寄存器名称会被替换到汇编模板字符串中。- 分配的寄存器在汇编代码开始时将包含未定义的值。
<expr>必须是一个(可能未初始化的)位置表达式,分配的寄存器的内容会在汇编代码结束时写入其中。- 可以指定下划线 (
_) 代替表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(实际上起到了 clobber 的作用)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64; // and `out` can be used to pass values back to rust. unsafe { core::arch::asm!("/* {} */", out(reg) x); } } }
lateout(<reg>) <expr>- 与
out相同,不同之处在于寄存器分配器可以重用分配给in的寄存器。 - 你应该只在所有输入都被读取后才写入该寄存器,否则可能会 clobber 一个输入。
- 与
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64; // `lateout` is the same as `out` // but the compiler knows we don't care about the value of any inputs by the // time we overwrite it. unsafe { core::arch::asm!("mov {}, 5", lateout(reg) x); } assert_eq!(x, 5) } }
inout(<reg>) <expr><reg>可以指代寄存器类或显式寄存器。分配的寄存器名称会被替换到汇编模板字符串中。- 分配的寄存器在汇编代码开始时将包含
<expr>的值。 <expr>必须是一个可变的已初始化位置表达式,分配的寄存器的内容会在汇编代码结束时写入其中。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut x: i64 = 4; // `inout` can be used to modify values in-register unsafe { core::arch::asm!("inc {}", inout(reg) x); } assert_eq!(x, 5); } }
inout(<reg>) <in expr> => <out expr>- 与
inout相同,不同之处在于寄存器的初始值取自<in expr>的值。 <out expr>必须是一个(可能未初始化的)位置表达式,分配的寄存器的内容会在汇编代码结束时写入其中。- 可以指定下划线 (
_) 代替<out expr>的表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(实际上起到了 clobber 的作用)。 <in expr>和<out expr>可以有不同的类型。
- 与
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64; // `inout` can also move values to different places unsafe { core::arch::asm!("inc {}", inout(reg) 4u64=>x); } assert_eq!(x, 5); } }
inlateout(<reg>) <expr>/inlateout(<reg>) <in expr> => <out expr>- 与
inout相同,不同之处在于寄存器分配器可以重用分配给in的寄存器(如果编译器知道in与inlateout具有相同的初始值,则可能发生这种情况)。 - 你应该只在所有输入都被读取后才写入该寄存器,否则可能会 clobber 一个输入。
- 与
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut x: i64 = 4; // `inlateout` is `inout` using `lateout` unsafe { core::arch::asm!("inc {}", inlateout(reg) x); } assert_eq!(x, 5); } }
sym <path><path>必须引用一个fn或static。- 引用该项的经过修饰的符号名称会被替换到汇编模板字符串中。
- 替换后的字符串不包含任何修饰符(例如 GOT、PLT、重定位等)。
<path>允许指向#[thread_local]静态变量,在这种情况下,汇编代码可以将符号与重定位(例如@plt、@TPOFF)结合起来从线程本地数据中读取。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { extern "C" fn foo() { println!("Hello from inline assembly") } // `sym` can be used to refer to a function (even if it doesn't have an // external name we can directly write) unsafe { core::arch::asm!("call {}", sym foo, clobber_abi("C")); } } }
const <expr><expr>必须是一个整数常量表达式。该表达式遵循与内联const块相同的规则。- 表达式的类型可以是任何整数类型,但默认是
i32,就像整数字面量一样。 - 表达式的值会被格式化为字符串,并直接替换到汇编模板字符串中。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // swizzle [0, 1, 2, 3] => [3, 2, 0, 1] const SHUFFLE: u8 = 0b01_00_10_11; let x: core::arch::x86_64::__m128 = unsafe { core::mem::transmute([0u32, 1u32, 2u32, 3u32]) }; let y: core::arch::x86_64::__m128; // Pass a constant value into an instruction that expects an immediate like `pshufd` unsafe { core::arch::asm!("pshufd {xmm}, {xmm}, {shuffle}", xmm = inlateout(xmm_reg) x=>y, shuffle = const SHUFFLE ); } let y: [u32; 4] = unsafe { core::mem::transmute(y) }; assert_eq!(y, [3, 2, 0, 1]); } }
label <block>- 代码块的地址被替换到汇编模板字符串中。汇编代码可以跳转到替换后的地址。
- 代码块执行完成后,
asm!表达式返回。 - 代码块的类型必须是 unit 或
!(never)。 - 该代码块开始一个新的安全上下文;
label块内的非安全操作必须包裹在一个内部unsafe块中,即使整个asm!表达式已经包裹在unsafe中。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] unsafe { core::arch::asm!("jmp {}", label { println!("Hello from inline assembly label"); }); } }
操作数表达式从左到右求值,就像函数调用参数一样。在 asm! 执行完毕后,输出会按从左到右的顺序写入。如果两个输出指向同一个位置,这一点很重要:该位置将包含最右边输出的值。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut y: i64; // y gets its value from the second output, rather than the first unsafe { core::arch::asm!("mov {}, 0", "mov {}, 1", out(reg) y, out(reg) y); } assert_eq!(y, 1); } }
由于 global_asm! 存在于函数外部,它只能使用 sym 和 const 操作数。
fn main() {} let x = 5; // register operands aren't allowed, since we aren't in a function #[cfg(target_arch = "x86_64")] core::arch::global_asm!("", in(reg) 5); // ERROR: the `in` operand cannot be used with `global_asm!` #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
fn main() {} fn foo() {} #[cfg(target_arch = "x86_64")] // `const` and `sym` are both allowed, however core::arch::global_asm!("/* {} {} */", const 0, sym foo);
寄存器操作数
输入和输出操作数可以指定为显式寄存器,也可以指定为寄存器类,寄存器分配器可以从其中选择一个寄存器。显式寄存器指定为字符串字面量(例如 "eax"),而寄存器类指定为标识符(例如 reg)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut y: i64; // We can name both `reg`, or an explicit register like `eax` to get an // integer register unsafe { core::arch::asm!("mov eax, {:e}", in(reg) 5, lateout("eax") y); } assert_eq!(y, 5); } }
请注意,显式寄存器将寄存器别名(例如 ARM 上的 r14 对比 lr)和寄存器的较小视图(例如 eax 对比 rax)视为与基本寄存器等效。
对两个输入操作数或两个输出操作数使用相同的显式寄存器会导致编译时错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // We can't name eax twice unsafe { core::arch::asm!("", in("eax") 5, in("eax") 4); } // ERROR: register `eax` conflicts with register `eax` } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // ... even using different aliases unsafe { core::arch::asm!("", in("ax") 5, in("rax") 4); } // ERROR: register `rax` conflicts with register `ax` } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
此外,在输入操作数或输出操作数中使用重叠的寄存器(例如 ARM VFP)也会导致编译时错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5; // al overlaps with ax, so we can't name both of them. unsafe { core::arch::asm!("", in("ax") 5, in("al") 4i8); } // ERROR: register `al` conflicts with register `ax` } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
内联汇编只允许以下类型作为操作数
- 整数(有符号和无符号)
- 浮点数
- 指针(仅限瘦指针)
- 函数指针
- SIMD 向量(使用
#[repr(simd)]定义且实现了Copy的结构体)。这包括在std::arch中定义的架构特定向量类型,例如__m128(x86) 或int8x16_t(ARM)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { extern "C" fn foo() {} // Integers are allowed... let y: i64 = 5; unsafe { core::arch::asm!("/* {} */", in(reg) y); } // and pointers... let py = &raw const y; unsafe { core::arch::asm!("/* {} */", in(reg) py); } // floats as well... let f = 1.0f32; unsafe { core::arch::asm!("/* {} */", in(xmm_reg) f); } // even function pointers and simd vectors. let func: extern "C" fn() = foo; unsafe { core::arch::asm!("/* {} */", in(reg) func); } let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) }; unsafe { core::arch::asm!("/* {} */", in(xmm_reg) z); } } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { struct Foo; let x: Foo = Foo; // Complex types like structs are not allowed unsafe { core::arch::asm!("/* {} */", in(reg) x); } // ERROR: cannot use value of type `Foo` for inline assembly } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
以下是当前支持的寄存器类列表
| 架构 | 寄存器类 | 寄存器 | 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。
每个寄存器类都有关于它们可以与哪些值类型一起使用的约束。这是必需的,因为值加载到寄存器中的方式取决于其类型。例如,在大端系统中,将 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, f64x2i8x32, i16x16, i32x8, i64x4, f32x8, f64x4 |
| x86 | zmm_reg | avx512f | i32, f32, i64, f64,i8x16, i16x8, i32x4, i64x2, f32x4, f64x2i8x32, i16x16, i32x8, i64x4, f32x8, f64x4i8x64, i16x32, i32x16, i64x8, f32x16, f64x8 |
| x86 | kreg | avx512f | i8, i16 |
| x86 | kreg | avx512bw | i32, i64 |
| x86 | mmx_reg | 不适用 | 仅用于 clobber |
| x86 | x87_reg | 不适用 | 仅用于 clobber |
| x86 | tmm_reg | 不适用 | 仅用于 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 | 不适用 | 仅用于 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 | 不适用 | 仅用于 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 | 不适用 | 仅用于 clobber |
| s390x | areg | 不适用 | 仅用于 clobber |
注意
对于上表,指针、函数指针和
isize/usize被视为等效的整数类型(根据目标是i16/i32/i64)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x = 5i32; let y = -1i8; let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) }; // reg is valid for `i32`, `reg_byte` is valid for `i8`, and xmm_reg is valid for `__m128i` // We can't use `tmm0` as an input or output, but we can clobber it. unsafe { core::arch::asm!("/* {} {} {} */", in(reg) x, in(reg_byte) y, in(xmm_reg) z, out("tmm0") _); } } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let z = unsafe { core::arch::x86_64::_mm_set_epi64x(1, 0) }; // We can't pass an `__m128i` to a `reg` input unsafe { core::arch::asm!("/* {} */", in(reg) z); } // ERROR: type `__m128i` cannot be used with this register class } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
如果值的大小小于分配给它的寄存器大小,那么对于输入,该寄存器的上层位将包含未定义的值,对于输出则会被忽略。唯一的例外是 RISC-V 上的 freg 寄存器类,其中 f32 值根据 RISC-V 架构的要求在 f64 中进行 NaN 封装。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut x: i64; // Moving a 32-bit value into a 64-bit value, oops. #[allow(asm_sub_register)] // rustc warns about this behavior unsafe { core::arch::asm!("mov {}, {}", lateout(reg) x, in(reg) 4i32); } // top 32-bits are indeterminate assert_eq!(x, 4); // This assertion is not guaranteed to succeed assert_eq!(x & 0xFFFFFFFF, 4); // However, this one will succeed } }
当为 inout 操作数指定单独的输入和输出表达式时,两个表达式必须具有相同的类型。唯一的例外是如果两个操作数都是指针或整数,在这种情况下,它们只需要具有相同的大小。存在此限制是因为 LLVM 和 GCC 中的寄存器分配器有时无法处理具有不同类型的 tied 操作数。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // Pointers and integers can mix (as long as they are the same size) let x: isize = 0; let y: *mut (); // Transmute an `isize` to a `*mut ()`, using inline assembly magic unsafe { core::arch::asm!("/*{}*/", inout(reg) x=>y); } assert!(y.is_null()); // Extremely roundabout way to make a null pointer } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i32 = 0; let y: f32; // But we can't reinterpret an `i32` to an `f32` like this unsafe { core::arch::asm!("/* {} */", inout(reg) x=>y); } // ERROR: incompatible types for asm inout argument } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
寄存器名称
一些寄存器有多个名称。编译器将它们都视为与基本寄存器名称相同。以下是所有支持的寄存器别名列表
| 架构 | 基本寄存器 | 别名 |
|---|---|---|
| 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] |
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let z = 0i64; // rax is an alias for eax and ax unsafe { core::arch::asm!("", in("rax") z); } } }
某些寄存器不能用于输入或输出操作数
| 架构 | 不支持的寄存器 | 原因 |
|---|---|---|
| 全部 | sp, r15 (s390x) | 栈指针在汇编代码结束时或跳转到 label 块之前必须恢复到其原始值。 |
| 全部 | bp (x86), x29 (AArch64 和 Arm64EC), x8 (RISC-V), $fp (LoongArch), r11 (s390x) | 帧指针不能用作输入或输出。 |
| ARM | r7 或 r11 (ARM) | 在 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 目标上,这是 OS 保留的寄存器。 |
| Arm64EC | xzr | 这是一个常量零寄存器,不能被修改。 |
| Arm64EC | x18 | 这是 OS 保留的寄存器。 |
| Arm64EC | x13, x14, x23, x24, x28, v[16-31], p[0-15], ffr | 这些是 AArch64 寄存器,Arm64EC 不支持它们。 |
| ARM | pc | 这是程序计数器,不是真实的寄存器。 |
| ARM | r9 | 在某些 ARM 目标上,这是 OS 保留的寄存器。 |
| RISC-V | x0 | 这是一个常量零寄存器,不能被修改。 |
| RISC-V | gp, tp (RISC-V) | 这些寄存器是保留的,不能用作输入或输出。 |
| LoongArch | $r0 或 $zero (LoongArch) | 这是一个常量零寄存器,不能被修改。 |
| LoongArch | $r2 或 $tp (LoongArch) | 这是为 TLS 保留的。 |
| LoongArch | $r21 | 这是由 ABI 保留的。 |
| s390x | c[0-15] (s390x) | 由内核保留。 |
| s390x | a[0-1] (s390x) | 保留供系统使用。 |
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // bp is reserved unsafe { core::arch::asm!("", in("bp") 5i32); } // ERROR: invalid register `bp`: the frame pointer cannot be used as an operand for inline asm } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
帧指针和基指针寄存器保留供 LLVM 内部使用。虽然 asm! 语句不能显式指定使用保留寄存器,但在某些情况下 LLVM 会为 reg 操作数分配其中一个保留寄存器。使用保留寄存器的汇编代码应该小心,因为 reg 操作数可能使用相同的寄存器。
模板修饰符
占位符可以通过修饰符进行增强,修饰符在花括号中的 : 后面指定。这些修饰符不影响寄存器分配,但会改变操作数插入模板字符串时的格式。
每个模板占位符只允许一个修饰符。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // We can't specify both `r` and `e` at the same time. unsafe { core::arch::asm!("/* {:er}", in(reg) 5i32); } // ERROR: asm template modifier must be a single character } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
支持的修饰符是 LLVM(和 GCC)汇编模板参数修饰符的子集,但不使用相同的字母代码。
| 架构 | 寄存器类 | 修饰符 | 示例输出 | 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 四字(128 位)寄存器的低或高双字寄存器名称。- 在 x86 上:我们对没有修饰符的
reg的行为与 GCC 的行为不同。GCC 会根据操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。- 在 x86 的
xmm_reg上:x、t和g这些 LLVM 修饰符尚未在 LLVM 中实现(它们仅由 GCC 支持),但这应该是一个简单的更改。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut x = 0x10u16; // u16::swap_bytes using `xchg` // low half of `{x}` is referred to by `{x:l}`, and the high half by `{x:h}` unsafe { core::arch::asm!("xchg {x:l}, {x:h}", x = inout(reg_abcd) x); } assert_eq!(x, 0x1000u16); } }
如前一节所述,传递一个小于寄存器宽度大小的输入值将导致寄存器的上层位包含未定义的值。如果内联汇编只访问寄存器的低层位,这不是问题,可以通过使用模板修饰符在汇编代码中使用子寄存器名称(例如 ax 而不是 rax)来做到这一点。由于这是一个容易出错的地方,编译器会根据输入类型在适当的地方建议使用的模板修饰符。如果对操作数的所有引用都已包含修饰符,则会抑制该操作数的警告。
ABI clobbers
clobber_abi 关键字可用于对汇编代码应用一组默认的 clobber。这将根据需要自动插入调用具有特定调用约定的函数所需的 clobber 约束:如果调用约定未在调用期间完全保留寄存器的值,则会隐式将 lateout("...") _ 添加到操作数列表中(其中 ... 被寄存器名称替换)。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { extern "C" fn foo() -> i32 { 0 } let z: i32; // To call a function, we have to inform the compiler that we're clobbering // callee saved registers unsafe { core::arch::asm!("call {}", sym foo, out("rax") z, clobber_abi("C")); } assert_eq!(z, 0); } }
clobber_abi 可以指定任意次数。它将为所有指定调用约定的并集中的所有唯一寄存器插入 clobber。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { extern "sysv64" fn foo() -> i32 { 0 } extern "win64" fn bar(x: i32) -> i32 { x + 1} let z: i32; // We can even call multiple functions with different conventions and // different saved registers unsafe { core::arch::asm!( "call {}", "mov ecx, eax", "call {}", sym foo, sym bar, out("rax") z, clobber_abi("C") ); } assert_eq!(z, 1); } }
使用 clobber_abi 时,编译器不允许使用泛型寄存器类输出:所有输出必须指定显式寄存器。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { extern "C" fn foo(x: i32) -> i32 { 0 } let z: i32; // explicit registers must be used to not accidentally overlap. unsafe { core::arch::asm!( "mov eax, {:e}", "call {}", out(reg) z, sym foo, clobber_abi("C") ); // ERROR: asm with `clobber_abi` must specify explicit registers for outputs } assert_eq!(z, 0); } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
显式寄存器输出优先于 clobber_abi 插入的隐式 clobber:只有当寄存器未用作输出时,才会为其插入 clobber。
以下 ABI 可与 clobber_abi 一起使用
| 架构 | ABI 名称 | 被 clobber 的寄存器 |
|---|---|---|
| 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 列表中。
rustc 中会更新每个 ABI 的 clobber 寄存器列表,因为架构会增加新的寄存器:这确保了当 LLVM 开始在其生成的代码中使用这些新寄存器时,asm! 的 clobber 将保持正确。
选项
标志用于进一步影响内联汇编代码的行为。目前定义了以下选项
pure:汇编代码没有副作用,最终必须返回,并且其输出仅取决于其直接输入(即值本身,而不是它们指向的内容)或从内存读取的值(除非同时设置了nomem选项)。这允许编译器执行汇编代码的次数少于程序中指定的次数(例如,通过将其提升出循环),如果输出未使用,甚至可以完全消除它。pure选项必须与nomem或readonly选项之一结合使用,否则会发出编译时错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i32 = 0; let z: i32; // pure can be used to optimize by assuming the assembly has no side effects unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure, nomem)); } assert_eq!(z, 1); } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i32 = 0; let z: i32; // Either nomem or readonly must be satisfied, to indicate whether or not // memory is allowed to be read unsafe { core::arch::asm!("inc {}", inout(reg) x => z, options(pure)); } // ERROR: the `pure` option must be combined with either `nomem` or `readonly` assert_eq!(z, 0); } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
nomem:汇编代码不读写任何汇编代码外部可访问的内存。这使得编译器可以在汇编代码执行期间将修改的全局变量的值缓存在寄存器中,因为它知道汇编代码不会读写它们。编译器还假定汇编代码不执行任何与其它线程的同步操作,例如通过 fences。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut x = 0i32; let z: i32; // Accessing outside memory from assembly when `nomem` is // specified is disallowed unsafe { core::arch::asm!("mov {val:e}, dword ptr [{ptr}]", ptr = in(reg) &mut x, val = lateout(reg) z, options(nomem) ) } // Writing to outside memory from assembly when `nomem` is // specified is also undefined behaviour unsafe { core::arch::asm!("mov dword ptr [{ptr}], {val:e}", ptr = in(reg) &mut x, val = in(reg) z, options(nomem) ) } } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i32 = 0; let z: i32; // If we allocate our own memory, such as via `push`, however. // we can still use it unsafe { core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}", x = inout(reg) x => z, options(nomem) ); } assert_eq!(z, 1); } }
readonly:汇编代码不写入任何汇编代码外部可访问的内存。这使得编译器可以在汇编代码执行期间将未修改的全局变量的值缓存在寄存器中,因为它知道汇编代码不会写入它们。编译器还假定这段汇编代码不执行任何与其它线程的同步操作,例如通过 fences。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let mut x = 0; // We cannot modify outside memory when `readonly` is specified unsafe { core::arch::asm!("mov dword ptr[{}], 1", in(reg) &mut x, options(readonly)) } } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64 = 0; let z: i64; // We can still read from it, though unsafe { core::arch::asm!("mov {x}, qword ptr [{x}]", x = inout(reg) &x => z, options(readonly) ); } assert_eq!(z, 0); } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i64 = 0; let z: i64; // Same exception applies as with nomem. unsafe { core::arch::asm!("push {x}", "add qword ptr [rsp], 1", "pop {x}", x = inout(reg) x => z, options(readonly) ); } assert_eq!(z, 1); } }
preserves_flags:汇编代码不修改标志寄存器(定义见下面的规则)。这使得编译器可以避免在汇编代码执行后重新计算条件标志。
noreturn:汇编代码不会向下执行;如果向下执行,行为是未定义的。它仍然可以跳转到label块。如果任何label块返回 unit,则asm!块将返回 unit。否则将返回!(never)。与调用不返回的函数一样,汇编代码执行前不会丢弃作用域内的局部变量。
fn main() -> ! { #[cfg(target_arch = "x86_64")] { // We can use an instruction to trap execution inside of a noreturn block unsafe { core::arch::asm!("ud2", options(noreturn)); } } #[cfg(not(target_arch = "x86_64"))] panic!("no return"); }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // You are responsible for not falling past the end of a noreturn asm block unsafe { core::arch::asm!("", options(noreturn)); } } }
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] let _: () = unsafe { // You may still jump to a `label` block core::arch::asm!("jmp {}", label { println!(); }, options(noreturn)); }; }
nostack:汇编代码不向栈推送数据,也不写入栈红区(如果目标支持)。如果不使用此选项,则保证栈指针会根据目标 ABI 为函数调用进行适当对齐。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // `push` and `pop` are UB when used with nostack unsafe { core::arch::asm!("push rax", "pop rax", options(nostack)); } } }
att_syntax:此选项仅在 x86 上有效,并导致汇编器使用 GNU 汇编器的.att_syntax prefix模式。寄存器操作数在替换时会带有一个前导的%。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let x: i32; let y = 1i32; // We need to use AT&T Syntax here. src, dest order for operands unsafe { core::arch::asm!("mov {y:e}, {x:e}", x = lateout(reg) x, y = in(reg) y, options(att_syntax) ); } assert_eq!(x, y); } }
raw:这使得模板字符串被解析为原始汇编字符串,不对{和}进行特殊处理。这主要用于使用include_str!包含来自外部文件的原始汇编代码。
编译器对选项执行一些额外的检查
nomem和readonly选项是互斥的:同时指定两者会导致编译时错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // nomem is strictly stronger than readonly, they can't be specified together unsafe { core::arch::asm!("", options(nomem, readonly)); } // ERROR: the `nomem` and `readonly` options are mutually exclusive } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
- 在没有输出或只有被丢弃输出 (
_) 的汇编块上指定pure会导致编译时错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { // pure blocks need at least one output unsafe { core::arch::asm!("", options(pure)); } // ERROR: asm with the `pure` option must have at least one output } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
- 在有输出且没有 labels 的汇编块上指定
noreturn会导致编译时错误。
- 在有输出的汇编块中包含任何
label块会导致编译时错误。
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let z: i32; // noreturn can't have outputs unsafe { core::arch::asm!("mov {:e}, 1", out(reg) z, options(noreturn)); } // ERROR: asm outputs are not allowed with the `noreturn` option } #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch"); }
global_asm! 只支持 att_syntax 和 raw 选项。其余选项对于全局作用域的内联汇编没有意义。
fn main() {} #[cfg(target_arch = "x86_64")] // nomem is useless on global_asm! core::arch::global_asm!("", options(nomem)); #[cfg(not(target_arch = "x86_64"))] core::compile_error!("Test not supported on this arch");
内联汇编规则
为了避免未定义行为,在使用函数作用域内联汇编 (asm!) 时必须遵守这些规则
- 进入汇编代码时,任何未指定为输入的寄存器将包含未定义的值。
- 在内联汇编上下文中,“未定义的值”意味着寄存器可以(非确定性地)拥有架构允许的任何可能值。值得注意的是,它与 LLVM 的
undef不同,后者每次读取时都可以具有不同的值(因为在汇编代码中不存在这种概念)。
- 在内联汇编上下文中,“未定义的值”意味着寄存器可以(非确定性地)拥有架构允许的任何可能值。值得注意的是,它与 LLVM 的
- 任何未指定为输出的寄存器在退出汇编代码时必须与进入时具有相同的值,否则行为是未定义的。
- 这仅适用于可以指定为输入或输出的寄存器。其他寄存器遵循目标特定的规则。
- 请注意,
lateout可能与in分配到同一个寄存器,在这种情况下,此规则不适用。但是,代码不应依赖于此,因为它取决于寄存器分配的结果。
- 如果执行从汇编代码中展开(unwind),行为是未定义的。
- 如果汇编代码调用一个函数,而该函数随后展开,此规则也适用。
- 汇编代码允许读写的内存位置集合与 FFI 函数允许的内存位置集合相同。
- 有关确切规则,请参阅非安全代码指南。
- 如果设置了
readonly选项,则只允许内存读取。 - 如果设置了
nomem选项,则不允许对内存进行任何读取或写入。 - 这些规则不适用于汇编代码私有的内存,例如在其中分配的栈空间。
- 编译器不能假定汇编代码中的指令是最终实际执行的指令。
- 这实际上意味着编译器必须将汇编代码视为一个黑盒,只考虑接口规范,而不是指令本身。
- 通过目标特定的机制,允许运行时代码修补。
- 然而,不能保证源代码中的每个汇编代码块直接对应于目标文件中指令的单个实例;编译器可以自由地复制或去重
asm!块中的汇编代码。
- 除非设置了
nostack选项,否则汇编代码允许使用栈指针以下的栈空间。- 进入汇编代码时,保证栈指针已根据目标 ABI 为函数调用进行了适当对齐。
- 你负责确保栈不会溢出(例如,使用栈探测来确保触及保护页)。
- 在分配栈内存时,应根据目标 ABI 的要求调整栈指针。
- 在离开汇编代码之前,必须将栈指针恢复到其原始值。
- 如果设置了
noreturn选项,则如果执行向下通过汇编代码的末尾,行为是未定义的。
- 如果设置了
pure选项,则如果asm!具有除其直接输出以外的副作用,行为是未定义的。如果两次执行具有相同输入的asm!代码导致不同的输出,行为也是未定义的。- 与
nomem选项一起使用时,“输入”仅指asm!的直接输入。 - 与
readonly选项一起使用时,“输入”包括汇编代码的直接输入以及允许读取的任何内存。
- 与
- 如果设置了
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 上,进入汇编代码时方向标志 (DF 在
EFLAGS中) 是清零的,退出时也必须清零。- 如果在退出汇编代码时方向标志被设置,行为是未定义的。
- 在 x86 上,x87 浮点寄存器栈必须保持不变,除非所有
st([0-7])寄存器都被标记为 clobber,例如使用out("st(0)") _, out("st(1)") _, ...。- 如果所有 x87 寄存器都被 clobber,则进入汇编代码时保证 x87 寄存器栈是空的。汇编代码必须确保退出汇编代码时 x87 寄存器栈也是空的。
#[cfg(target_arch = "x86_64")] pub fn fadd(x: f64, y: f64) -> f64 { let mut out = 0f64; let mut top = 0u16; // we can do complex stuff with x87 if we clobber the entire x87 stack unsafe { core::arch::asm!( "fld qword ptr [{x}]", "fld qword ptr [{y}])", "faddp", "fstp qword ptr [{out}]", "xor eax, eax", "fstsw ax", "shl eax, 11", x = in(reg) &x, y = in(reg) &y, out = in(reg) &mut out, out("st(0)") _, out("st(1)") _, out("st(2)") _, out("st(3)") _, out("st(4)") _, out("st(5)") _, out("st(6)") _, out("st(7)") _, out("eax") top );} assert_eq!(top & 0x7, 0); out } pub fn main() { #[cfg(target_arch = "x86_64")]{ assert_eq!(fadd(1.0, 1.0), 2.0); } }
- 在 arm64ec 上,调用函数时强制要求使用带有适当 thunk 的调用检查器。
- 恢复栈指针和非输出寄存器到其原始值的要求仅在退出汇编代码时适用。
- 这意味着即使未标记为
noreturn,不会向下执行且不跳转到任何label块的汇编代码也不需要保留这些寄存器。 - 当返回到与你进入时不同的
asm!块的汇编代码(例如用于上下文切换)时,这些寄存器必须包含你正在退出的asm!块进入时它们的值。- 你不能退出一个尚未进入的
asm!块的汇编代码。你也不能退出一个已经退出过其汇编代码的asm!块的汇编代码(除非先再次进入它)。 - 你负责切换任何目标特定的状态(例如线程本地存储、栈边界)。
- 你不能从一个
asm!块中的地址跳转到另一个asm!块中的地址,即使在同一个函数或块内也不行,除非你将它们的上下文视为可能不同并需要进行上下文切换。你不能假定这些上下文中的任何特定值(例如当前栈指针或栈指针以下的临时值)在两个asm!块之间保持不变。 - 你可以访问的内存位置集合是你进入和退出的
asm!块所允许的内存位置的交集。
- 你不能退出一个尚未进入的
- 这意味着即使未标记为
- 你不能假定源代码中相邻的两个
asm!块,即使它们之间没有任何其他代码,最终在二进制文件中也会出现在连续的地址,且中间没有其他指令。
- 你不能假定一个
asm!块在输出二进制文件中恰好出现一次。编译器被允许实例化asm!块的多个副本,例如当包含它的函数被内联到多个位置时。
- 在 x86 上,内联汇编不得以适用于编译器生成的指令的指令前缀(例如
LOCK)结束。- 由于内联汇编的编译方式,编译器目前无法检测到这一点,但将来可能会捕获并拒绝此类情况。
注意
通常来说,
preserves_flags涵盖的标志是在执行函数调用时不保留的标志。
正确性和有效性
除了之前的所有规则之外,asm! 的字符串参数在所有其他参数求值、格式化执行以及操作数转换后,最终必须成为对于目标架构而言在语法和语义上都有效的汇编代码。格式化规则允许编译器生成语法正确的汇编代码。关于操作数的规则允许将 Rust 操作数有效地转换为汇编代码并从中转换出来。遵守这些规则对于最终展开的汇编代码既正确又有效是必要的,但不是充分的。例如
- 参数可能被放置在格式化后语法不正确的位置
- 指令可能书写正确,但给出了架构上无效的操作数
- 架构上未指定的指令可能会被汇编成未指定的代码
- 一组指令,每条都正确且有效,如果紧密相连放置,可能会导致未定义行为
因此,这些规则是不完全的。编译器不要求检查初始字符串或最终生成的汇编代码的正确性和有效性。汇编器可能会检查正确性和有效性,但不是必需的。使用 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
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { let bytes: *const u8; let len: usize; unsafe { core::arch::asm!( "jmp 3f", "2: .ascii \"Hello World!\"", "3: lea {bytes}, [2b+rip]", "mov {len}, 12", bytes = out(reg) bytes, len = out(reg) len ); } let s = unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(bytes, len)) }; assert_eq!(s, "Hello World!"); } }
目标特定指令支持
Dwarf unwinding
支持 DWARF unwinding 信息的 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 位)
在 32 位和 64 位 x86 目标上,保证支持以下附加指令
.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