Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

内联汇编

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

内联汇编支持在以下架构上是稳定的:

  • x86 和 x86-64
  • ARM
  • AArch64 和 Arm64EC
  • RISC-V
  • LoongArch
  • s390x

如果在不支持的 target 上使用汇编宏,编译器将报错。

示例

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

// 使用移位和加法将 x 乘以 6
let mut x: u64 = 4;
unsafe {
    asm!(
        "mov {tmp}, {x}",
        "shl {tmp}, 1",
        "shl {x}, 2",
        "add {x}, {tmp}",
        x = inout(reg) x,
        tmp = out(reg) _,
    );
}
assert_eq!(x, 4 * 6);
}
}

语法格式

下面的语法指定了可以传递给 asm!global_asm!naked_asm! 宏的参数。

Syntax
AsmArgsAsmAttrFormatString ( , AsmAttrFormatString )* ( , AsmAttrOperand )* ,?

FormatStringSTRING_LITERAL | RAW_STRING_LITERAL | MacroInvocation

AsmAttrFormatString → ( OuterAttribute )* FormatString

AsmOperand
      ClobberAbi
    | AsmOptions
    | RegOperand

AsmAttrOperand → ( OuterAttribute )* AsmOperand

ClobberAbiclobber_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? }
    )

ParamNameIDENTIFIER_OR_KEYWORD | RAW_IDENTIFIER

DualDirSpecExpression
      Expression
    | Expression => Expression

RegSpecRegisterClass | ExplicitRegister

RegisterClassIDENTIFIER_OR_KEYWORD

ExplicitRegisterSTRING_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! 宏仅允许在 裸函数 中使用。

#![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);

模板字符串参数

汇编器模板使用与 格式化字符串 相同的语法格式(即占位符通过大括号指定)。

对应的参数按顺序、按索引或按名称访问。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
let y: i64;
let z: i64;
// 这个
unsafe { core::arch::asm!("mov {}, {}", out(reg) x, in(reg) 5); }
// ... 这个
unsafe { core::arch::asm!("mov {0}, {1}", out(reg) y, in(reg) 5); }
// ... 以及这个
unsafe { core::arch::asm!("mov {out}, {in}", out = out(reg) z, in = in(reg) 5); }
// 都有相同的行为
assert_eq!(x, y);
assert_eq!(y, z);
}
}

但是,不支持(由 RFC #2795 引入的)隐式命名参数。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x = 5;
// 我们不能直接从作用域引用 `x`,我们需要一个类似 `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;
// 我们可以将多个字符串分开,就像它们是写在一起的一样
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")] {
// 模板字符串需要首先出现在 asm 调用中
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")] {
// 命名操作数需要放在位置操作数之后
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")] {
// 我们也不能将显式寄存器放在位置操作数之前
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")] {
// 显式寄存器操作数不会被替换,请在字符串中显式使用 `eax`
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")] {
// 我们必须在格式化字符串中命名所有的操作数
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");
}

除了将操作数替换到模板字符串中以形成传递给汇编器的代码的方式外,确切的汇编代码语法是特定于 target 的,且对编译器是不透明的。

目前,所有受支持的 target 都遵循 LLVM 内部汇编器使用的汇编代码语法,这通常对应于 GNU 汇编器(GAS)的语法。在 x86 上,默认使用 GAS 的 .intel_syntax noprefix 模式。在 ARM 上,使用 .syntax unified 模式。这些 target 对汇编代码施加了额外的限制:任何汇编器状态(例如可以通过 .section 更改的当前节)必须在汇编字符串结束时恢复为其原始值。不符合 GAS 语法的汇编代码将导致特定于汇编器的行为。有关内联汇编所用指令的进一步约束由 指令支持 指出。

属性

在语义上,只有 cfgcfg_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",
);

注意

rustc 中,汇编宏对这些属性的处理与处理语言中类似属性的普通系统是分开实现的。这解释了受支持属性种类有限的原因,并可能导致行为上的细微差异。

在语法上,第一个操作数之前必须至少有一个模板字符串。

#![allow(unused)]
fn main() {
// 这被拒绝是因为 `a = out(reg) x` 不会被解析为
// 模板字符串。
core::arch::asm!(
    #[cfg(false)]
    a = out(reg) x, // ERROR.
    "",
);
}

操作数类型

