Pure Rust · #![forbid(unsafe_code)] · no_std

Assembly to machine code,
at runtime.

asm-rs turns assembly source text into machine code bytes for x86, x86-64, ARM32, Thumb, AArch64 and RISC-V — with labels, branch relaxation and a full preprocessor. No LLVM, no system assembler, no C.

cargo add asm-rs

One call, machine code out

The whole pipeline — preprocessor, lexer, parser, optimizer, encoder, linker — sits behind one function. Labels resolve, branches relax to the shortest form that reaches, and you get bytes.

use asm_rs::{assemble, Arch};

let code = assemble("
    xor  edi, edi        # status = 0
    mov  eax, 60         # __NR_exit
    syscall
", Arch::X86_64)?;

assert_eq!(code, [0x31, 0xFF, 0xB8, 0x3C, 0, 0, 0, 0x0F, 0x05]);

Why asm-rs

Correct, and checked against someone else

Output is cross-validated against independent decoders — iced-x86, bad64, yaxpeax-arm, riscv-decode — not against asm-rs's own expectations. Branch displacements are swept across each encoding's full range, and a differential fuzz target asks a decoder where each branch really goes.

No unsafe, anywhere

#![forbid(unsafe_code)] at the crate root, with zero required runtime dependencies. Memory safety is a property of the build, not a promise in a README.

Built for untrusted input

Every unbounded operation is metered — statements, labels, output size, macro recursion and expansion. Limits return errors rather than exhausting memory, so you can hand it text you did not write.

Runs where Rust runs

no_std + alloc, so it embeds in firmware, kernels and WebAssembly. Per-architecture Cargo features keep only what you use.

Predictable by default

The default optimization level only applies transforms that leave registers, memory and flags observably unchanged. Rewrites that clobber flags are opt-in, so output never surprises you.

Compile time, too

asm_bytes! and asm_array! assemble during compilation and emit a const byte array — no runtime cost, no runtime failure mode.

Six targets, one API

ArchitectureVariantsCoverage
x8632-bit, 16-bit real modeFull base ISA, .code16/.code32
x86-6464-bitSSE–SSE4.2, AVX/AVX2, AVX-512, AES-NI, BMI1/2, FMA3
ARM32A32 (ARMv7)Condition codes, barrel shifter, register ranges, literal pools
ThumbT16 / T32Automatic 16/32-bit selection, IT blocks
AArch64A64 (ARMv8+)NEON/AdvSIMD, LSE atomics, shifted/extended operands, vector lanes
RISC-VRV32I / RV64IM/A/C extensions, optional F/D and V, auto-compression

Full instruction reference →

use asm_rs::{Assembler, Arch};

let mut asm = Assembler::new(Arch::Aarch64);
asm.emit("
loop:
    subs x0, x0, #1     // UAL `#` immediates
    b.ne loop           // backward branch, resolved
    ret
")?;

let out = asm.finish()?;
println!("{}", out.listing());

Labels and relaxation, handled

Forward and backward references resolve automatically. Branches start in their short form and grow only when a target is out of reach — Szymanski's monotonic algorithm, so layout always converges.

A target the encoding cannot represent — too far, or misaligned for a scaled displacement — is an error, never a silently truncated jump.

Start assembling

Add one dependency and call one function.