Unsafe Rust

Inline Assembly Overview

Inline assembly embeds target-architecture machine instructions directly inside a Rust function via the std::arch::asm! macro (or at module scope via std::arch::global_asm!). Because the compiler cannot verify what registers, flags, memory addresses, or calling-convention invariants raw CPU instructions modify, executing asm! always requires an unsafe block.

Supported Architectures

Stabilized in core::arch / std::arch, asm! supports major targets out of the box:

  • x86 and x86_64 (uses Intel syntax by default; AT&T syntax optional via options(att_syntax))
  • aarch64 (ARM64) and arm
  • riscv32 and riscv64
  • loongarch64 and s390x

Operand Direction Specifiers in asm!

Operands bind Rust variables to hardware register classes (reg, reg_byte, xmm_reg) using explicit data-flow direction keywords:

Operand Specifier Syntax Meaning
in(reg) expr Read-only input Copies expr into an allocated general-purpose register before the assembly runs
out(reg) var Write-only output Writes the register's final value into var after the assembly finishes (_ discards output)
inout(reg) var Read-modify-write Initializes register from var on entry and writes the modified register back to var on exit
inout(reg) in_expr => out_var Split input/output Initializes register with in_expr and writes final register value to out_var
lateout(reg) var Late output Output written after all inputs are consumed (allows compiler to reuse an input register)
in("rax") expr / out("x0") var Explicit register Pins operand to a specific architectural register (required for syscalls and instructions like cpuid)
clobber_abi("C") ABI clobber Declares that the assembly may overwrite caller-saved registers of the "C" ABI

Portable Multi-Architecture asm! Example

Using conditional compilation (#[cfg(target_arch = "...")]), an asm! block can execute architecture-specific instructions while binding cleanly to Rust local variables:

use std::arch::asm;

fn multiply_add_asm(a: u64, b: u64, c: u64) -> u64 {
    let mut result: u64 = a;

    // SAFETY: The assembly instructions only mutate the register allocated for
    // `result` and preserve the stack pointer and ABI invariants.
    unsafe {
        #[cfg(target_arch = "aarch64")]
        asm!(
            "madd {res}, {lhs}, {rhs}, {acc}",
            res = out(reg) result,
            lhs = in(reg) a,
            rhs = in(reg) b,
            acc = in(reg) c,
            options(pure, nomem, nostack),
        );

        #[cfg(target_arch = "x86_64")]
        asm!(
            "imul {res}, {rhs}",
            "add {res}, {acc}",
            res = inout(reg) result,
            rhs = in(reg) b,
            acc = in(reg) c,
            options(pure, nomem, nostack),
        );
    }

    result
}

fn main() {
    let ans = multiply_add_asm(6, 7, 8);
    println!("6 * 7 + 8 = {}", ans);
}
6 * 7 + 8 = 50

Important options(...) Flags

Providing accurate options(...) flags enables the optimizer to schedule and eliminate dead asm! blocks safely:

Option Flag Contract Asserted by Programmer
pure Assembly has no side effects and its output depends solely on its inputs (must be paired with nomem or readonly)
nomem Assembly does not read or write any memory addresses
readonly Assembly may read memory, but never writes to memory
nostack Assembly does not push data onto or touch the stack frame (allows preserving the x86_64/AArch64 red zone)
preserves_flags Assembly does not modify CPU condition flags (EFLAGS / NZCV)
noreturn Assembly never falls through to the end (diverges like !)