支持几种类型的操作数:

  • in(<reg>) <expr>
    • <reg> 可以引用一个寄存器类或一个显式寄存器。分配的寄存器名称将被替换到汇编模板字符串中。
    • 在汇编代码开始时,分配的寄存器将包含 <expr> 的值。
    • 分配的寄存器在汇编代码结束时必须包含相同的值(除非 lateout 被分配到同一个寄存器)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `in` 可用于将值传递到内联汇编中...
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;
// 而 `out` 可用于将值传回 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` 与 `out` 相同
// 但编译器知道在我们要覆盖它的时候,我们并不关心任何输入的值。
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` 可用于在寄存器内修改值
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> 指定下划线(_)而不是表达式,这将导致寄存器的内容在汇编代码结束时被丢弃(实际上起到了破坏列表的作用)。
    • <in expr><out expr> 可以具有不同的类型。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i64;
// `inout` 也可以将值移动到不同的地方
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` 是使用 `lateout` 的 `inout`
unsafe { core::arch::asm!("inc {}", inlateout(reg) x); }
assert_eq!(x, 5);
}
}
  • sym <path>
    • <path> 必须引用一个 fnstatic
    • 引用该项的混淆后的符号名称将被替换到汇编模板字符串中。
    • 替换后的字符串不包括任何修饰符(例如 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` 可用于引用函数(即使它没有我们可以直接编写的外部名称)
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;
// 将常量值传递给需要立即数的指令,如 `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>
    • 该块的地址将被替换到汇编模板字符串中。汇编代码可以跳转到替换后的地址。
    • 对于区分直接跳转和间接跳转的 target(例如启用了 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 从第二个输出而不是第一个输出获取其值
unsafe { core::arch::asm!("mov {}, 0", "mov {}, 1", out(reg) y, out(reg) y); }
assert_eq!(y, 1);
}
}

由于 naked_asm! 定义了整个函数体,且编译器无法发出任何额外的代码来处理操作数,因此它只能使用 symconst 操作数。

由于 global_asm! 存在于函数之外,它只能使用 symconst 操作数。

fn main() {}
// 不允许寄存器操作数,因为我们不在函数中
#[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` 和 `sym` 都是允许的
core::arch::global_asm!("/* {} {} */", const 0, sym foo);

寄存器操作数

输入和输出操作数既可以指定为显式寄存器,也可以指定为寄存器分配器可以从中选择寄存器的寄存器类。显式寄存器被指定为字符串字面量(例如 "eax"),而寄存器类被指定为标识符(例如 reg)。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut y: i64;
// 我们既可以将两者都命名为 `reg`,也可以使用显式寄存器(如 `eax`)来获取一个
// 整数寄存器
unsafe { core::arch::asm!("mov eax, {:e}", in(reg) 5, lateout("eax") y); }
assert_eq!(y, 5);
}
}

请注意,显式寄存器将寄存器别名(例如 ARM 上的 r14lr)和寄存器的较小视图(例如 eaxrax)视为等同于基本寄存器。

在两个输入操作数或两个输出操作数中使用相同的显式寄存器是一个编译时错误。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 我们不能两次命名 eax
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")] {
// ... 即使使用不同的别名
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 与 ax 重叠,所以我们不能同时命名它们。
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() {}

// 允许整数...
let y: i64 = 5;
unsafe { core::arch::asm!("/* {} */", in(reg) y); }

// 以及指针...
let py = &raw const y;
unsafe { core::arch::asm!("/* {} */", in(reg) py); }

// 还有浮点数...
let f = 1.0f32;
unsafe { core::arch::asm!("/* {} */", in(xmm_reg) f); }

// 甚至是函数指针和 simd 向量。
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;
// 不允许像结构体这样的复杂类型
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 约束代码
x86regax, bx, cx, dx, si, di, bp, r[8-15] (仅限 x86-64)r
x86reg_abcdax, bx, cx, dxQ
x86-32reg_byteal, bl, cl, dl, ah, bh, ch, dhq
x86-64reg_byte*al, bl, cl, dl, sil, dil, bpl, r[8-15]bq
x86xmm_regxmm[0-7] (x86) xmm[0-15] (x86-64)x
x86ymm_regymm[0-7] (x86) ymm[0-15] (x86-64)x
x86zmm_regzmm[0-7] (x86) zmm[0-31] (x86-64)v
x86kregk[1-7]Yk
x86kreg0k0仅限破坏列表
x86x87_regst([0-7])仅限破坏列表
x86mmx_regmm[0-7]仅限破坏列表
x86-64tmm_regtmm[0-7]仅限破坏列表
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffr仅限破坏列表
Arm64ECregx[0-12], x[15-22], x[25-27], x30r
Arm64ECvregv[0-15]w
Arm64ECvreg_low16v[0-15]x
ARM (ARM/Thumb2)regr[0-12], r14r
ARM (Thumb1)regr[0-7]r
ARMsregs[0-31]t
ARMsreg_low16s[0-15]x
ARMdregd[0-31]w
ARMdreg_low16d[0-15]t
ARMdreg_low8d[0-8]x
ARMqregq[0-15]w
ARMqreg_low8q[0-7]t
ARMqreg_low4q[0-3]x
RISC-Vregx1, x[5-7], x[9-15], x[16-31] (非 RV32E)r
RISC-Vfregf[0-31]f
RISC-Vvregv[0-31]仅限破坏列表
LoongArchreg$r1, $r[4-20], $r[23,30]r
LoongArchfreg$f[0-31]f
s390xregr[0-10], r[12-14]r
s390xreg_addrr[1-10], r[12-14]a
s390xfregf[0-15]f
s390xvregv[0-31]仅限破坏列表
s390xarega[2-15]仅限破坏列表

