OXYGENHUB
◆ main enhanced-x16-emulator / src / code_map.h
11.8 KiB · Objective-C · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — disassembly-accuracy core ("code map").
2
//
3
// THE PROBLEM: 65C02/65C816 instructions are variable length, and on the
4
// 65C816 the operand width of many instructions depends on the M/X bits of the
5
// processor status register. A naive linear disassembler therefore drifts out
6
// of alignment: start one byte off (or assume the wrong operand width) and
7
// every following instruction is garbage until it accidentally re-syncs.
8
//
9
// THE FIX: capture ground truth while the program actually runs. Every time the
10
// CPU fetches an instruction we record (a) that the address is a REAL
11
// instruction start, keyed by the bank context that actually backs it, and (b)
12
// the effective processor status (M/X/E folded in) at that moment. The anchored
13
// disassembler then aligns instruction boundaries using, in priority order:
14
//   1. recorded instruction starts (live execution ground truth),
15
//   2. cc65 .dbg span starts (compiler-emitted boundaries),
16
//   3. a backward self-sync heuristic (try candidate offsets, keep the one that
17
//      decodes cleanly onto the anchor).
18
// A decode is never allowed to swallow a recorded start UNLESS it is itself
19
// recorded -- two recorded starts that overlap are two real execution paths,
20
// not a bad guess straddling a good one -- so an over-wide guess is corrected
21
// at the next piece of hard evidence rather than drifting on.
22
//
23
// WHERE THE WIDTHS ARE STILL A GUESS, so callers know what they are getting:
24
// only five instructions change the 65C816 register widths -- REP, SEP, XCE,
25
// PLP and RTI. REP and SEP apply their own status bits exactly (the bits are in
26
// the operand), though the widths those imply are still subject to the E
27
// estimate below. XCE is exact only when both of its inputs are right, and
28
// neither is guaranteed: it swaps the carry and emulation flags, and the carry
29
// is not tracked through any data-dependent change (ADC, SBC, the compares, the
30
// shifts and rotates), while E is not recorded by anchors at all. The last two
31
// restore a status byte pulled off the stack; this linear, instruction-stream
32
// analysis does not recover it.
33
//
34
// None of these gaps is bounded unconditionally.
35
//
36
// A wrong STATUS estimate can be corrected when an accurate, believed anchor is
37
// reached -- but that anchor fixes its OWN line, and accuracy does not
38
// automatically carry forward: an anchor on a PLP or an RTI supplies the status
39
// going INTO it, and the byte it restores is still not modelled, so the very
40
// next line can be wrong again. A same-opcode stale anchor is "recorded" and
41
// hands back its old status, and there may be no accurate anchor ahead at all.
42
//
43
// A wrong E is weaker again, because anchors do not store E: an anchor fixes
44
// the width of its own line, and propagation then folds the stale E back in,
45
// which can mis-size the next unanchored line wherever the stale E and the real
46
// one imply different widths. It is not unrecoverable -- an XCE decoded with an
47
// accurate carry writes a correct E, as does an explicit CLC/SEC before one --
48
// but nothing guarantees such a sequence appears, so no persistent recovery is
49
// promised.
50
//
51
// Note the emulation-mode escape clause applies to the ESTIMATE, not the
52
// machine: the real CPU in emulation mode always has 8-bit widths, but the
53
// fold-in here uses the estimated E, so a diverged estimate can size operands
54
// as though native while the machine is really in emulation. Pinned by tests.
55
//
56
// This costs nothing in practice unless all of the following hold at once: the
57
// machine is a 65C816 (on the 65C02 widths never vary), the line being drawn
58
// has no currently believed anchor, and the status the estimate carries into
59
// that line is not the status the code there really runs with. That last one is
60
// broader than the unmodelled instructions above -- an address-linear walk over
61
// a branch or call, or a walk started away from the PC, does it too. See
62
// docs/code-map-width-propagation.md. Every line this could affect already
63
// reports `recorded = false`.
64
//
65
// Anchors are not deleted when the code under them changes -- there is no write
66
// hook -- so each stores the opcode byte that was executing and is ignored once
67
// memory no longer matches. That catches most overlay loads, a second program
68
// loaded over the first, and self-modifying code, without needing a hook on
69
// every memory write. It is a one-byte check, not a proof: replacement code
70
// that happens to repeat the same opcode byte at the same address keeps the old
71
// anchor, and with it the old recorded status. Such an anchor is still
72
// believed, so it can cover a genuine instruction start in the new code; and on
73
// a 65C816 its stale status can also make the line decode wider -- or narrower
74
// -- than the new code really is. Self-correcting once an accurate anchor it
75
// has not swallowed is reached, if there is one ahead, and pinned by tests. See
76
// docs/code-map-width-propagation.md.
77
//
78
// This header is plain C and is shared by the C core (recording hooks, DAP) and
79
// the C++ ImGui panel (via debug_ui_bridge.h).
80
#ifndef _CODE_MAP_H_
81
#define _CODE_MAP_H_
82
83
#include <stdint.h>
84
#include <stdbool.h>
85
86
#ifdef __cplusplus
87
extern "C" {
88
#endif
89
90
// Record a real instruction start as the CPU executes it. Call this at every
91
// instruction fetch, from every execution site, so coverage is complete.
92
//   pc       : the instruction-start address (regs.pc before step6502()).
93
//   pbank    : CPU program bank (regs.k; always 0 on the 65C02). Part of the
94
//              key only where it actually selects memory -- on a Gen2 machine a
95
//              non-zero program bank maps its own flat 64K, while on gen1 the
96
//              program bank selects nothing even with -c816.
97
//   rambank  : active RAM bank  (memory_get_ram_bank())  — banks $A000-$BFFF.
98
//   rombank  : active ROM bank  (memory_get_rom_bank())  — banks $C000-$FFFF.
99
//   status   : effective processor status. The caller folds the emulation-mode
100
//              width bits in (E set => M/X forced to 8-bit) so the value can be
101
//              fed straight into disasm()'s implied_status.
102
// Cheap: one bitset set, one status store and one opcode read/store. Safe to
103
// call every instruction.
104
void code_map_record(uint16_t pc, uint8_t pbank, uint8_t rambank, uint8_t rombank, uint8_t status);
105
106
// True if `pc` has been recorded as a real instruction start in the given bank
107
// context AND the opcode byte recorded there still matches memory.
108
bool code_map_is_recorded_start(uint16_t pc, uint8_t pbank, uint8_t rambank, uint8_t rombank);
109
110
// The effective status recorded at `pc`, or `fallback` if `pc` was never
111
// recorded in this context -- or if its anchor is no longer believed, because
112
// the opcode byte under it has changed.
113
uint8_t code_map_recorded_status(uint16_t pc, uint8_t pbank, uint8_t rambank, uint8_t rombank, uint8_t fallback);
114
115
typedef enum {
116
	CM_LINE_INSTRUCTION = 0, // a whole instruction: text is its disassembly
117
	CM_LINE_DATA        = 1, // raw bytes: text is ".byte $xx,..." over exactly
118
	                         // `size` bytes, and there is no effective address
119
} code_map_line_kind_t;
120
121
// One disassembled line produced by the anchored disassembler.
122
typedef struct {
123
	uint16_t addr;         // start address of this line
124
	uint8_t  size;         // bytes this line covers (always >= 1)
125
	uint8_t  status;       // implied_status actually used to decode it
126
	int32_t  eff_addr;     // effective address, or -1 if none
127
	bool     recorded;     // a whole instruction, backed by live-execution
128
	                       // coverage. False for every CM_LINE_DATA row.
129
	uint8_t  kind;         // code_map_line_kind_t: instruction or raw data
130
	bool     start_recorded; // this ADDRESS carries a currently believed anchor.
131
	                         // Distinguishes "no live-execution evidence here"
132
	                         // from "there is, but the row could not be shown
133
	                         // whole on this path". Not proof the current code
134
	                         // ran: an anchor can be evicted with its bank
135
	                         // context (false negative) or survive a same-opcode
136
	                         // overwrite (false confidence).
137
	uint8_t  bytes[4];     // raw bytes (first `size` valid)
138
	char     text[48];     // mnemonic + operands, or ".byte $xx,..."
139
} code_map_line_t;
140
141
// Produce an aligned disassembly window centered on `center`.
142
//   center            : address to center on (usually regs.pc).
143
//   bank              : CPU program bank (regs.k; 0 on the 65C02 / X16 gen1).
144
//   rambank, rombank  : bank context for banked reads and coverage lookup.
145
//   lines_before      : number of instructions the backward walk resolves
146
//                       before `center` (capped at 255; negative is treated as
147
//                       none). Tiling can turn one resolved instruction into
148
//                       several rows -- see the note on the returned count
149
//                       below.
150
//   lines_after       : number of rows from `center` onward, including the
151
//                       center row itself. A row count, not an instruction
152
//                       count: a decode forced to give way emits a
153
//                       CM_LINE_DATA row, which consumes one of them.
154
//   out               : caller-provided array receiving the lines, low->high.
155
//   max_out           : capacity of `out`.
156
//   out_center_index  : receives the index within `out` of the `center` line
157
//                       (-1 if it did not fit); may be NULL.
158
//
159
// The lines written TILE the range they cover: each one starts exactly where
160
// the previous one ended, with no gaps and no overlaps, so a caller can render
161
// them as consecutive rows without reconciling contradictory byte ranges.
162
//
163
// Holding that invariant occasionally costs an extra line. Where a decode has
164
// to be cut short -- it would swallow an address live execution proved is an
165
// instruction start, or it disagrees with a boundary the backward walk already
166
// resolved -- the bytes up to the next start are covered by a further line
167
// rather than left out. The count returned can therefore exceed
168
// `lines_before + lines_after`; it never exceeds `max_out`. Callers wanting a
169
// fixed row count should use the returned count, not the requested one.
170
//
171
// Returns the number of lines written (<= max_out). Only `out[0 .. returned-1]`
172
// are meaningful: resolving the window may use the rest of the buffer up to
173
// `max_out` as scratch, so a caller must not expect entries beyond the returned
174
// count to be preserved. Nothing is ever written past `max_out`.
175
int code_map_disasm_window(uint16_t center, uint8_t bank, uint8_t rambank, uint8_t rombank,
176
                           int lines_before, int lines_after,
177
                           code_map_line_t *out, int max_out, int *out_center_index);
178
179
// Disassemble a run of `count` rows starting at `start`, aligned via the flag
180
// map. Fills `out` low->high and returns the count written. Used by the DAP
181
// `disassemble` request. `next_addr` (may be NULL) receives the address just
182
// past the last row. Rows are normally whole instructions, but a decode forced
183
// to give way to a recorded start inside it comes back as CM_LINE_DATA, so a
184
// caller asking for N rows may get fewer than N instructions' worth of address
185
// range.
186
int code_map_disasm_forward(uint16_t start, uint8_t bank, uint8_t rambank, uint8_t rombank,
187
                            int count, code_map_line_t *out, int max_out, uint16_t *next_addr);
188
189
// Given a known instruction boundary `addr`, resolve the start address of the
190
// instruction immediately preceding it, using recorded starts, then .dbg spans,
191
// then a backward self-sync heuristic. Returns the predecessor address (falls
192
// back to addr-1 when nothing aligns cleanly).
193
uint16_t code_map_prev_instruction(uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank);
194
195
// Forget all recorded coverage and flags (e.g. on machine reset).
196
void code_map_reset(void);
197
198
#ifdef __cplusplus
199
} // extern "C"
200
#endif
201
202
#endif // _CODE_MAP_H_