内联汇编
内联汇编的支持通过 asm!、naked_asm! 和 global_asm! 宏提供。它可用于将手写汇编嵌入到编译器生成的汇编输出中。
内联汇编在以下架构上是稳定的:
- x86 和 x86-64
- ARM
- AArch64 和 Arm64EC
- RISC-V
- LoongArch
- s390x
- PowerPC 和 PowerPC64
如果在不支持的目标上使用汇编宏,编译器将发出错误。
示例
#![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);
}
}
语法
以下语法指定可以传递给 asm!、global_asm! 和 naked_asm! 宏的参数。
Syntax
AsmArgs → AsmAttrFormatString ( , AsmAttrFormatString )* ( , AsmAttrOperand )* ,?
FormatString → STRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation
AsmAttrFormatString → ( OuterAttribute )* FormatString
AsmOperand →
ClobberAbi
| AsmOptions
| RegOperand
AsmAttrOperand → ( OuterAttribute )* AsmOperand
ClobberAbi → clobber_abi ( Abi ( , Abi )* ,? )
AsmOptions →
options ( ( AsmOption ( , AsmOption )* ,? )? )
AsmOption →
pure
| nomem
| readonly
| preserves_flags
| noreturn
| nostack
| att_syntax
| raw
RegOperand → ( ParamName = )?
(
DirSpec ( RegSpec ) Expression
| DualDirSpec ( RegSpec ) DualDirSpecExpression
| sym PathExpression
| const Expression
| label { Statements? }
)
ParamName → IDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER
DualDirSpecExpression →
Expression
| Expression => Expression
RegSpec → RegisterClass | ExplicitRegister
RegisterClass → IDENTIFIER_OR_KEYWORD
ExplicitRegister → STRING_LITERAL
DirSpec →
in
| out
| lateout
DualDirSpec →
inout
| inlateout
作用域
内联汇编可以通过三种方式之一使用。
使用 asm! 宏,汇编代码在函数作用域中发出,并集成到编译器生成的函数汇编代码中。此汇编代码必须遵守严格规则以避免未定义行为。请注意,在某些情况下,编译器可能选择将汇编代码作为单独的函数发出并生成对它的调用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
unsafe { core::arch::asm!("/* {} */", in(reg) 0); }
}
}
使用 naked_asm! 宏,汇编代码在函数作用域中发出,并构成函数的完整汇编代码。naked_asm! 宏仅在[裸函数]naked functions中允许。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "C" fn wrapper() {
core::arch::naked_asm!("/* {} */", const 0);
}
}
}
使用 global_asm! 宏,汇编代码在全局作用域中发出,在函数外部。这可用于使用汇编代码手写整个函数,并且通常提供更大的自由度来使用任意寄存器和汇编器指令。
fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!("/* {} */", const 0);
模板字符串参数
汇编器模板使用与[格式字符串]format strings相同的语法(即占位符由花括号指定)。
相应的参数按顺序、按索引或按名称访问。
#![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")] {
// 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")] {
// 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")] {
// 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")] {
// 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 更改的当前部分)必须在 asm 字符串末尾恢复到其原始值。不符合 GAS 语法的汇编代码将导致特定于汇编器的行为。内联汇编使用的指令的进一步约束由指令支持指示。
属性
在语义上,只有 cfg 和 cfg_attr 属性在内联汇编模板字符串和操作数上被接受。其他属性被解析但在汇编宏展开时被拒绝。
fn main() {}
#[cfg(target_arch = "x86_64")]
core::arch::global_asm!(
#[cfg(not(panic = "abort"))]
".cfi_startproc",
// ...
"ret",
#[cfg(not(panic = "abort"))]
".cfi_endproc",
);
Note
在
rustc中,汇编宏对这些属性的处理与处理语言中类似属性的正常系统分开。这解释了支持的属性种类有限,并可能导致行为上的细微差异。
在语法上,第一个操作数之前必须至少有一个模板字符串。
#![allow(unused)]
fn main() {
// This is rejected because `a = out(reg) x` does not parse as a
// template string.
core::arch::asm!(
#[cfg(false)]
a = out(reg) x, // ERROR.
"",
);
}
操作数类型
支持几种类型的操作数:
in(<reg>) <expr><reg>可以引用寄存器类或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。- 分配的寄存器在汇编代码开头将包含
<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); }
}
}
Note
如果值的类型小于寄存器,则高位的值是特定于平台的。某些目标将高位清零,而其他目标则保持不变。
out(<reg>) <expr><reg>可以引用寄存器类或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。- 分配的寄存器在汇编代码开头将包含未定义的值。
<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的寄存器。 - 您应该只在所有输入都被读取后才写入寄存器,否则可能会破坏输入。
- 与
#![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>可以引用寄存器类或显式寄存器。分配的寄存器名称被替换到 asm 模板字符串中。- 分配的寄存器在汇编代码开头将包含
<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具有相同的初始值,则可能发生这种情况)。 - 您应该只在所有输入都被读取后才写入寄存器,否则可能会破坏输入。
- 与
#![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。- 引用该项的修饰符号名称被替换到 asm 模板字符串中。
- 替换的字符串不包含任何修饰符(例如 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,就像整数字面量一样。 - 表达式的值被格式化为字符串并直接替换到 asm 模板字符串中。
#![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 模板字符串中。汇编代码可以跳转到替换的地址。
- 对于区分直接跳转和间接跳转的目标(例如启用
cf-protection的 x86-64),汇编代码不得间接跳转到替换的地址。 - 执行块后,
asm!表达式返回。 - 块的类型必须是单元或
!(永不)。 - 块启动新的安全上下文;
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);
}
}
由于 naked_asm! 定义了整个函数主体,编译器无法发出任何额外代码来处理操作数,因此它只能使用 sym 和 const 操作数。
由于 global_asm! 存在于函数外部,因此它只能使用 sym 和 const 操作数。
fn main() {}
// 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")] {
// 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] | v |
| s390x | areg | a[2-15] | 仅 clobber |
| PowerPC | reg | r0, r[3-12], r[14-28] | r |
| PowerPC | reg_nonzero | r[3-12], r[14-28] | b |
| PowerPC | spe_acc | spe_acc | 仅 clobber |
| PowerPC64 | reg | r0, r[3-12], r[14-29] | r |
| PowerPC64 | reg_nonzero | r[3-12], r[14-29] | b |
| PowerPC/PowerPC64 | freg | f[0-31] | f |
| PowerPC/PowerPC64 | vreg | v[0-31] | v |
| PowerPC/PowerPC64 | vsreg | vs[0-63] | wa |
| PowerPC/PowerPC64 | cr | cr[0-7], cr | 仅 clobber |
| PowerPC/PowerPC64 | ctr | ctr | 仅 clobber |
| PowerPC/PowerPC64 | lr | lr | 仅 clobber |
| PowerPC/PowerPC64 | xer | xer | 仅 clobber |
Note
- 在 x86 上,我们将
reg_byte与reg区别对待,因为编译器可以单独分配al和ah,而reg保留整个寄存器。- 在 x86-64 上,高字节寄存器(例如
ah)在reg_byte寄存器类中不可用。- 某些寄存器类标记为“仅 clobber“,这意味着这些类中的寄存器不能用于输入或输出,只能用于
out(<explicit register>) _或lateout(<explicit register>) _形式的 clobber。spe_acc寄存器仅在 PowerPC SPE 目标上可用。
每个寄存器类对其可以使用的值类型有限制。这是必要的,因为将值加载到寄存器的方式取决于其类型。例如,在大端系统上,将 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 |
| LoongArch32 | reg | 无 | i8, i16, i32, f32 |
| LoongArch64 | reg | 无 | i8, i16, i32, i64, f32, f64 |
| LoongArch | freg | f | f32 |
| LoongArch | freg | d | f64 |
| s390x | reg, reg_addr | 无 | i8, i16, i32, i64 |
| s390x | freg | 无 | f32, f64 |
| s390x | vreg | vector | i32, f32, i64, f64, i128, i8x16, i16x8, i32x4, i64x2, f32x4, f64x2 |
| s390x | areg | N/A | 仅 clobber |
| PowerPC | spe_acc | 无 | 仅 clobber |
| PowerPC/PowerPC64 | reg | 无 | i8, i16, i32, i64(仅 PowerPC64) |
| PowerPC/PowerPC64 | reg_nonzero | 无 | i8, i16, i32, i64(仅 PowerPC64) |
| PowerPC/PowerPC64 | freg | 无 | f32, f64 |
| PowerPC/PowerPC64 | vreg | altivec | i8x16, i16x8, i32x4, f32x4 |
| PowerPC/PowerPC64 | vreg | vsx | f32, f64, i64x2, f64x2 |
| PowerPC/PowerPC64 | vsreg | vsx | vsx 和 altivec vreg 类型的并集 |
| PowerPC/PowerPC64 | cr | 无 | 仅 clobber |
| PowerPC/PowerPC64 | ctr | 无 | 仅 clobber |
| PowerPC/PowerPC64 | lr | 无 | 仅 clobber |
| PowerPC/PowerPC64 | xer | 无 | 仅 clobber |
Note
对于上表,指针、函数指针和
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 中的寄存器分配器有时无法处理具有不同类型的绑定操作数。
#![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] |
| PowerPC/PowerPC64 | r1 | sp |
| PowerPC/PowerPC64 | r31 | fp |
| PowerPC/PowerPC64 | r[0-31] | [0-31] |
| PowerPC/PowerPC64 | f[0-31] | fr[0-31] |
#![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), r1(PowerPC 和 PowerPC64) | 栈指针必须在汇编代码末尾或跳转到 label 块之前恢复到其原始值。 |
| 所有 | bp(x86), x29(AArch64 和 Arm64EC), x8(RISC-V), $fp(LoongArch), r11(s390x), fp(PowerPC 和 PowerPC64) | 帧指针不能用作输入或输出。 |
| ARM | r7 或 r11 | 在 ARM 上,帧指针可以是 r7 或 r11,具体取决于目标。帧指针不能用作输入或输出。 |
| 所有 | si(x86-32), bx(x86-64), r6(ARM), x19(AArch64 和 Arm64EC), x9(RISC-V), $s8(LoongArch), r29 和 r30(PowerPC), r30(PowerPC64) | 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] | 系统使用保留。 |
| PowerPC/PowerPC64 | r2, r13 | 这些是系统保留的寄存器。 |
| PowerPC/PowerPC64 | vrsave | vrsave 寄存器不能用作输入或输出。 |
#![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)[asm 模板参数修饰符]asm template argument modifiers的子集,但不使用相同的字母代码。
| 架构 | 寄存器类 | 修饰符 | 示例输出 | 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 | 无 |
| s390x | vreg | 无 | %v0 | 无 |
| PowerPC/PowerPC64 | reg | 无 | 0 | 无 |
| PowerPC/PowerPC64 | reg_nonzero | 无 | 3 | 无 |
| PowerPC/PowerPC64 | freg | 无 | 0 | 无 |
| PowerPC/PowerPC64 | vreg | 无 | 0 | 无 |
| PowerPC/PowerPC64 | vsreg | 无 | 0 | 无 |
Note
- 在 ARM 上
e/f:这打印 NEON 四字(128 位)寄存器的低或高双字寄存器名称。- 在 x86 上:我们对没有修饰符的
reg的行为与 GCC 不同。GCC 将根据操作数值类型推断修饰符,而我们默认为完整的寄存器大小。- 在 x86
xmm_reg上:x、t和gLLVM 修饰符尚未在 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 clobber
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("sysv64"),
clobber_abi("win64"),
);
}
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] |
Note
- 在 AArch64 上,只有当
x18在目标上不被视为保留寄存器时,才包含在 clobber 列表中。- 在 RISC-V 上,只有当
x[16-17]和x[28-31]在目标上不被视为保留寄存器时,才包含在 clobber 列表中。
每个 ABI 的 clobber 寄存器列表在 rustc 中随着架构获得新寄存器而更新:这确保当 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:汇编代码不读取或写入汇编代码外部可访问的任何内存。这允许编译器在汇编代码执行期间缓存修改的全局变量的值,因为它知道它们不会被读取或写入。编译器还假设汇编代码不执行任何与其他线程的同步,例如通过屏障。
#![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:汇编代码不写入汇编代码外部可访问的任何内存。这允许编译器在汇编代码执行期间缓存未修改的全局变量的值,因为它知道它们不会被写入。编译器还假设此汇编代码不执行任何与其他线程的同步,例如通过屏障。
#![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块返回单元,则asm!块将返回单元。否则它将返回!(永不)。与调用不返回的函数一样,作用域内的局部变量在执行汇编代码之前不会被丢弃。
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");
}
- 在没有输出或只有丢弃输出(
_)的 asm 块上指定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");
}
- 在有输出且没有标签的 asm 块上指定
noreturn是编译时错误。
#![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");
}
- 在有输出的 asm 块中有任何
label块是编译时错误。
naked_asm! 仅支持 att_syntax 和 raw 选项。其余选项没有意义,因为内联汇编定义了整个函数主体。
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相同的寄存器,在这种情况下此规则不适用。但是,代码不应依赖于此,因为它取决于寄存器分配的结果。
- 如果执行从汇编代码展开,则行为未定义。
- 如果汇编代码调用的函数随后展开,这也适用。
- 汇编代码允许读取和写入的内存位置集与 FFI 函数允许的相同。
- 如果设置了
readonly选项,则只允许内存读取。 - 如果设置了
nomem选项,则不允许读取或写入内存。 - 这些规则不适用于汇编代码私有的内存,例如在其中分配的栈空间。
- 如果设置了
- 编译器不能假设汇编代码中的指令是实际将执行的指令。
- 这实际上意味着编译器必须将汇编代码视为黑盒,只考虑接口规范,而不是指令本身。
- 允许通过特定于目标的机制进行运行时代码修补。
- 但是,不能保证源代码中的每个汇编代码块直接对应于目标文件中的单个指令实例;编译器可以自由地复制或去重
asm!块中的汇编代码。
- 除非设置了
nostack选项,否则汇编代码允许使用栈指针下方的栈空间。- 进入汇编代码时,栈指针保证适合对齐(根据目标 ABI)以进行函数调用。
- 您有责任确保不会溢出栈(例如使用栈探测以确保触及保护页)。
- 分配栈内存时,应根据目标 ABI 的要求调整栈指针。
- 离开汇编代码之前,必须将栈指针恢复到其原始值。
- 除非设置了
nostack选项,否则当目标 ABI 要求在调用者的帧中存储某些值时(例如在 PowerPC64 上保存lr时),汇编代码可以修改调用者的栈帧。
- 如果设置了
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、vxsat和vxrm)。
- LoongArch
$fcc[0-7]中的浮点条件标志。
- PowerPC/PowerPC64
fpscr中的浮点状态和粘滞位(除 DRN、VE、OE、UE、ZE、XE、NI 或 RN 之外的任何字段)。vscr中的向量状态和粘滞位(除 NJ 之外的任何字段)。
- PowerPC SPE
spefscr的粘滞和状态位(除 FINXE、FINVE、FDBZE、FUNFE、FOVFE 或 FRMC 之外的任何字段)。
- s390x
- 条件码寄存器
cc。
- 条件码寄存器
- x86
- 在 x86 上,方向标志(
EFLAGS中的 DF)在进入汇编代码时是清除的,并且在退出时必须是清除的。- 如果在退出汇编代码时方向标志被设置,则行为未定义。
- 在 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 的调用检查器。
- 恢复栈指针和非输出寄存器到其原始值的要求仅在退出汇编代码时适用。
- 这意味着不穿透且不跳转到任何
label块的汇编代码,即使未标记为noreturn,也不需要保留这些寄存器。 - 当返回到与您进入的不同的
asm!块的汇编代码时(例如用于上下文切换),这些寄存器必须包含您退出的asm!块进入时的值。- 您不能退出尚未进入的
asm!块的汇编代码。您也不能退出已经退出的asm!块的汇编代码(除非先重新进入它)。 - 您有责任切换任何特定于目标的状态(例如线程本地存储、栈边界)。
- 您不能从一个
asm!块中的地址跳转到另一个块中的地址,即使在同一函数或块内,而无需将其上下文视为可能不同并需要上下文切换。您不能假设这些上下文中的任何特定值(例如当前栈指针或栈指针下方的临时值)在两个asm!块之间保持不变。 - 您可以访问的内存位置集是您进入和退出的
asm!块允许的交集。
- 您不能退出尚未进入的
- 这意味着不穿透且不跳转到任何
- 您不能假设源代码中相邻的两个
asm!块(即使它们之间没有其他代码)将在二进制文件中连续地址处,中间没有任何其他指令。
- 您不能假设
asm!块将恰好在输出二进制文件中出现一次。编译器允许实例化asm!块的多个副本,例如当包含它的函数在多个位置内联时。
- 在 x86 上,内联汇编不得以指令前缀(如
LOCK)结尾,该前缀将应用于编译器生成的指令。- 由于内联汇编的编译方式,编译器目前无法检测到这一点,但将来可能会捕获并拒绝此情况。
Note
作为一般规则,
preserves_flags涵盖的标志是执行函数调用时不保留的那些。
裸内联汇编规则
为避免未定义行为,在裸函数中使用函数作用域内联汇编(naked_asm!)时必须遵循以下规则:
- 根据调用约定和函数签名,未用于函数输入的任何寄存器在进入
naked_asm!块时将包含未定义的值。- 内联汇编上下文中的“未定义值“意味着寄存器可以(非确定地)具有架构允许的任何可能值。特别是,它与 LLVM 的
undef不同,后者每次读取时可能具有不同的值(因为汇编代码中不存在这样的概念)。
- 内联汇编上下文中的“未定义值“意味着寄存器可以(非确定地)具有架构允许的任何可能值。特别是,它与 LLVM 的
- 所有被调用者保存的寄存器在返回时必须与进入时具有相同的值。
- 调用者保存的寄存器可以自由使用。
- 执行穿透汇编代码末尾时行为未定义。
- 汇编代码中的每条路径都应以返回指令终止或发散。
- 汇编代码允许读取和写入的内存位置集与 FFI 函数允许的相同。
- 编译器不能假设
naked_asm!块中的指令是实际将执行的指令。- 这实际上意味着编译器必须将
naked_asm!视为黑盒,只考虑接口规范,而不是指令本身。 - 允许通过特定于目标的机制进行运行时代码修补。
- 这实际上意味着编译器必须将
- 允许从
naked_asm!块展开。- 为了正确行为,必须使用发出展开元数据的适当汇编器指令。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "sysv64-unwind" fn unwinding_naked() {
core::arch::naked_asm!(
// "CFI" here stands for "call frame information".
".cfi_startproc",
// The CFA (canonical frame address) is the value of `rsp`
// before the `call`, i.e. before the return address, `rip`,
// was pushed to `rsp`, so it's eight bytes higher in memory
// than `rsp` upon function entry (after `rip` has been
// pushed).
//
// This is the default, so we don't have to write it.
//".cfi_def_cfa rsp, 8",
//
// The traditional thing to do is to preserve the base
// pointer, so we'll do that.
"push rbp",
// Since we've now extended the stack downward by 8 bytes in
// memory, we need to adjust the offset to the CFA from `rsp`
// by another 8 bytes.
".cfi_adjust_cfa_offset 8",
// We also then annotate where we've stored the caller's value
// of `rbp`, relative to the CFA, so that when unwinding into
// the caller we can find it, in case we need it to calculate
// the caller's CFA relative to it.
//
// Here, we've stored the caller's `rbp` starting 16 bytes
// below the CFA. I.e., starting from the CFA, there's first
// the `rip` (which starts 8 bytes below the CFA and continues
// up to it), then there's the caller's `rbp` that we just
// pushed.
".cfi_offset rbp, -16",
// As is traditional, we set the base pointer to the value of
// the stack pointer. This way, the base pointer stays the
// same throughout the function body.
"mov rbp, rsp",
// We can now track the offset to the CFA from the base
// pointer. This means we don't need to make any further
// adjustments until the end, as we don't change `rbp`.
".cfi_def_cfa_register rbp",
// We can now call a function that may panic.
"call {f}",
// Upon return, we restore `rbp` in preparation for returning
// ourselves.
"pop rbp",
// Now that we've restored `rbp`, we must specify the offset
// to the CFA again in terms of `rsp`.
".cfi_def_cfa rsp, 8",
// Now we can return.
"ret",
".cfi_endproc",
f = sym may_panic,
)
}
extern "sysv64-unwind" fn may_panic() {
panic!("unwind");
}
}
}
Note
有关上面
cfi汇编器指令的更多信息,请参阅以下资源:
正确性和有效性
除了所有先前的规则外,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 展开
以下指令在支持 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