注意

  • 在 x86 上,我们将 reg_bytereg 区别对待,因为编译器可以分别分配 alah,而 reg 会保留整个寄存器。
  • 在 x86-64 上,高字节寄存器(例如 ah)在 reg_byte 寄存器类中不可用。
  • 一些寄存器类被标记为 “仅限破坏列表” ,这意味着这些类中的寄存器不能用于输入或输出,只能用于 out(<explicit register>) _lateout(<explicit register>) _ 形式的破坏列表。

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

架构寄存器类target 特性允许的类型
x86-32regNonei16, i32, f32
x86-64regNonei16, i32, f32, i64, f64
x86reg_byteNonei8
x86xmm_regssei32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
x86ymm_regavxi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
x86zmm_regavx512fi32, f32, i64, f64,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
i8x32, i16x16, i32x8, i64x4, f32x8, f64x4
i8x64, i16x32, i32x16, i64x8, f32x16, f64x8
x86kregavx512fi8, i16
x86kregavx512bwi32, i64
x86mmx_regN/A仅限破坏列表
x86x87_regN/A仅限破坏列表
x86tmm_regN/A仅限破坏列表
AArch64regNonei8, i16, i32, f32, i64, f64
AArch64vregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
AArch64pregN/A仅限破坏列表
Arm64ECregNonei8, i16, i32, f32, i64, f64
Arm64ECvregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
ARMregNonei8, i16, i32, f32
ARMsregvfp2i32, f32
ARMdregvfp2i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2
ARMqregneoni8x16, i16x8, i32x4, i64x2, f32x4
RISC-V32regNonei8, i16, i32, f32
RISC-V64regNonei8, i16, i32, f32, i64, f64
RISC-Vfregff32
RISC-Vfregdf64
RISC-VvregN/A仅限破坏列表
LoongArch32regNonei8, i16, i32, f32
LoongArch64regNonei8, i16, i32, i64, f32, f64
LoongArchfregff32
LoongArchfregdf64
s390xreg, reg_addrNonei8, i16, i32, i64
s390xfregNonef32, f64
s390xvregN/A仅限破坏列表
s390xaregN/A仅限破坏列表

注意

就上表而言,指针、函数指针和 isize/usize 被视为等效的整数类型(取决于 target,为 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 对 `i32` 有效,reg_byte 对 `i8` 有效,而 xmm_reg 对 `__m128i` 有效
// 我们不能使用 `tmm0` 作为输入或输出,但我们可以将其加入破坏列表。
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) };
// 我们不能将 `__m128i` 传递给 `reg` 输入
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 架构的要求被 NaN 装箱(NaN-boxed)在 f64 中。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x: i64;
// 将 32 位值移动到 64 位值中,哎呀。
#[allow(asm_sub_register)] // rustc 对此行为发出警告
unsafe { core::arch::asm!("mov {}, {}", lateout(reg) x, in(reg) 4i32); }
// 高 32 位是不确定的
assert_eq!(x, 4); // 不保证此断言成功
assert_eq!(x & 0xFFFFFFFF, 4); // 但是,这个断言会成功
}
}

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

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// 指针和整数可以混用(只要它们大小相同)
let x: isize = 0;
let y: *mut ();
// 使用内联汇编魔术将 `isize` 转换为 `*mut ()`
unsafe { core::arch::asm!("/*{}*/", inout(reg) x=>y); }
assert!(y.is_null()); // 一种极其曲折的创建空指针的方法
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let y: f32;
// 但我们不能像这样将 `i32` 重新解释为 `f32`
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");
}

寄存器名称

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

