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
  • 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
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! 宏仅在[裸函数]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 语法的汇编代码将导致特定于汇编器的行为。内联汇编使用的指令的进一步约束由指令支持指示。

属性

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

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 的寄存器(如果编译器知道 ininlateout 具有相同的初始值,则可能发生这种情况)。
    • 您应该只在所有输入都被读取后才写入寄存器,否则可能会破坏输入。
#![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> 必须引用 fnstatic
    • 引用该项的修饰符号名称被替换到 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! 定义了整个函数主体,编译器无法发出任何额外代码来处理操作数,因此它只能使用 symconst 操作数。

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

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 上的 r14lr)和寄存器的较小视图(例如 eaxrax)视为等同于基本寄存器。

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

#![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 约束代码
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仅 clobber
x86x87_regst([0-7])仅 clobber
x86mmx_regmm[0-7]仅 clobber
x86-64tmm_regtmm[0-7]仅 clobber
AArch64regx[0-30]r
AArch64vregv[0-31]w
AArch64vreg_low16v[0-15]x
AArch64pregp[0-15], ffr仅 clobber
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]仅 clobber
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]v
s390xarega[2-15]仅 clobber
PowerPCregr0, r[3-12], r[14-28]r
PowerPCreg_nonzeror[3-12], r[14-28]b
PowerPCspe_accspe_acc仅 clobber
PowerPC64regr0, r[3-12], r[14-29]r
PowerPC64reg_nonzeror[3-12], r[14-29]b
PowerPC/PowerPC64fregf[0-31]f
PowerPC/PowerPC64vregv[0-31]v
PowerPC/PowerPC64vsregvs[0-63]wa
PowerPC/PowerPC64crcr[0-7], cr仅 clobber
PowerPC/PowerPC64ctrctr仅 clobber
PowerPC/PowerPC64lrlr仅 clobber
PowerPC/PowerPC64xerxer仅 clobber

Note

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

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

架构寄存器类目标功能允许的类型
x86-32regi16, i32, f32
x86-64regi16, i32, f32, i64, f64
x86reg_bytei8
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仅 clobber
x86x87_regN/A仅 clobber
x86tmm_regN/A仅 clobber
AArch64regi8, 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仅 clobber
Arm64ECregi8, i16, i32, f32, i64, f64
Arm64ECvregneoni8, i16, i32, f32, i64, f64,
i8x8, i16x4, i32x2, i64x1, f32x2, f64x1,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
ARMregi8, i16, i32, f32
ARMsregvfp2i32, f32
ARMdregvfp2i64, f64, i8x8, i16x4, i32x2, i64x1, f32x2
ARMqregneoni8x16, i16x8, i32x4, i64x2, f32x4
RISC-V32regi8, i16, i32, f32
RISC-V64regi8, i16, i32, f32, i64, f64
RISC-Vfregff32
RISC-Vfregdf64
RISC-VvregN/A仅 clobber
LoongArch32regi8, i16, i32, f32
LoongArch64regi8, i16, i32, i64, f32, f64
LoongArchfregff32
LoongArchfregdf64
s390xreg, reg_addri8, i16, i32, i64
s390xfregf32, f64
s390xvregvectori32, f32, i64, f64, i128,
i8x16, i16x8, i32x4, i64x2, f32x4, f64x2
s390xaregN/A仅 clobber
PowerPCspe_acc仅 clobber
PowerPC/PowerPC64regi8, i16, i32, i64(仅 PowerPC64)
PowerPC/PowerPC64reg_nonzeroi8, i16, i32, i64(仅 PowerPC64)
PowerPC/PowerPC64fregf32, f64
PowerPC/PowerPC64vregaltiveci8x16, i16x8, i32x4, f32x4
PowerPC/PowerPC64vregvsxf32, f64, i64x2, f64x2
PowerPC/PowerPC64vsregvsxvsx 和 altivec vreg 类型的并集
PowerPC/PowerPC64cr仅 clobber
PowerPC/PowerPC64ctr仅 clobber
PowerPC/PowerPC64lr仅 clobber
PowerPC/PowerPC64xer仅 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");
}

寄存器名称

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

