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. - Editable instructions. Swap registers, immediates, memory bases, conditions and branch targets on a decoded
Instruction, thenencode()the result. Register replacement is class- and width-checked, operand decorations are preserved, and every setter is total — an edit with no valid encoding surfaces as anEncodeError, never a panic. - Implicit register reads/writes.
implicit_registers()reports the state an instruction touches without naming it: the link register of a call/return, theX30/SP/X16/X17of the pointer-authentication forms, the eight-registerLD64B/ST64Bgroup, the SVEFFR,PCfor PC-relative address generation, andNZCV. It is merged intoinstruction_info()'s access set. - 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 EnumValueError, plus 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.1.0"
Opt into more as needed:
# Owned-string conveniences and the cached info factory.
= { = "0.1.0", = ["alloc"] }
# All optional implementation modules plus std and the GNU adapter.
= { = "0.1.0", = ["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).implicit_registers()— the registers touched without an operand naming them (see below).
Implicit register reads and writes
A64 hides real dataflow behind the mnemonic. BL writes X30; PACIASP read-modifies X30 using SP; PACIA1716 read-modifies X17 using X16; LD64B <Xt> writes the eight registers Xt..Xt+7 while spelling only Xt; LDFF1* read-modifies the SVE FFR; ADR reads PC. None of that appears in the operand list.
implicit_registers() reports it as a fixed-capacity, allocation-free list. Architectural state with no numbered register is modelled as a pseudo-register appended to Register — Nzcv, Ffr, Za, Pc — which the decoder never produces as an operand and the formatter never prints (Register::is_pseudo() identifies them).
use ;
PC is reported only where it is a genuine data input (ADR/ADRP, the PC-relative literal loads, the FEAT_PAuth_LR PAC*SPPC forms). Sequential fetch and branch-target formation are not reported — otherwise every instruction would read PC — and a branch's resolved absolute target is already available from near_branch_target().
See docs/API.md for the full rule table.
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.
Re-encoding with different operands
Because the encoder works from semantics alone, editing a decoded Instruction and encoding it gives you the word for the edited instruction. That makes fARM64 usable as a small rewriter, not just a disassembler.
use ;
The setters:
| Setter | Edits |
|---|---|
set_op_register / set_op_register_unchecked |
the register of a single-register operand, keeping arrangement / lane / shift / extend / predicate |
set_op_immediate |
an immediate's value, keeping its Operand variant (and so how it is packed) |
set_op / push_op / set_op_count |
the operand list wholesale |
set_memory_base / set_memory_index / set_memory_displacement64 |
a memory operand, keeping its addressing mode |
set_condition |
the condition-code operand |
set_near_branch_target / set_label |
a resolved absolute target |
set_code / set_mnemonic |
the encoding identity / the displayed alias |
set_ip / relocate |
the address — set_ip keeps labels at the same absolute address, relocate keeps them at the same relative displacement |
Rules worth knowing:
- Every setter is total: it returns
falseand changes nothing when the edit does not apply, rather than panicking. set_op_registeris class- and width-checked — it will not put aWregister where anXregister was, because the operand size lives inCode, not in the operand. It also enforces an operand shape's own range where one is narrower than the register file (the 3-bit predicate-as-counterPNgfield takes onlyp8..p15). Pairset_op_register_uncheckedwithset_codeto change the width deliberately.- Edits are not validated against the encoding. A value with no representation in the instruction's fields (a non-bitmask logical immediate, an out-of-range branch target, a displacement the encoding cannot scale, an
ADRPtarget that is not 4 KiB-aligned) surfaces as anEncodeErrorfromencode()— never a silently rounded value. word()keeps returning the word the instruction was decoded from;is_modified()says it may be stale.re_encode()encodes and stores the new word in its place, and changes nothing on failure. Moving an instruction that is not PC-relative leaves both alone:set_ip/relocateonly mark an instruction modified when its encoding depends onip.
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.1.0 adds implicit register read/write analysis (implicit_registers(), plus the Nzcv/Ffr/Za/Pc pseudo-registers) and in-place instruction editing, so a decoded Instruction can be re-encoded with different operands. It also corrects several access-classification and flow-control results; see the changelog for the behaviour changes, notably that used_registers() now reports NZCV as Register::Nzcv. 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.