架构基本寄存器别名
x86axeax, rax
x86bxebx, rbx
x86cxecx, rcx
x86dxedx, rdx
x86siesi, rsi
x86diedi, rdi
x86bpbpl, ebp, rbp
x86spspl, esp, rsp
x86ipeip, rip
x86st(0)st
x86r[8-15]r[8-15]b, r[8-15]w, r[8-15]d
x86xmm[0-31]ymm[0-31], zmm[0-31]
AArch64x[0-30]w[0-30]
AArch64x29fp
AArch64x30lr
AArch64spwsp
AArch64xzrwzr
AArch64v[0-31]b[0-31], h[0-31], s[0-31], d[0-31], q[0-31]
Arm64ECx[0-30]w[0-30]
Arm64ECx29fp
Arm64ECx30lr
Arm64ECspwsp
Arm64ECxzrwzr
Arm64ECv[0-15]b[0-15], h[0-15], s[0-15], d[0-15], q[0-15]
ARMr[0-3]a[1-4]
ARMr[4-9]v[1-6]
ARMr9rfp
ARMr10sl
ARMr11fp
ARMr12ip
ARMr13sp
ARMr14lr
ARMr15pc
RISC-Vx0zero
RISC-Vx1ra
RISC-Vx2sp
RISC-Vx3gp
RISC-Vx4tp
RISC-Vx[5-7]t[0-2]
RISC-Vx8fp, s0
RISC-Vx9s1
RISC-Vx[10-17]a[0-7]
RISC-Vx[18-27]s[2-11]
RISC-Vx[28-31]t[3-6]
RISC-Vf[0-7]ft[0-7]
RISC-Vf[8-9]fs[0-1]
RISC-Vf[10-17]fa[0-7]
RISC-Vf[18-27]fs[2-11]
RISC-Vf[28-31]ft[8-11]
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 是 eax 和 ax 的别名
unsafe { core::arch::asm!("", in("rax") z); }
}
}

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

架构不受支持的寄存器原因
Allsp, r15 (s390x)栈指针必须在汇编代码结束时或跳转到 label 块之前恢复其原始值。
Allbp (x86), x29 (AArch64 和 Arm64EC), x8 (RISC-V), $fp (LoongArch), r11 (s390x)帧指针不能用作输入或输出。
ARMr7r11在 ARM 上,帧指针可以是 r7r11,具体取决于 target。帧指针不能用作输入或输出。
Allsi (x86-32), bx (x86-64), r6 (ARM), x19 (AArch64 和 Arm64EC), x9 (RISC-V), $s8 (LoongArch)LLVM 内部将其用作具有复杂栈帧的函数的 “base pointer” 。
x86ip这是程序计数器,不是真实的寄存器。
AArch64xzr这是一个常量零寄存器,无法修改。
AArch64x18在某些 AArch64 target 上,这是操作系统保留的寄存器。
Arm64ECxzr这是一个常量零寄存器,无法修改。
Arm64ECx18这是一个操作系统保留的寄存器。
Arm64ECx13, x14, x23, x24, x28, v[16-31], p[0-15], ffr这些是 Arm64EC 不支持的 AArch64 寄存器。
ARMpc这是程序计数器,不是真实的寄存器。
ARMr9在某些 ARM target 上,这是操作系统保留的寄存器。
RISC-Vx0这是一个常量零寄存器,无法修改。
RISC-Vgp, tp这些寄存器是保留的,不能用作输入或输出。
LoongArch$r0$zero这是一个常量零寄存器,无法修改。
LoongArch$r2$tp这是为 TLS 保留的。
LoongArch$r21这是由 ABI 保留的。
s390xc[0-15]由内核保留。
s390xa[0-1]保留供系统使用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// bp 是保留的
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")] {
// 我们不能同时指定 `r` 和 `e`。
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-32regNoneeaxk
x86-64regNoneraxq
x86-32reg_abcdlalb
x86-64reglalb
x86reg_abcdhahh
x86regxaxw
x86regeeaxk
x86-64regrraxq
x86reg_byteNoneal / ahNone
x86xmm_regNonexmm0x
x86ymm_regNoneymm0t
x86zmm_regNonezmm0g
x86*mm_regxxmm0x
x86*mm_regyymm0t
x86*mm_regzzmm0g
x86kregNonek1None
AArch64/Arm64ECregNonex0x
AArch64/Arm64ECregww0w
AArch64/Arm64ECregxx0x
AArch64/Arm64ECvregNonev0None
AArch64/Arm64ECvregvv0None
AArch64/Arm64ECvregbb0b
AArch64/Arm64ECvreghh0h
AArch64/Arm64ECvregss0s
AArch64/Arm64ECvregdd0d
AArch64/Arm64ECvregqq0q
ARMregNoner0None
ARMsregNones0None
ARMdregNoned0P
ARMqregNoneq0q
ARMqrege / fd0 / d1e / f
RISC-VregNonex1None
RISC-VfregNonef0None
LoongArchregNone$r1None
LoongArchfregNone$f0None
s390xregNone%r0None
s390xreg_addrNone%r1None
s390xfregNone%f0None