架构基本寄存器别名
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]
PowerPC/PowerPC64r1sp
PowerPC/PowerPC64r31fp
PowerPC/PowerPC64r[0-31][0-31]
PowerPC/PowerPC64f[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)帧指针不能用作输入或输出。
ARMr7r11在 ARM 上,帧指针可以是 r7r11,具体取决于目标。帧指针不能用作输入或输出。
所有si(x86-32), bx(x86-64), r6(ARM), x19(AArch64 和 Arm64EC), x9(RISC-V), $s8(LoongArch), r29r30(PowerPC), r30(PowerPC64)LLVM 在内部将其用作具有复杂栈帧的函数的“基指针“。
x86ip这是程序计数器,不是真正的寄存器。
AArch64xzr这是一个常量零寄存器,不能被修改。
AArch64x18在某些 AArch64 目标上,这是操作系统保留的寄存器。
Arm64ECxzr这是一个常量零寄存器,不能被修改。
Arm64ECx18这是操作系统保留的寄存器。
Arm64ECx13, x14, x23, x24, x28, v[16-31], p[0-15], ffr这些是 Arm64EC 不支持的 AArch64 寄存器。
ARMpc这是程序计数器,不是真正的寄存器。
ARMr9在某些 ARM 目标上,这是操作系统保留的寄存器。
RISC-Vx0这是一个常量零寄存器,不能被修改。
RISC-Vgp, tp这些寄存器是保留的,不能用作输入或输出。
LoongArch$r0$zero这是一个常量零寄存器,不能被修改。
LoongArch$r2$tp这是为 TLS 保留的。
LoongArch$r21这是 ABI 保留的。
s390xc[0-15]内核保留。
s390xa[0-1]系统使用保留。
PowerPC/PowerPC64r2, r13这些是系统保留的寄存器。
PowerPC/PowerPC64vrsavevrsave 寄存器不能用作输入或输出。
#![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-32regeaxk
x86-64regraxq
x86-32reg_abcdlalb
x86-64reglalb
x86reg_abcdhahh
x86regxaxw
x86regeeaxk
x86-64regrraxq
x86reg_byteal / ah
x86xmm_regxmm0x
x86ymm_regymm0t
x86zmm_regzmm0g
x86*mm_regxxmm0x
x86*mm_regyymm0t
x86*mm_regzzmm0g
x86kregk1
AArch64/Arm64ECregx0x
AArch64/Arm64ECregww0w
AArch64/Arm64ECregxx0x
AArch64/Arm64ECvregv0
AArch64/Arm64ECvregvv0
AArch64/Arm64ECvregbb0b
AArch64/Arm64ECvreghh0h
AArch64/Arm64ECvregss0s
AArch64/Arm64ECvregdd0d
AArch64/Arm64ECvregqq0q
ARMregr0
ARMsregs0
ARMdregd0P
ARMqregq0q
ARMqrege / fd0 / d1e / f
RISC-Vregx1
RISC-Vfregf0
LoongArchreg$r1
LoongArchfreg$f0
s390xreg%r0
s390xreg_addr%r1
s390xfreg%f0
s390xvreg%v0
PowerPC/PowerPC64reg0
PowerPC/PowerPC64reg_nonzero3
PowerPC/PowerPC64freg0
PowerPC/PowerPC64vreg0
PowerPC/PowerPC64vsreg0

Note

  • 在 ARM 上 e / f:这打印 NEON 四字(128 位)寄存器的低或高双字寄存器名称。
  • 在 x86 上:我们对没有修饰符的 reg 的行为与 GCC 不同。GCC 将根据操作数值类型推断修饰符,而我们默认为完整的寄存器大小。
  • 在 x86 xmm_reg 上:xtg 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 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 选项必须与 nomemreadonly 选项组合使用,否则会发出编译时错误。
#![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! 从外部文件包含原始汇编代码时主要有用。

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

  • nomemreadonly 选项是互斥的:同时指定两者是编译时错误。
#![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_syntaxraw 选项。其余选项没有意义,因为内联汇编定义了整个函数主体。

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

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 不同,后者每次读取时可能具有不同的值(因为汇编代码中不存在这样的概念)。
  • 未指定为输出的任何寄存器在退出汇编代码时必须与进入时具有相同的值,否则行为未定义。
    • 这仅适用于可以指定为输入或输出的寄存器。其他寄存器遵循特定于目标的规则。
    • 请注意,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)。
      • 向量扩展状态(vtypevlvxsatvxrm)。
    • 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 上,方向标志(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);
}
}
  • 恢复栈指针和非输出寄存器到其原始值的要求仅在退出汇编代码时适用。
    • 这意味着不穿透且不跳转到任何 label 块的汇编代码,即使未标记为 noreturn,也不需要保留这些寄存器。
    • 当返回到与您进入的不同的 asm! 块的汇编代码时(例如用于上下文切换),这些寄存器必须包含您退出asm! 块进入时的值。
      • 您不能退出尚未进入的 asm! 块的汇编代码。您也不能退出已经退出的 asm! 块的汇编代码(除非先重新进入它)。
      • 您有责任切换任何特定于目标的状态(例如线程本地存储、栈边界)。
      • 您不能从一个 asm! 块中的地址跳转到另一个块中的地址,即使在同一函数或块内,而无需将其上下文视为可能不同并需要上下文切换。您不能假设这些上下文中的任何特定值(例如当前栈指针或栈指针下方的临时值)在两个 asm! 块之间保持不变。
      • 您可以访问的内存位置集是您进入和退出的 asm! 块允许的交集。
  • 您不能假设源代码中相邻的两个 asm! 块(即使它们之间没有其他代码)将在二进制文件中连续地址处,中间没有任何其他指令。
  • 您不能假设 asm! 块将恰好在输出二进制文件中出现一次。编译器允许实例化 asm! 块的多个副本,例如当包含它的函数在多个位置内联时。
  • 在 x86 上,内联汇编不得以指令前缀(如 LOCK)结尾,该前缀将应用于编译器生成的指令。
    • 由于内联汇编的编译方式,编译器目前无法检测到这一点,但将来可能会捕获并拒绝此情况。

Note

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

裸内联汇编规则

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

  • 根据调用约定和函数签名,未用于函数输入的任何寄存器在进入 naked_asm! 块时将包含未定义的值。
    • 内联汇编上下文中的“未定义值“意味着寄存器可以(非确定地)具有架构允许的任何可能值。特别是,它与 LLVM 的 undef 不同,后者每次读取时可能具有不同的值(因为汇编代码中不存在这样的概念)。
  • 所有被调用者保存的寄存器在返回时必须与进入时具有相同的值。
  • 调用者保存的寄存器可以自由使用。
  • 执行穿透汇编代码末尾时行为未定义。
    • 汇编代码中的每条路径都应以返回指令终止或发散。
  • 汇编代码允许读取和写入的内存位置集与 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