fARM64
fARM64 is a pure-Rust, #![no_std], zero-heap AArch64 (A64) disassembler and semantic encoder. It decodes 64-bit Arm machine code into a rich, Copy value-type Instruction, renders it through a pluggable Formatter, and can re-encode an Instruction to a 32-bit word. The public API is deliberately iced-x86-shaped (a borrowing Decoder, a value-type Instruction, typed OpKind/Operand accessors, and a token-emitting Formatter). The Arm architectural decode tree is hand-written from the Arm Architecture Reference Manual (the "Arm ARM"); tests also cross-check independent toolchains and corpora. Apple AMX and GXF are implementation-defined exceptions whose encodings come from public reverse-engineering references and are explicitly runtime-gated.
Highlights
- Freestanding by default.
#![no_std]unconditionally, with noallocand nostdin the default build. No-CRT / bare-metal / wasm friendly; builds forwasm32-unknown-unknownandaarch64-unknown-none. - Zero heap on the core path.
Decoder::decode_intowrites into a caller-ownedCopyInstruction(noVec, noBox, no internal pointers); the default formatter writes into a fixed&mut [u8](BufSink) or anycore::fmt::Write. Copyvalue-typeInstruction. Pass it by value; inline[Operand; MAX_OPERANDS]storage,<= 112bytes, asserted at compile time. Never panics on malformed input — bad words decode toCode::Invalidwith a recordedlast_error.- Ergonomic iteration.
Decoderis anIterator(bothfor insn in &mut decand consumingfor insn in dec), plus adecode_intofast path for tight loops. - Broad ISA coverage. Full base A64 plus Advanced SIMD / FP, SVE / SVE2, SME / SME2, the crypto extensions, and a long tail of recent additions: MOPS, CSSC, RCPC3, D128, THE, LSE128, SVE2p1, CMPBR, CPA, and more.
- Encoder included.
Instruction::encode()reconstructs the 32-bit word from instruction semantics (never from the stored raw word), proving the decode is invertible. - Pluggable formatting. Default Arm UAL
FmtFormatter, an optionalGnuFormattercompatibility adapter behindfmt-gnu, a token-classifyingFormatterOutputsink, and aSymbolResolverhook.GnuFormattercurrently emits the same UAL text asFmtFormatter. - Two feature layers. Cargo features compile optional implementation modules; a runtime
FeatureSetcontrols which architectural and implementation-defined encodings the decoder accepts.
Supported targets
| Target | Notes |
|---|---|
x86_64-*, aarch64-* (hosted) |
development and std testing |
wasm32-unknown-unknown |
default features (no_std, no alloc) |
aarch64-unknown-none |
bare-metal, no-CRT; checked with --no-default-features |
any target providing core |
the default tier is core-only |
Feature matrix
Cargo features decide which optional implementation modules are compiled; the runtime FeatureSet decides which encodings are accepted at decode time. They are independent layers, but not every runtime extension has a matching Cargo feature.
| Cargo feature | Tier | Effect |
|---|---|---|
| (none / default) | A | no_std, no alloc, freestanding. Decoder + FmtFormatter + all enums + encoder. Always builds. |
alloc |
B | Adds String/Vec conveniences (format_to_string, a reusable cached InstructionInfoFactory, and a token-collecting String sink). |
std |
C | Implies alloc; adds std::error::Error for DecodeError/EncodeError and std-only test helpers. |
fmt-gnu |
A | Adds GnuFormatter, currently a UAL-equivalent compatibility adapter. Pure no_std. |
sve |
A | Compiles the SVE/SVE2 decoder and encoder modules. |
sme |
A | Compiles the SME/SME2 decoder and encoder modules. |
crypto |
A | Compiles the Advanced SIMD crypto decoder. Its public enum variants and encoder support remain present without this feature. |
full |
A | Enables sve, sme, and crypto. |
no-alloc-audit |
test | Enables allocation-counting tests for the zero-heap core path; not intended as a downstream capability. |
The default build links neither alloc nor std. std implies alloc. The runtime FeatureSet (FeatureSet::ALL, FeatureSet::BASE, .with(Feature::Sve), .has(..)) is orthogonal to all of the above.
Install
[]
# Default: no_std, no alloc, zero-heap decoder + formatter + encoder.
= "0.0.1"
Opt into more as needed:
# Owned-string conveniences and the cached info factory.
= { = "0.0.1", = ["alloc"] }
# All optional implementation modules plus std and the GNU adapter.
= { = "0.0.1", = ["std", "full", "fmt-gnu"] }
The import path uses the stylized crate name: use fARM64::....
Quick start
Zero-allocation decode-and-print into a fixed stack buffer — no heap, no std:
use ;
use ;
Decoder::new(data, ip, options) borrows the byte slice; ip is the address of data[0] and PC-relative operands resolve against it. decode() returns a Copy Instruction and advances the cursor by 4. The FmtFormatter::format call goes through the Formatter trait, which is why that trait is imported.
Decoding
use ;
Key points:
Decoder::new(data, ip, options)never panics. There is also atry_newreturningResult<Decoder, DecodeError>for API symmetry. A64 has no bitness parameter — it is always 64-bit, always 4-byte fixed-width.- Iteration forms.
for insn in &mut decborrows the decoder (you can inspectdec.last_error()afterward);for insn in decconsumes it. Both yield until fewer than 4 bytes remain. decode()vsdecode_into().decode()returns a freshInstruction.decode_into(&mut out)writes into a caller-ownedInstruction, which is the preferred zero-allocation form in a tight loop because nothing is constructed or moved per iteration:
use ;
- Position and address.
position()/set_position(pos)move the byte cursor (and keepipconsistent relative to the original base);ip()/set_ip(ip)read/set the current decode address directly. - Invalid handling. A malformed or unallocated word never panics — it decodes to an
InstructionwithCode::Invalid(checkinsn.is_invalid()), anddec.last_error()returns the reason (DecodeError::Unmatched,DecodeError::EndOfInstructionon a short tail, orDecodeError::Noneon success).
Restricting accepted extensions
DecoderOptions carries a FeatureSet. The default accepts everything (FeatureSet::ALL); narrow it to reject encodings outside the extensions you target:
use ;
Inspecting an Instruction
use ;
use ;
What each accessor means:
code()— the encoding-level identity (Code), one variant per distinct ARM ARM encoding row. Use this when you need the exact encoding (e.g. for re-encoding, or to distinguishB.condfromB).mnemonic()/mnemonic().name()— the width/encoding-independentMnemonic(alias-resolved for preferred disassembly such asMOV/CMP/LSL), and its&'static strspelling.op_count()/op_kind(n)/op(n)— operand count, theOpKinddiscriminant of slotn(out-of-range yieldsOpKind::None), and the full richOperand(out-of-range yieldsOperand::None).op_register(n)/op_immediate(n)— fast indexed accessors.op_registerreturnsRegister::Noneif slotnis not a plain register;op_immediatereturns the unsigned/logical/signed-as-u64/label value, or0otherwise.len()/ip()/next_ip()/word()— fixed length (always 4), decode address, following address (ip + 4), and the raw little-endian word.flow_control()—FlowControlclassification (branch / call / return / exception / next).set_flags()—FlagEffectNZCV behaviour (SetsNormal,SetsFloat, orNone).
Formatting
The default FmtFormatter renders ARM UAL syntax. It writes through the Formatter trait into any FormatterOutput sink. There is a single operand-dispatch path; nothing allocates inside the formatter itself.
Zero-alloc into a fixed buffer or any core::fmt::Write
use Write;
use ;
use ;
Owned String (requires alloc)
use ;
use ;
FormatterOptions
FmtFormatter::with_options(opts) overrides the defaults. Fields and their defaults:
| Field | Default | Meaning |
|---|---|---|
aliases |
true |
Emit preferred aliases (MOV/CMP/MUL/LSL/NOP/...) instead of canonical forms. |
uppercase_mnemonics |
false |
Upper-case mnemonics. |
uppercase_registers |
false |
Upper-case register names. |
use_sp_not_xzr |
true |
Render reg-31 as sp/wsp rather than xzr/wzr where the role is ambiguous. |
hex_prefix |
"0x" |
Prefix for hex literals. |
signed_immediates |
true |
Render signed immediates with an explicit - and hex magnitude. |
show_lsl_zero |
false |
Show LSL #0 explicitly instead of eliding it. |
space_after_operand_separator |
true |
", " vs "," between operands. |
first_operand_char_index |
8 |
Column at which the first operand starts (mnemonic field width). |
use ;
GnuFormatter is available behind feature = "fmt-gnu". It is a compatibility adapter that currently delegates to the UAL renderer, so its output is identical to FmtFormatter; the separate type leaves room for GNU-specific policy later without changing call sites.
A token sink (FormatterOutput + TokenKind)
For syntax coloring or post-processing, implement FormatterOutput and receive every chunk together with its TokenKind:
use ;
use ;
Resolving branch targets (SymbolResolver)
SymbolResolver maps an address to a borrowed name (no allocation required):
use Instruction;
use ;
;
Encoding
Instruction::encode() (or the free function fARM64::encode(&insn)) reconstructs the 32-bit little-endian word from the instruction's semantics — its Code, Mnemonic, operands, and ip. It deliberately never reads Instruction::word(), so a successful round-trip proves the decode is invertible. The encoder is no_std, zero-alloc, and total: it returns EncodeError rather than panicking.
use ;
EncodeError variants: Unsupported (this Code/group is not implemented yet), InvalidOperand (operand missing or of the wrong kind), InvalidImmediate (an immediate, shift, or PC-relative target with no valid field encoding), and Invalid (the Code::Invalid sentinel has no encoding).
Because the encoder rebuilds from the canonical Code, the guarantee is a semantic round-trip (the re-encoded word decodes to an equivalent instruction), not necessarily a byte-identical one for encodings that have multiple equivalent spellings.
Feature gating explained
There are two independent layers:
- Cargo features decide which optional implementation modules are compiled into the binary. Omitting
sveorsmeleaves those large decoder/encoder modules out;cryptogates the Advanced SIMD crypto decoder. Public enums are not compiled out, and features such as FP16, BF16, LSE, PAuth, and MTE have no Cargo gate. - The runtime
FeatureSetdecides what the decoder will accept at decode time. It is the fine-grained architectural gate and includes many extensions that are always compiled, as well as Apple AMX/GXF. An enabled runtime feature cannot restore an implementation module omitted by Cargo.
use ;
FeatureSet::ALL (the default) accepts everything; FeatureSet::BASE/NONE accept only the base ISA; .with(Feature::X) enables one extension; .has(Feature::X) queries one. Feature::Base is always present.
no_std, embedded, and wasm
The default build is #![no_std] with no alloc: it links neither an allocator nor std. The core decode path (Decoder + Instruction) and the default formatter (FmtFormatter + BufSink) never allocate — all names are &'static str from const tables, there are no thread-locals, no I/O, no time, and no panics-as-control-flow. This makes fARM64 usable directly in kernels, bootloaders, hypervisors, and wasm32-unknown-unknown. The alloc and std features are strictly additive conveniences; enabling them never changes the zero-heap behaviour of the core path.
Validation and testing
fARM64 is validated with focused unit/integration tests plus optional differential sweeps. Large corpus-dependent sweeps are #[ignore]d and require a locally supplied corpus that is not included in the published crate:
- Binary Ninja corpus comparison —
tests/golden.rs. Decodes a locally supplied corpus and compares rendered text.cargo test --features "std full" --test golden -- --ignored --nocapture - LLVM differential —
tests/llvm_diff.rs. A discovery sweep that compares fARM64 with an installedllvm-mc.cargo test --features "std full" --test llvm_diff -- --ignored --nocapture - Encoder round-trip —
tests/roundtrip.rs. Decodes the corpus, re-encodes from semantics, and checks the semantic round-trip.cargo test --features "std full" --test roundtrip -- --ignored --nocapture - Example CLI —
examples/disasm.rs. Decodes 8-hex-digit words from args or stdin:cargo run --example disasm 11000420 d503201f
The fast (non-ignored) unit and integration tests run with cargo test --features "std full". See docs/VALIDATION.md for the reproducible validation procedure and the distinction between required tests and optional local-oracle sweeps.
Project layout and architecture
src/
lib.rs crate docs, public re-exports, MAX_OPERANDS / INSN_LEN, static asserts
decoder.rs Decoder, DecoderOptions, iterators, position/ip, last_error
decode/ hand-written recursive A64 decode tree (+ shared ARM pseudocode)
encode/ hand-written A64 encoder (the inverse of decode)
instruction.rs the Copy value-type Instruction and its accessors
operand.rs Operand enum + OpKind discriminant
register.rs Register, RegClass, RegWidth, gp_register
enums.rs Condition / ShiftType / ExtendType / VectorArrangement / FlowControl / FlagEffect
mnemonic.rs Code (encoding identity) + Mnemonic (display) enums
features.rs Feature + FeatureSet (runtime accept/reject)
format/ Formatter trait, FmtFormatter, BufSink, options, token sink
info/ sysop/ sysreg/ tables/ info factory, system ops/registers, name tables
tests/ golden.rs, llvm_diff.rs, roundtrip.rs, the_atomics.rs
examples/ disasm.rs
docs/ DESIGN.md, API.md, ENCODING.md, ROADMAP.md, VALIDATION.md
Design and reference docs: docs/DESIGN.md, docs/API.md, docs/ENCODING.md, docs/ROADMAP.md, docs/VALIDATION.md.
Status
Version 0.0.1 is the initial crates.io release. The checked-in test suite is the release gate; optional corpus and LLVM sweeps provide additional local cross-checking but are not packaged and no fixed coverage percentage is promised. The Code/Mnemonic/Register/Feature enums are #[non_exhaustive] with an append-only discriminant policy.
License
Licensed under the MIT License; see LICENSE. Arm architectural instruction handling is based on the publicly documented Arm ARM. Apple AMX naming and encodings reference the public corsix/amx reverse-engineering project; GXF encodings reference Asahi Linux's Apple Proprietary Instructions documentation and are isolated behind Feature::Gxf. See NOTICE for provenance details.