注意

  • 在 ARM 上 e / f:这会打印 NEON quad (128-bit) 寄存器的低双字或高双字寄存器名称。
  • 在 x86 上:我们对不带修饰符的 reg 的行为与 GCC 所做的不同。GCC 将根据操作数值类型推断修饰符,而我们默认使用完整的寄存器大小。
  • 在 x86 xmm_reg 上:xtg LLVM 修饰符尚未在 LLVM 中实现(它们仅受 GCC 支持),但这应该是一个简单的改动。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0x10u16;

// 使用 `xchg` 的 u16::swap_bytes
// `{x}` 的低半部分由 `{x:l}` 引用,高半部分由 `{x:h}` 引用
unsafe { core::arch::asm!("xchg {x:l}, {x:h}", x = inout(reg_abcd) x); }
assert_eq!(x, 0x1000u16);
}
}

如上一节所述,传递小于寄存器宽度的输入值将导致寄存器的高位包含未定义的值。如果内联汇编仅访问寄存器的低位,这就不是问题,可以通过使用模板修饰符在汇编代码中使用子寄存器名称(例如使用 ax 而不是 rax)来实现。由于这是一个容易掉进去的陷阱,编译器将根据输入类型建议在适当的地方使用模板修饰符。如果对操作数的所有引用都已经有了修饰符,则会抑制该操作数的警告。

ABI破坏列表

clobber_abi 关键字可用于为汇编代码应用一组默认的破坏列表。这将根据调用具有特定调用约定(calling convention)的函数自动插入必要的破坏约束:如果调用约定不能在调用过程中完全保留寄存器的值,则会隐式地将 lateout("...") _ 添加到操作数列表中(其中 ... 被替换为寄存器的名称)。

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
extern "C" fn foo() -> i32 { 0 }

let z: i32;
// 要调用函数,我们必须告知编译器我们正在破坏
// 被调用者保存的寄存器
unsafe { core::arch::asm!("call {}", sym foo, out("rax") z, clobber_abi("C")); }
assert_eq!(z, 0);
}
}

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

#![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;
// 我们甚至可以调用多个具有不同约定和
// 不同保存寄存器的函数
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;
// 必须使用显式寄存器以免意外重叠。
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 插入的隐式破坏列表:仅当寄存器未用作输出时,才会为该寄存器插入破坏列表。

以下 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
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 在 target 上不被视为保留寄存器时,才将其包含在破坏列表中。
  • 在 RISC-V 上,仅当 x[16-17]x[28-31] 在 target 上不被视为保留寄存器时,才将其包含在破坏列表中。

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

选项

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

  • pure:汇编代码没有副作用,必须最终返回,并且其输出仅取决于其直接输入(即值本身,而不是它们指向的内容)或从内存读取的值(除非还设置了 nomem 选项)。这允许编译器执行汇编代码的次数少于程序中指定的次数(例如通过将其提升到循环之外),或者如果输出未被使用,甚至可以完全消除它。pure 选项必须与 nomemreadonly 选项之一结合使用,否则会发出编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let x: i32 = 0;
let z: i32;
// `pure` 可用于通过假设汇编没有副作用来进行优化
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;
// 必须满足 `nomem` 或 `readonly` 之一,以指示是否
// 允许读取内存
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:汇编代码不会读取或写入汇编代码之外可访问的任何内存。这允许编译器在执行汇编代码期间将修改后的全局变量的值缓存在寄存器中,因为它知道汇编代码不会读取或写入它们。编译器还假设汇编代码不会与其他线程执行任何形式的同步,例如通过内存屏障(fence)。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let mut x = 0i32;
let z: i32;
// 当指定 `nomem` 时,不允许从汇编访问外部内存
unsafe {
    core::arch::asm!("mov {val:e}, dword ptr [{ptr}]",
        ptr = in(reg) &mut x,
        val = lateout(reg) z,
        options(nomem)
    )
}

