🎓 A1Learner
🔍

Rust Inline Assembly

Inline assembly is a way to give the CPU one machine instruction directly. Kernel and hardware code sometimes must speak one instruction directly.

Two instructions, in and out

asm! comes from std::arch. The first argument is a template string. Its {} slots are filled by the operands listed after it. Each extra string is one more instruction line:

use std::arch::asm;

let input: u64 = 3;
let output: u64;
unsafe {
    asm!("mov {0}, {1}", "add {0}, 5", out(reg) output, in(reg) input);
}
println!("output = {output}");

inout: one register, both jobs

in sends a value in and out receives a result. inout does both through the same register — reg lets the compiler pick any general register:

let mut x: u64 = 3;
unsafe {
    asm!("add {0}, 5", inout(reg) x);
}
println!("x = {x}");

Name the damage: a clobber

This code also overwrites register r10 without returning it. Listing r10 as an extra out operand named _ tells the compiler to save what it needs. Unlisted damage is called a clobber:

let mut value: u64 = 1;
unsafe {
    asm!("mov {0}, 9", "mov r10, 0", inout(reg) value, out("r10") _);
}
println!("value = {value}");

Guard machine-specific code

Instruction text differs on every CPU family, so give each machine its own version with #[cfg]:

#[cfg(target_arch = "x86_64")]
fn fast_path() { /* the x86-64 asm! version */ }

#[cfg(not(target_arch = "x86_64"))]
fn fast_path() { /* a plain Rust fallback */ }

Options make promises

Add options only when they are true. These promise: no side effects, no memory access and no stack pushes — and they let the compiler optimise harder:

let y: u64;
unsafe {
    asm!("mov {0}, 7", out(reg) y, options(pure, nomem, nostack));
}
println!("y = {y}");

Example

use std::arch::asm;
#[cfg(target_arch = "x86_64")]
fn run() {
    let input: u64 = 3;
    let output: u64;
    unsafe {
        asm!("mov {0}, {1}", "add {0}, 5", out(reg) output, in(reg) input);
    }
    println!("3 + 5 = {}", output);
    let mut x: u64 = 3;
    unsafe {
        asm!("add {0}, 5", inout(reg) x);
    }
    println!("inout gives {}", x);
}
#[cfg(not(target_arch = "x86_64"))]
fn run() {
    println!("this example needs x86_64");
}
fn main() { run(); }

Key points

  • Every asm! call must sit inside an unsafe block.
  • Without one, the compiler stops with error E0133.
  • It cannot check handwritten instructions.
  • You take responsibility instead.
  • Inline assembly exists for x86 and x86-64, ARM, AArch64 and RISC-V.
  • The instruction text differs on each.
  • Guard machine-specific code with #[cfg(target_arch = "...")] when other machines may build it.
  • Inline assembly hands single machine instructions to the CPU from inside Rust code.
  • You will almost never need asm! in a normal program.

In this example, run() exists in two versions and the #[cfg] lines keep the x86-64 one on this machine. Inside it, the first asm! has two instruction lines. mov copies the input, 3, into the output register. add adds 5. So output becomes 8. The line prints 3 + 5 = 8. in(reg) input sends the value in and out(reg) output receives the result. The second asm! is shorter. inout(reg) x is both input and output. Adding 5 turns x itself into 8. Then inout gives 8 prints. Both calls sit inside unsafe blocks, as every asm! must.

Note

It exists for kernels, drivers and the last drops of speed — reach for it last, not first.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why must asm! sit inside an unsafe block?