Skip to main content

rustysnes_cpu/
disasm.rs

1//! A minimal 65C816 disassembler for human-facing debug output.
2//!
3//! Decode-only: [`disassemble_one`] takes a byte-peek closure (`FnMut`, so a caller may plug in
4//! a real `Bus::read24` when a side-effect-free peek accessor isn't convenient) and does nothing
5//! but decode the bytes it's handed — it never touches CPU state and has no connection to
6//! [`crate::exec`], the real cycle-accurate interpreter, so a bug here can never affect emulation
7//! correctness, only what a debugger prints. Prefer a genuinely read-only peek where one exists;
8//! the closure type doesn't enforce that, it only enables it. Built for the frontend's debugger
9//! overlay (`docs/frontend.md` §Debugger overlay) and for ad hoc instruction-level tracing (e.g.
10//! `docs/audit/`'s boot investigations).
11
12use alloc::format;
13use alloc::string::String;
14
15/// Addressing mode, used only to compute operand length and format the operand string.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum Mode {
18    Implied,
19    Accumulator,
20    /// Immediate, width follows the `M` (accumulator/memory) flag.
21    ImmediateM,
22    /// Immediate, width follows the `X` (index) flag.
23    ImmediateX,
24    /// Immediate, always one byte (`REP`/`SEP`/`COP`/`BRK`/`WDM`).
25    Immediate8,
26    Direct,
27    DirectX,
28    DirectY,
29    DirectIndirect,
30    DirectIndirectX,
31    DirectIndirectY,
32    DirectIndirectLong,
33    DirectIndirectLongY,
34    Absolute,
35    AbsoluteX,
36    AbsoluteY,
37    AbsoluteIndirect,
38    AbsoluteIndirectX,
39    AbsoluteIndirectLong,
40    AbsoluteLong,
41    AbsoluteLongX,
42    StackRelative,
43    StackRelativeIndirectY,
44    Relative,
45    RelativeLong,
46    BlockMove,
47}
48
49/// `(mnemonic, addressing mode)` for every one of the 256 opcodes — the fixed, standard WDC
50/// 65C816 opcode map (identical across every 65816 reference/emulator; not derived from
51/// `crate::exec`, which decodes the same map for execution rather than display).
52#[rustfmt::skip]
53const OPCODES: [(&str, Mode); 256] = {
54    use Mode::{
55        Absolute as A, AbsoluteIndirect as AI, AbsoluteIndirectLong as AIL,
56        AbsoluteIndirectX as AIX, AbsoluteLong as AL, AbsoluteLongX as ALX, AbsoluteX as AX,
57        AbsoluteY as AY, Accumulator as Acc, BlockMove as BM, Direct as D, DirectIndirect as DI,
58        DirectIndirectLong as DIL, DirectIndirectLongY as DILY, DirectIndirectX as DIX,
59        DirectIndirectY as DIY, DirectX as DX, DirectY as DY, Immediate8 as I8, ImmediateM as IM,
60        ImmediateX as IX, Implied as Imp, Relative as Rel, RelativeLong as RelL,
61        StackRelative as SR, StackRelativeIndirectY as SRIY,
62    };
63    [
64        ("BRK", I8), ("ORA", DIX), ("COP", I8), ("ORA", SR), ("TSB", D), ("ORA", D), ("ASL", D), ("ORA", DIL),
65        ("PHP", Imp), ("ORA", IM), ("ASL", Acc), ("PHD", Imp), ("TSB", A), ("ORA", A), ("ASL", A), ("ORA", AL),
66        ("BPL", Rel), ("ORA", DIY), ("ORA", DI), ("ORA", SRIY), ("TRB", D), ("ORA", DX), ("ASL", DX), ("ORA", DILY),
67        ("CLC", Imp), ("ORA", AY), ("INC", Acc), ("TCS", Imp), ("TRB", A), ("ORA", AX), ("ASL", AX), ("ORA", ALX),
68        ("JSR", A), ("AND", DIX), ("JSL", AL), ("AND", SR), ("BIT", D), ("AND", D), ("ROL", D), ("AND", DIL),
69        ("PLP", Imp), ("AND", IM), ("ROL", Acc), ("PLD", Imp), ("BIT", A), ("AND", A), ("ROL", A), ("AND", AL),
70        ("BMI", Rel), ("AND", DIY), ("AND", DI), ("AND", SRIY), ("BIT", DX), ("AND", DX), ("ROL", DX), ("AND", DILY),
71        ("SEC", Imp), ("AND", AY), ("DEC", Acc), ("TSC", Imp), ("BIT", AX), ("AND", AX), ("ROL", AX), ("AND", ALX),
72        ("RTI", Imp), ("EOR", DIX), ("WDM", I8), ("EOR", SR), ("MVP", BM), ("EOR", D), ("LSR", D), ("EOR", DIL),
73        ("PHA", Imp), ("EOR", IM), ("LSR", Acc), ("PHK", Imp), ("JMP", A), ("EOR", A), ("LSR", A), ("EOR", AL),
74        ("BVC", Rel), ("EOR", DIY), ("EOR", DI), ("EOR", SRIY), ("MVN", BM), ("EOR", DX), ("LSR", DX), ("EOR", DILY),
75        ("CLI", Imp), ("EOR", AY), ("PHY", Imp), ("TCD", Imp), ("JMP", AL), ("EOR", AX), ("LSR", AX), ("EOR", ALX),
76        ("RTS", Imp), ("ADC", DIX), ("PER", RelL), ("ADC", SR), ("STZ", D), ("ADC", D), ("ROR", D), ("ADC", DIL),
77        ("PLA", Imp), ("ADC", IM), ("ROR", Acc), ("RTL", Imp), ("JMP", AI), ("ADC", A), ("ROR", A), ("ADC", AL),
78        ("BVS", Rel), ("ADC", DIY), ("ADC", DI), ("ADC", SRIY), ("STZ", DX), ("ADC", DX), ("ROR", DX), ("ADC", DILY),
79        ("SEI", Imp), ("ADC", AY), ("PLY", Imp), ("TDC", Imp), ("JMP", AIX), ("ADC", AX), ("ROR", AX), ("ADC", ALX),
80        ("BRA", Rel), ("STA", DIX), ("BRL", RelL), ("STA", SR), ("STY", D), ("STA", D), ("STX", D), ("STA", DIL),
81        ("DEY", Imp), ("BIT", IM), ("TXA", Imp), ("PHB", Imp), ("STY", A), ("STA", A), ("STX", A), ("STA", AL),
82        ("BCC", Rel), ("STA", DIY), ("STA", DI), ("STA", SRIY), ("STY", DX), ("STA", DX), ("STX", DY), ("STA", DILY),
83        ("TYA", Imp), ("STA", AY), ("TXS", Imp), ("TXY", Imp), ("STZ", A), ("STA", AX), ("STZ", AX), ("STA", ALX),
84        ("LDY", IX), ("LDA", DIX), ("LDX", IX), ("LDA", SR), ("LDY", D), ("LDA", D), ("LDX", D), ("LDA", DIL),
85        ("TAY", Imp), ("LDA", IM), ("TAX", Imp), ("PLB", Imp), ("LDY", A), ("LDA", A), ("LDX", A), ("LDA", AL),
86        ("BCS", Rel), ("LDA", DIY), ("LDA", DI), ("LDA", SRIY), ("LDY", DX), ("LDA", DX), ("LDX", DY), ("LDA", DILY),
87        ("CLV", Imp), ("LDA", AY), ("TSX", Imp), ("TYX", Imp), ("LDY", AX), ("LDA", AX), ("LDX", AY), ("LDA", ALX),
88        ("CPY", IX), ("CMP", DIX), ("REP", I8), ("CMP", SR), ("CPY", D), ("CMP", D), ("DEC", D), ("CMP", DIL),
89        ("INY", Imp), ("CMP", IM), ("DEX", Imp), ("WAI", Imp), ("CPY", A), ("CMP", A), ("DEC", A), ("CMP", AL),
90        ("BNE", Rel), ("CMP", DIY), ("CMP", DI), ("CMP", SRIY), ("PEI", D), ("CMP", DX), ("DEC", DX), ("CMP", DILY),
91        ("CLD", Imp), ("CMP", AY), ("PHX", Imp), ("STP", Imp), ("JMP", AIL), ("CMP", AX), ("DEC", AX), ("CMP", ALX),
92        ("CPX", IX), ("SBC", DIX), ("SEP", I8), ("SBC", SR), ("CPX", D), ("SBC", D), ("INC", D), ("SBC", DIL),
93        ("INX", Imp), ("SBC", IM), ("NOP", Imp), ("XBA", Imp), ("CPX", A), ("SBC", A), ("INC", A), ("SBC", AL),
94        ("BEQ", Rel), ("SBC", DIY), ("SBC", DI), ("SBC", SRIY), ("PEA", A), ("SBC", DX), ("INC", DX), ("SBC", DILY),
95        ("SED", Imp), ("SBC", AY), ("PLX", Imp), ("XCE", Imp), ("JSR", AIX), ("SBC", AX), ("INC", AX), ("SBC", ALX),
96    ]
97};
98
99/// Operand byte count (excludes the opcode byte itself).
100const fn operand_len(mode: Mode, m8: bool, x8: bool) -> usize {
101    match mode {
102        Mode::Implied | Mode::Accumulator => 0,
103        Mode::ImmediateM => {
104            if m8 {
105                1
106            } else {
107                2
108            }
109        }
110        Mode::ImmediateX => {
111            if x8 {
112                1
113            } else {
114                2
115            }
116        }
117        Mode::Immediate8
118        | Mode::Direct
119        | Mode::DirectX
120        | Mode::DirectY
121        | Mode::DirectIndirect
122        | Mode::DirectIndirectX
123        | Mode::DirectIndirectY
124        | Mode::DirectIndirectLong
125        | Mode::DirectIndirectLongY
126        | Mode::StackRelative
127        | Mode::StackRelativeIndirectY
128        | Mode::Relative => 1,
129        Mode::Absolute
130        | Mode::AbsoluteX
131        | Mode::AbsoluteY
132        | Mode::AbsoluteIndirect
133        | Mode::AbsoluteIndirectX
134        | Mode::RelativeLong
135        | Mode::BlockMove => 2,
136        Mode::AbsoluteLong | Mode::AbsoluteLongX | Mode::AbsoluteIndirectLong => 3,
137    }
138}
139
140/// Disassemble one instruction at `pbr:pc`.
141///
142/// Reads operand bytes via `peek` (a 24-bit-address byte accessor — prefer a genuinely
143/// side-effect-free peek, e.g. `Bus::peek_*`, over a real bus `read` where one is available).
144/// `m8`/`x8` are [`crate::regs::Regs::m8`]/[`crate::regs::Regs::x8`] (`true` = 8-bit width) —
145/// needed because `LDA #`-style immediates are 1 or 2 operand bytes depending on them.
146///
147/// Returns `(text, length)`: a human-readable `"MNEMONIC operand"` string, and the total
148/// instruction length in bytes (opcode + operand, always >= 1) so a caller can advance to the
149/// next instruction without re-decoding.
150///
151/// # Panics
152/// Never in practice: the only `unwrap`/slice-conversion is over a fixed 2-byte prefix of the
153/// local 3-byte `op` buffer, never over caller-controlled data.
154#[must_use]
155pub fn disassemble_one(
156    mut peek: impl FnMut(u32) -> u8,
157    pbr: u8,
158    pc: u16,
159    m8: bool,
160    x8: bool,
161) -> (String, usize) {
162    let base = (u32::from(pbr) << 16) | u32::from(pc);
163    let opcode = peek(base);
164    let (mnemonic, mode) = OPCODES[opcode as usize];
165    let n = operand_len(mode, m8, x8);
166    let mut op = [0u8; 3];
167    for (i, byte) in op.iter_mut().enumerate().take(n) {
168        let i = u32::try_from(i).unwrap_or(u32::MAX);
169        *byte = peek(base.wrapping_add(1 + i) & 0x00FF_FFFF);
170    }
171    let text = match mode {
172        Mode::Implied => String::from(mnemonic),
173        Mode::Accumulator => format!("{mnemonic} A"),
174        Mode::ImmediateM | Mode::ImmediateX | Mode::Immediate8 => {
175            if n == 2 {
176                format!(
177                    "{mnemonic} #${:04X}",
178                    u16::from(op[0]) | (u16::from(op[1]) << 8)
179                )
180            } else {
181                format!("{mnemonic} #${:02X}", op[0])
182            }
183        }
184        Mode::Direct => format!("{mnemonic} ${:02X}", op[0]),
185        Mode::DirectX => format!("{mnemonic} ${:02X},X", op[0]),
186        Mode::DirectY => format!("{mnemonic} ${:02X},Y", op[0]),
187        Mode::DirectIndirect => format!("{mnemonic} (${:02X})", op[0]),
188        Mode::DirectIndirectX => format!("{mnemonic} (${:02X},X)", op[0]),
189        Mode::DirectIndirectY => format!("{mnemonic} (${:02X}),Y", op[0]),
190        Mode::DirectIndirectLong => format!("{mnemonic} [${:02X}]", op[0]),
191        Mode::DirectIndirectLongY => format!("{mnemonic} [${:02X}],Y", op[0]),
192        Mode::StackRelative => format!("{mnemonic} ${:02X},S", op[0]),
193        Mode::StackRelativeIndirectY => format!("{mnemonic} (${:02X},S),Y", op[0]),
194        Mode::Absolute => format!(
195            "{mnemonic} ${:04X}",
196            u16::from(op[0]) | (u16::from(op[1]) << 8)
197        ),
198        Mode::AbsoluteX => format!(
199            "{mnemonic} ${:04X},X",
200            u16::from(op[0]) | (u16::from(op[1]) << 8)
201        ),
202        Mode::AbsoluteY => format!(
203            "{mnemonic} ${:04X},Y",
204            u16::from(op[0]) | (u16::from(op[1]) << 8)
205        ),
206        Mode::AbsoluteIndirect => format!(
207            "{mnemonic} (${:04X})",
208            u16::from(op[0]) | (u16::from(op[1]) << 8)
209        ),
210        Mode::AbsoluteIndirectX => format!(
211            "{mnemonic} (${:04X},X)",
212            u16::from(op[0]) | (u16::from(op[1]) << 8)
213        ),
214        Mode::AbsoluteIndirectLong => format!(
215            "{mnemonic} [${:04X}]",
216            u16::from(op[0]) | (u16::from(op[1]) << 8)
217        ),
218        Mode::AbsoluteLong => format!("{mnemonic} ${:02X}{:02X}{:02X}", op[2], op[1], op[0]),
219        Mode::AbsoluteLongX => format!("{mnemonic} ${:02X}{:02X}{:02X},X", op[2], op[1], op[0]),
220        Mode::Relative => {
221            let rel = i16::from(op[0].cast_signed());
222            let target = pc.wrapping_add(2).wrapping_add(rel.cast_unsigned());
223            format!("{mnemonic} ${target:04X}")
224        }
225        Mode::RelativeLong => {
226            let rel = i16::from_le_bytes([op[0], op[1]]);
227            let target = pc.wrapping_add(3).wrapping_add(rel.cast_unsigned());
228            format!("{mnemonic} ${target:04X}")
229        }
230        Mode::BlockMove => format!("{mnemonic} ${:02X},${:02X}", op[0], op[1]),
231    };
232    (text, 1 + n)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::disassemble_one;
238
239    fn rom(bytes: &'static [u8]) -> impl Fn(u32) -> u8 {
240        move |addr: u32| bytes.get(addr as usize).copied().unwrap_or(0)
241    }
242
243    /// Like [`rom`], but `bytes[0]` is placed at address `base` instead of address `0` — needed
244    /// whenever the test passes a non-zero `pc` (relative-branch tests compute their target from
245    /// `pc`, so the opcode bytes must actually live there).
246    fn rom_at(base: u32, bytes: &'static [u8]) -> impl Fn(u32) -> u8 {
247        move |addr: u32| {
248            addr.checked_sub(base)
249                .and_then(|i| bytes.get(i as usize))
250                .copied()
251                .unwrap_or(0)
252        }
253    }
254
255    #[test]
256    fn decodes_implied() {
257        let (text, len) = disassemble_one(rom(&[0xEA]), 0, 0, true, true);
258        assert_eq!(text, "NOP");
259        assert_eq!(len, 1);
260    }
261
262    #[test]
263    fn decodes_immediate_8bit_accumulator() {
264        // LDA #$42 with M=1 (8-bit accumulator).
265        let (text, len) = disassemble_one(rom(&[0xA9, 0x42, 0xFF]), 0, 0, true, true);
266        assert_eq!(text, "LDA #$42");
267        assert_eq!(len, 2);
268    }
269
270    #[test]
271    fn decodes_immediate_16bit_accumulator() {
272        // LDA #$1234 with M=0 (16-bit accumulator).
273        let (text, len) = disassemble_one(rom(&[0xA9, 0x34, 0x12]), 0, 0, false, true);
274        assert_eq!(text, "LDA #$1234");
275        assert_eq!(len, 3);
276    }
277
278    #[test]
279    fn decodes_absolute() {
280        let (text, len) = disassemble_one(rom(&[0xAD, 0x00, 0x42]), 0, 0, true, true);
281        assert_eq!(text, "LDA $4200");
282        assert_eq!(len, 3);
283    }
284
285    #[test]
286    fn decodes_absolute_long() {
287        let (text, len) = disassemble_one(rom(&[0xAF, 0x00, 0x80, 0xC0]), 0, 0, true, true);
288        assert_eq!(text, "LDA $C08000");
289        assert_eq!(len, 4);
290    }
291
292    #[test]
293    fn decodes_direct_page_indexed() {
294        let (text, len) = disassemble_one(rom(&[0xB5, 0x10]), 0, 0, true, true);
295        assert_eq!(text, "LDA $10,X");
296        assert_eq!(len, 2);
297    }
298
299    #[test]
300    fn decodes_forward_branch() {
301        // BNE $+4 from PC=$1000 -> target = 1000 + 2 + 2 = $1004.
302        let (text, len) = disassemble_one(rom_at(0x1000, &[0xD0, 0x02]), 0, 0x1000, true, true);
303        assert_eq!(text, "BNE $1004");
304        assert_eq!(len, 2);
305    }
306
307    #[test]
308    fn decodes_backward_branch() {
309        // BRA $-2 from PC=$1000 -> target = 1000 + 2 - 2 = $1000 (branch to self).
310        let (text, len) = disassemble_one(rom_at(0x1000, &[0x80, 0xFE]), 0, 0x1000, true, true);
311        assert_eq!(text, "BRA $1000");
312        assert_eq!(len, 2);
313    }
314
315    #[test]
316    fn decodes_block_move() {
317        let (text, len) = disassemble_one(rom(&[0x54, 0x00, 0x7E]), 0, 0, true, true);
318        assert_eq!(text, "MVN $00,$7E");
319        assert_eq!(len, 3);
320    }
321
322    #[test]
323    fn decodes_relative_long() {
324        // BRL $+4 from PC=$2000 -> target = 2000 + 3 + 4 = $2007.
325        let (text, len) =
326            disassemble_one(rom_at(0x2000, &[0x82, 0x04, 0x00]), 0, 0x2000, true, true);
327        assert_eq!(text, "BRL $2007");
328        assert_eq!(len, 3);
329    }
330
331    #[test]
332    fn every_opcode_decodes_without_panicking() {
333        for opcode in 0u8..=255 {
334            let bytes: [u8; 4] = [opcode, 0, 0, 0];
335            let (text, len) = disassemble_one(
336                move |addr: u32| bytes.get(addr as usize).copied().unwrap_or(0),
337                0,
338                0,
339                true,
340                true,
341            );
342            assert!(!text.is_empty());
343            assert!((1..=4).contains(&len));
344        }
345    }
346}