// 当指定 `nomem` 时,从汇编写入外部内存也是未定义行为
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;
// 然而,如果我们分配自己的内存,例如通过 `push`。
// 我们仍然可以使用它
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;
// 当指定 `readonly` 时,我们不能修改外部内存
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;
// 不过我们仍然可以从中读取
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;
// 与 `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:汇编代码不会顺序执行(fall through);如果它这样做,则行为是未定义的。它仍然可以跳转到 label 块。如果任何 label 块返回单元类型,则 asm! 块将返回单元类型。否则它将返回 !(从不)。与调用不返回的函数一样,作用域内的局部变量在执行汇编代码之前不会被丢弃。
fn main() -> ! {
#[cfg(target_arch = "x86_64")] {
    // 我们可以在 `noreturn` 块内使用一条指令来捕获执行
    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")] {
// 您负责确保执行不会越过 `noreturn` 汇编块的末尾
unsafe { core::arch::asm!("", options(noreturn)); }
}
}
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")]
let _: () = unsafe {
    // 您仍然可以跳转到 `label` 块
    core::arch::asm!("jmp {}", label {
        println!();
    }, options(noreturn));
};
}
  • nostack:汇编代码不会将数据压入栈,也不会写入栈红区(red-zone)(如果 target 支持)。如果 使用此选项,则编译器保证在汇编代码开始时,栈指针已(根据 target ABI)适当对齐,以便进行函数调用。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `push` 和 `pop` 在与 `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;
// 我们在这里需要使用 AT&T 语法。操作数的顺序是源、目的
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! 从外部文件包含原始汇编代码时特别有用。

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

  • nomemreadonly 选项是互斥的:同时指定两者是一个编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
// `nomem` 严格强于 `readonly` ,它们不能同时指定
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` 块需要至少一个输出
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");
}
  • 在具有输出且没有 label 的汇编块上指定 noreturn 是一个编译时错误。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
let z: i32;
// `noreturn` 不能有输出
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");
}
  • 在具有输出的汇编块中拥有任何 label 块是一个编译时错误。

naked_asm! 仅支持 att_syntaxraw 选项。其余选项没有意义,因为内联汇编定义了整个函数体。

global_asm! 仅支持 att_syntaxraw 选项。其余选项对于全局作用域内联汇编没有意义。

fn main() {}
#[cfg(target_arch = "x86_64")]
// `nomem` 在 `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 不同,后者每次读取时都可以具有不同的值(因为汇编代码中不存在这样的概念)。
  • 任何未指定为输出的寄存器在退出汇编代码时必须具有与进入时相同的值,否则行为是未定义的。
    • 这仅适用于可以指定为输入或输出的寄存器。其他寄存器遵循特定于 target 的规则。
    • 请注意,lateout 可能会被分配到与 in 相同的寄存器,在这种情况下,此规则不适用。然而,代码不应依赖于此,因为它取决于寄存器分配的结果。
  • 如果执行从汇编代码中展开(unwind),则行为是未定义的。
    • 这也适用于汇编代码调用了一个随后发生展开的函数的情况。
  • 汇编代码允许读取和写入的内存位置集与 FFI 函数所允许的相同。
    • 如果设置了 readonly 选项,则仅允许读取内存。
    • 如果设置了 nomem 选项,则不允许读取或写入内存。
    • 这些规则不适用于汇编代码私有的内存,例如在其内部分配的栈空间。
  • 编译器不能假设汇编代码中的指令就是最终会执行的指令。
    • 这实际上意味着编译器必须将汇编代码视为黑盒,仅考虑接口规范,而不是指令本身。
    • 允许通过特定于 target 的机制进行运行时代码补丁。
    • 然而,不保证源代码中的每个汇编代码块都直接对应于目标文件中的单个指令实例;编译器可以自由地复制或去重 asm! 块中的汇编代码。
  • 除非设置了 nostack 选项,否则允许汇编代码使用栈指针下方的栈空间。
    • 在进入汇编代码时,栈指针保证已(根据 target ABI)适当对齐,以便进行函数调用。
    • 您负责确保不会发生栈溢出(例如使用栈探测以确保触及保护页)。
    • 您应当根据 target ABI 的要求,在分配栈内存时调整栈指针。
    • 栈指针必须在离开汇编代码之前恢复为其原始值。
  • 如果设置了 noreturn 选项,则如果执行顺序执行到汇编代码末尾,则行为是未定义的。
  • 如果设置了 pure 选项,则如果 asm! 除了其直接输出之外还有副作用,则行为是未定义的。如果具有相同输入的 asm! 代码的两次执行导致不同的输出,则行为也是未定义的。
    • 当与 nomem 选项一起使用时, “输入” 只是 asm! 的直接输入。
    • 当与 readonly 选项一起使用时, “输入” 包括汇编代码的直接输入和它允许读取的任何内存。
  • 如果设置了 preserves_flags 选项,则退出汇编代码时必须恢复这些标志寄存器:
    • x86
      • EFLAGS 中的状态标志位(CF, PF, AF, ZF, SF, OF)。
      • 浮点状态字(全部)。
      • MXCSR 中的浮点异常标志(PE, UE, OE, ZF, 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)。
      • 向量扩展状态(vtypevlvxsatvxrm)。
    • LoongArch
      • $fcc[0-7] 中的浮点条件标志。
    • s390x
      • 条件代码寄存器 cc
  • 在 x86 上,方向标志位(EFLAGS 中的 DF)在进入汇编代码时为清除状态,且退出时必须为清除状态。
    • 如果退出汇编代码时设置了方向标志位,则行为是未定义的。
  • 在 x86 上,x87 浮点寄存器栈必须保持不变,除非所有 st([0-7]) 寄存器都已标记为使用 out("st(0)") _, out("st(1)") _, ... 进行了破坏。
    • 如果所有 x87 寄存器都被破坏,则保证在进入汇编代码时 x87 寄存器栈为空。汇编代码必须确保在退出汇编代码时 x87 寄存器栈也为空。
