Instruction's mnemonic, operands and flow are decoded from the encoding by iced-x86, which is x86/x64 only. Every other instruction set answers InstructionSet::Other, no operands and Flow::Unknown — a deliberate refusal (#145), not an oversight.
ARM64 is the one that matters: this crate reads ARM64 kernel dumps, and a consumer that follows control flow refuses those targets outright as a result (see the companion issue in windbg-mcp).
What is actually needed is much less than a disassembler
A caller following control flow needs Flow, not a full rendering — the engine already supplies the rendering in Instruction::text. ARM64's flow-relevant encodings are a small, regular set of fixed 32-bit words:
- unconditional branch and branch-with-link (
b, bl) — a 26-bit signed offset
- conditional branch (
b.cond) — a 19-bit signed offset
- compare-and-branch and test-and-branch (
cbz/cbnz, tbz/tbnz) — 19- and 14-bit offsets
- the register forms (
br, blr, ret) — indirect, so Flow::*(None)
- the trap forms (
brk, udf)
Everything else is Flow::Fallthrough. That is a bounded, testable piece of work against fixed-width encodings, and it does not require adopting a second decoder crate — though one (yaxpeax-arm, bad64, disarm64) is a reasonable alternative if operands are wanted too.
Decide Flow first and operands separately. Flow alone unblocks the consumer; typed operands for ARM64 are a larger job with no caller waiting for them, and Operand::Other is already the honest answer until then.
What to keep from how x64 was done
- Decode the encoding, never the rendering. #145 went the other way first and spent three review rounds on it: a comma inside
std::map<int,int>, a parenthesis inside operator(), a bracket inside operator[] each severed an operand and turned a direct call into an indirect one.
- A destination is an
Option. None means indirect, and a caller reading None as "no edge" stays sound.
- Branch targets are canonicalised into the instruction's address space (
canonical_target). ARM64 is fixed-width and 64-bit, so this should be a no-op there — but assert it rather than assume it.
- Measure against a real target rather than composed lines.
examples/typed_disassembly.rs exists for exactly this: run it over a whole ARM64 routine and read the Other and Unknown counts. On x64 that measurement is 376 instructions with zero of each, and it found two defects the unit tests had not.
Related
- #146 —
function_extent refuses ARM64's unwind record. Independent of this, and both are needed before a consumer can walk an ARM64 function: this one says where control goes, that one says where the function ends.
- Measured on an ARM64 kernel dump today: effective machine
0xaa64, nt!KeBugCheckEx renders as stp fp,lr,[sp,#-0x10]! and friends, and function_extent reports Unsupported.
Instruction'smnemonic,operandsandfloware decoded from the encoding byiced-x86, which is x86/x64 only. Every other instruction set answersInstructionSet::Other, no operands andFlow::Unknown— a deliberate refusal (#145), not an oversight.ARM64 is the one that matters: this crate reads ARM64 kernel dumps, and a consumer that follows control flow refuses those targets outright as a result (see the companion issue in
windbg-mcp).What is actually needed is much less than a disassembler
A caller following control flow needs
Flow, not a full rendering — the engine already supplies the rendering inInstruction::text. ARM64's flow-relevant encodings are a small, regular set of fixed 32-bit words:b,bl) — a 26-bit signed offsetb.cond) — a 19-bit signed offsetcbz/cbnz,tbz/tbnz) — 19- and 14-bit offsetsbr,blr,ret) — indirect, soFlow::*(None)brk,udf)Everything else is
Flow::Fallthrough. That is a bounded, testable piece of work against fixed-width encodings, and it does not require adopting a second decoder crate — though one (yaxpeax-arm,bad64,disarm64) is a reasonable alternative if operands are wanted too.Decide
Flowfirst and operands separately. Flow alone unblocks the consumer; typed operands for ARM64 are a larger job with no caller waiting for them, andOperand::Otheris already the honest answer until then.What to keep from how x64 was done
std::map<int,int>, a parenthesis insideoperator(), a bracket insideoperator[]each severed an operand and turned a direct call into an indirect one.Option.Nonemeans indirect, and a caller readingNoneas "no edge" stays sound.canonical_target). ARM64 is fixed-width and 64-bit, so this should be a no-op there — but assert it rather than assume it.examples/typed_disassembly.rsexists for exactly this: run it over a whole ARM64 routine and read theOtherandUnknowncounts. On x64 that measurement is 376 instructions with zero of each, and it found two defects the unit tests had not.Related
function_extentrefuses ARM64's unwind record. Independent of this, and both are needed before a consumer can walk an ARM64 function: this one says where control goes, that one says where the function ends.0xaa64,nt!KeBugCheckExrenders asstp fp,lr,[sp,#-0x10]!and friends, andfunction_extentreportsUnsupported.