#[cfg(target_arch = "x86_64")]
pub fn fadd(x: f64, y: f64) -> f64 {
  let mut out = 0f64;
  let mut top = 0u16;
  // 如果我们破坏了整个 x87 栈,我们可以用 x87 做复杂的事情
  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);
}
}
  • 恢复栈指针和非输出寄存器为其原始值的要求仅适用于退出汇编代码时。
    • 这意味着不顺序执行且不跳转到任何 label 块的汇编代码,即使没有标记为 noreturn,也不需要保留这些寄存器。
    • 当返回到与进入时不同的 asm! 块的汇编代码时(例如用于上下文切换),这些寄存器必须包含在进入您正在 退出asm! 块时所具有的值。
      • 您不能退出未进入的 asm! 块的汇编代码。也不能退出其汇编代码已退出的 asm! 块的汇编代码(除非再次进入它)。
      • 您负责切换任何特定于 target 的状态(例如线程局部存储、栈边界)。
      • 您不能从一个 asm! 块中的地址跳转到另一个块中的地址,即使在同一个函数或块内,而不将它们的上下文视为可能不同的并需要进行上下文切换。您不能假设这些上下文中的任何特定值(例如当前栈指针或栈指针下方的临时值)在两个 asm! 块之间将保持不变。
      • 您可以访问的内存位置集是您进入和退出的 asm! 块所允许的位置集的交集。
  • 您不能假设源代码中相邻的两个 asm! 块,即使它们之间没有任何其他代码,最终也会位于二进制文件中连续的地址且它们之间没有任何其他指令。
  • 您不能假设 asm! 块在输出二进制文件中恰好出现一次。编译器允许实例化 asm! 块的多个副本,例如当包含它的函数在多个地方内联时。
  • 在 x86 上,内联汇编不得以会应用于编译器生成的指令的指令前缀(例如 LOCK)结尾。
    • 由于内联汇编的编译方式,编译器目前无法检测到这一点,但将来可能会捕获并拒绝这种情况。

注意

作为一般规则,preserves_flags 涵盖的标志是那些在执行函数调用时 被保留的标志。

裸内联汇编规则

为避免未定义行为,在裸函数(naked_asm!)中使用函数作用域内联汇编时必须遵守以下规则:

  • 根据调用约定和函数签名,任何未用于函数输入的寄存器在进入 naked_asm! 块时都将包含一个未定义的值。
    • 在内联汇编的语境下, “未定义的值” 意味着寄存器可以(非确定性地)具有架构所允许的任何一个可能的值。值得注意的是,它与 LLVM 的 undef 不同,后者每次读取时都可以具有不同的值(因为汇编代码中不存在这样的概念)。
  • 所有被调用者保存(callee-saved)的寄存器在返回时必须具有与进入时相同的值。
  • 可以自由使用调用者保存(caller-saved)的寄存器。
  • 如果执行顺序执行过汇编代码末尾,则行为是未定义的。
    • 汇编代码中的每条路径都应以返回指令结束或发散。
  • 汇编代码允许读取和写入的内存位置集与 FFI 函数所允许的相同。
  • 编译器不能假设 naked_asm! 块中的指令就是最终会执行的指令。
    • 这实际上意味着编译器必须将 naked_asm! 视为黑盒,仅考虑接口规范,而不是指令本身。
    • 允许通过特定于 target 的机制进行运行时代码补丁。
  • 允许从 naked_asm! 块中展开。
    • 为了保证行为正确,必须使用发出展开元数据的适当汇编指令。
#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
#[unsafe(naked)]
extern "sysv64-unwind" fn unwinding_naked() {
    core::arch::naked_asm!(
        // 这里的 "CFI" 代表 "调用栈帧信息" (call frame information)。
        ".cfi_startproc",
        // CFA (规范栈帧地址) 是 `call` 之前 `rsp` 的值,
        // 即在返回地址 `rip` 被压入 `rsp` 之前的值,
        // 所以在进入函数时(在 `rip` 被压入后),
        // 它在内存中比 `rsp` 高 8 个字节。
        //
        // 这是默认设置,所以我们不必写它。
        //".cfi_def_cfa rsp, 8",
        //
        // 传统做法是保留基指针,所以我们就这么做。
        "push rbp",
        // 既然我们现在已在内存中将栈向下扩展了 8 个字节,
        // 我们需要将从 `rsp` 到 CFA 的偏移量再调整 8 个字节。
        ".cfi_adjust_cfa_offset 8",
        // 然后我们还注释了我们将调用者的 `rbp` 值相对于 CFA 存储在哪里,
        // 以便在展开到调用者时可以找到它,以防我们需要它
        // 来计算调用者相对于它的 CFA。
        //
        // 在这里,我们从 CFA 下方 16 字节开始存储调用者的 `rbp`。
        // 即从 CFA 开始,首先是 `rip`(从 CFA 下方 8 字节开始并持续到它),
        // 然后是我们刚刚压入的调用者的 `rbp`。
        ".cfi_offset rbp, -16",
        // 按照传统,我们将基指针设置为栈指针的值。
        // 这样,基指针在整个函数体中保持不变。
        "mov rbp, rsp",
        // 我们现在可以跟踪从基指针到 CFA 的偏移量。
        // 这意味着直到最后我们都不需要再做任何调整,
        // 因为我们不改变 `rbp`。
        ".cfi_def_cfa_register rbp",
        // 我们现在可以调用一个可能产生恐慌的函数。
        "call {f}",
        // 返回后,我们恢复 `rbp` 以准备返回我们自己。
        "pop rbp",
        // 现在我们已经恢复了 `rbp`,我们必须再次根据 `rsp`
        // 指定到 CFA 的偏移量。
        ".cfi_def_cfa rsp, 8",
        // 现在我们可以返回了。
        "ret",
        ".cfi_endproc",
        f = sym may_panic,
    )
}

extern "sysv64-unwind" fn may_panic() {
    panic!("unwind");
}
}
}

注意

有关上述 cfi 汇编指令的更多信息,请参阅以下资源:

正确性和有效性

除了所有先前的规则外,asm! 的字符串参数最终必须变为——在评估了所有其他参数、执行了格式化并转换了操作数之后——在语法上正确且在语义上对于 target 架构有效。格式化规则允许编译器生成具有正确语法的汇编代码。关于操作数的规则允许将 Rust 操作数有效转换进出汇编代码。遵守这些规则是必要的,但对于最终展开的汇编代码既正确又有效是不充分的。例如:

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

因此,这些规则是 非详尽的 。编译器不被要求检查初始字符串或最终生成的汇编代码的正确性和有效性。汇编器可以检查正确性和有效性,但不是必须这样做。使用 asm! 时,一个排版错误可能就足以使程序不健全,而汇编规则可能包括数千页的架构参考手册。程序员应当行使适当的谨慎,因为调用这种 unsafe 能力意味着承担不违反编译器和架构规则的责任。

指令支持

内联汇编支持 GNU AS 和 LLVM 内部汇编器所支持的指令子集,如下所示。使用其他指令的结果是特定于汇编器的(可能会导致错误,也可能会被原样接受)。

如果内联汇编包括任何修改后续汇编处理方式的 “stateful” 指令,则汇编代码必须在内联汇编结束前撤消任何此类指令的影响。

汇编器保证支持以下指令:

  • .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!");
}
}

特定于target的指令支持

Dwarf展开

在支持 DWARF 展开信息的 ELF target 上支持以下指令:

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

在具有结构化异常处理的 target 上,保证支持以下附加指令:

  • .seh_endproc
  • .seh_endprologue
  • .seh_proc
  • .seh_pushreg
  • .seh_savereg
  • .seh_setframe
  • .seh_stackalloc
x86(32 位和 64 位)

在 x86 target(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