OXYGENHUB
# 9120e87480a6 enhanced-x16-emulator / src / debug_core.h
16.9 KiB · Objective-C · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// Commander X16 Emulator — debugger core: breakpoints.
2
//
3
// The breakpoint table and the decision "should execution stop here?", kept
4
// apart from debugger.c so the logic can be exercised on its own. debugger.c
5
// is an SDL window; this is not, and the interesting behaviour here -- bank
6
// matching, conditions, ignore counts -- is exactly the kind that is worth
7
// testing directly rather than by driving a UI.
8
//
9
// Upstream tracked a single breakpoint in one global. That is enough to answer
10
// "stop at this address" and nothing else: no second breakpoint, no way to say
11
// which RAM bank was meant, no "only when A == 3", no "skip the first 100
12
// iterations". Those are the things a debugging session actually needs, and
13
// they are what this adds.
14
15
#ifndef _DEBUG_CORE_H_
16
#define _DEBUG_CORE_H_
17
18
#include <stdint.h>
19
#include <stdbool.h>
20
21
#ifdef __cplusplus
22
extern "C" {
23
#endif
24
25
// A bank selector, stored on a breakpoint or watchpoint.
26
//
27
// Banks only mean anything at $A000-$FFFF: the RAM window follows the RAM bank,
28
// the ROM window the ROM bank, and everything below $A000 selects memory by
29
// address alone. So there are exactly two things a selector can say --
30
// "whichever bank is mapped" or "this specific bank" -- and for an address
31
// outside a banked window it says nothing at all.
32
//
33
// A selector recorded for an unbanked address is normalised to DEBUG_BANK_ANY
34
// on the way in, so it can never become a hidden field that distinguishes two
35
// otherwise identical entries. (A UI should grey the field out for such an
36
// address, for the same reason.)
37
#define DEBUG_BANK_ANY (-1)
38
39
// Who asked for a breakpoint or watchpoint.
40
//
41
// Several independent things want breakpoints, they do not know about each
42
// other, and more than one of them can want the same address at the same time:
43
// a `-bp` on the command line, the user at the keyboard, and each of the three
44
// kinds of breakpoint a DAP client can set. Without a record of who asked, the
45
// first remove disarms the address for all of them -- and the entry that is
46
// left behind belongs to nobody, which on a headless `-debugport` run means a
47
// machine that halts with nothing able to resume it.
48
//
49
// The core only ever compares these, so the list can be extended freely.
50
typedef enum {
51
	DEBUG_OWNER_CLI = 0,        // -bp / -wp
52
	DEBUG_OWNER_UI,             // the SDL debugger's F9, and the ImGui panels
53
	DEBUG_OWNER_DAP_SOURCE,     // DAP setBreakpoints
54
	DEBUG_OWNER_DAP_FUNCTION,   // DAP setFunctionBreakpoints
55
	DEBUG_OWNER_DAP_INSTRUCTION,// DAP setInstructionBreakpoints
56
	DEBUG_OWNER_DAP_CONSOLE,    // bp_add / watch_add typed in the debug console
57
	DEBUG_OWNER_STEP,           // the debugger's own step-over/step-out target
58
	DEBUG_OWNER_COUNT
59
} debug_owner_t;
60
61
// A breakpoint address. `bank` is the 65C816 program bank; `x16Bank` is the
62
// bank selector described above. Breakpoints are identified by all three.
63
//
64
// `owners` and `enabled` are maintained by the core; a caller that builds one
65
// of these to hand to debug_bp_add_for() should set only the three address
66
// fields and leave the rest zeroed.
67
struct breakpoint {
68
	int      pc;
69
	uint8_t  bank;
70
	int      x16Bank;
71
	uint16_t owners;  // bitmask of debug_owner_t; never 0 for a live entry
72
	bool     enabled; // a disabled breakpoint keeps its place, and its count
73
};
74
75
// The live table. Read-only for everyone outside debug_core.c: everything that
76
// changes it goes through the functions below, so that ownership cannot be
77
// bypassed. Prefer debug_bp_count()/debug_bp_at().
78
extern struct breakpoint *breakPoints;
79
extern int                numBreakpoints;
80
81
// What a condition tests. Values are part of the debugger's external interface
82
// (the DAP server and the GUI both name them), so do not renumber.
83
enum {
84
	BPOPERAND_A = 0,
85
	BPOPERAND_X,
86
	BPOPERAND_Y,
87
	BPOPERAND_SP,
88
	BPOPERAND_P,
89
	BPOPERAND_BYTE, // the byte at operand_addr
90
	BPOPERAND_WORD, // the little-endian word at operand_addr
91
};
92
93
// How it is compared. Also external; do not renumber.
94
enum {
95
	BPCMP_EQ = 0,
96
	BPCMP_NE,
97
	BPCMP_LT,
98
	BPCMP_LE,
99
	BPCMP_GT,
100
	BPCMP_GE,
101
};
102
103
// Does a recorded bank selector apply to `addr` right now? See DEBUG_BANK_ANY.
104
// Public so that every matcher in the emulator uses one rule -- the step
105
// breakpoint in debugger.c included -- rather than each re-deriving it and
106
// disagreeing about what -1 means.
107
bool debug_bank_selector_matches(int selector, int addr, uint8_t pbank);
108
109
// What the core will actually record for a selector at this address. Outside a
110
// banked window there is nothing to select, so it stores DEBUG_BANK_ANY. A
111
// caller that keeps its own copy of the key must normalise the same way, or its
112
// copy will not match the entry the core created.
113
int debug_normalise_bank(int selector, int addr, uint8_t pbank);
114
115
// Which RAM/ROM window `pc` sits in given program bank `pbank`, or
116
// DEBUG_BANK_ANY where a bank means nothing there. Exported so callers that
117
// produce selectors use the same rule as the matcher above: a second copy will
118
// eventually disagree with it, and the one that existed did -- on gen1 with a
119
// non-zero program bank, where read6502 forces the bank to zero and the window
120
// registers still apply.
121
int debug_current_x16_bank(int pc, uint8_t pbank);
122
123
// Build a breakpoint from an address reference, deciding what a packed bank byte
124
// meant. `ref` is up to 24 bits: the low 16 are the address, and a non-zero top
125
// byte is the 65C816 program bank on a Gen2 and the RAM/ROM window bank
126
// otherwise -- read6502 forces the program bank to zero off Gen2, so a top byte
127
// there cannot be one. `explicit_bank` carries a window bank the caller was told
128
// separately (the "bb:aaaa" form), or DEBUG_BANK_ANY; naming one both ways is
129
// refused rather than resolved. `offset` is a byte displacement, wrapped inside
130
// the 16-bit address so it cannot carry into the bank.
131
//
132
// Returns false when the request could never match -- a bank named for an
133
// address no window register reaches, or a reference out of range -- so a caller
134
// can say so instead of arming an entry that silently never fires. `out` is
135
// left alone on failure, and receives an unowned, disabled entry on success,
136
// ready for debug_bp_add_for().
137
bool debug_bp_from_ref(long ref, int explicit_bank, long offset, struct breakpoint *out);
138
139
// ---- Table management ------------------------------------------------------
140
// Breakpoints are identified by all three of (pc, bank, x16Bank), so the same
141
// address in two different RAM banks is two different breakpoints.
142
//
143
// Every entry records which owners asked for it. Two owners wanting one address
144
// share a single entry -- there is only ever one place the CPU can stop -- but
145
// the entry survives until the last of them lets go. This is the whole point of
146
// the API: an owner can clear everything it asked for without having to know
147
// what anyone else wanted.
148
149
// What debug_bp_add_for() did. "Already there" is deliberately distinct from
150
// "no room": a caller that conflates them silently mishandles a full table.
151
typedef enum {
152
	DEBUG_ADD_CREATED = 0, // a new entry, now owned by the caller
153
	DEBUG_ADD_EXISTED,     // an entry was already there; the caller now owns it too
154
	DEBUG_ADD_FULL         // out of memory; nothing was armed
155
} debug_add_result_t;
156
157
// Add `owner`'s reference. Adding one that this owner already holds is
158
// idempotent. A re-added breakpoint is re-enabled, since asking for a
159
// breakpoint is a request for it to be armed.
160
debug_add_result_t debug_bp_add_for(struct breakpoint bp, debug_owner_t owner);
161
162
// Drop `owner`'s reference. The entry itself only goes when the last owner
163
// does. Returns whether this owner held one.
164
//
165
// The condition and hit count survive, exactly as they do for any other
166
// removal: a front end that replaces its whole breakpoint list on every edit
167
// (which is how the Debug Adapter Protocol works) passes through here
168
// constantly, and a hit count that reset on each keystroke would be useless.
169
bool debug_bp_remove_for(int pc, uint8_t bank, int x16Bank, debug_owner_t owner);
170
171
// Drop every reference `owner` holds -- what each DAP "here is the complete
172
// list" request wants, and what session teardown wants. Returns how many
173
// entries were disarmed entirely (as opposed to left for another owner).
174
int debug_bp_clear_owner(debug_owner_t owner);
175
176
// Delete the breakpoint outright, whoever asked for it.
177
//
178
// This is the human's delete -- F9, or `bp_remove` typed in the console -- and
179
// it is deliberately not a refcount operation. Someone at the keyboard asking
180
// for a breakpoint to go and watching it stay armed because a client also
181
// wanted it is worse than the confusion this ownership model exists to fix.
182
// Returns whether anything was there.
183
bool debug_bp_delete(int pc, uint8_t bank, int x16Bank);
184
185
// Does `owner` hold a reference here? For a UI marking its own breakpoints.
186
bool debug_bp_has_owner(int pc, uint8_t bank, int x16Bank, debug_owner_t owner);
187
188
// The human's F9: delete whatever is at this address, or create one owned by
189
// `owner` if there is nothing there.
190
void debug_bp_toggle_for(int pc, uint8_t bank, int x16Bank, debug_owner_t owner);
191
192
int  debug_bp_find(int pc, uint8_t bank, int x16Bank);
193
void debug_bp_clear_all(void);
194
195
// ---- Enable / disable ------------------------------------------------------
196
// A disabled breakpoint keeps its entry, its owners, its condition and its hit
197
// count, and simply does not stop the machine.
198
//
199
// Front ends used to implement this by removing the entry and remembering it
200
// themselves, which is the same mistake as reconstructing ownership from
201
// outside: the breakpoint vanishes from the table, so every other view of it
202
// loses its marker and its count.
203
bool debug_bp_set_enabled(int pc, uint8_t bank, int x16Bank, bool enabled);
204
bool debug_bp_is_enabled(int pc, uint8_t bank, int x16Bank);
205
206
// ---- Read-only view --------------------------------------------------------
207
// For UIs and the DAP server listing what is set. `debug_bp_at` returns NULL if
208
// `index` is out of range, and its result is valid only until the next call
209
// that adds or removes a breakpoint.
210
int                      debug_bp_count(void);
211
const struct breakpoint *debug_bp_at(int index);
212
213
// ---- The hot-path question -------------------------------------------------
214
// Tell the core the CPU has arrived at (pc, bank), and get back whether to stop.
215
//
216
// Named as an event rather than a question because it IS one: it counts the hit
217
// and spends the ignore budget. Call it once per instruction, only while
218
// actually running, and only from whatever owns execution. Calling it while
219
// halted re-counts the same arrival -- the debugger polls with the PC parked on
220
// the breakpoint, so that would burn through an ignore count while the machine
221
// stands still. Use debug_bp_is_set() for the read-only question.
222
bool debug_bp_on_arrival(int pc, uint8_t bank);
223
224
// Whether any *enabled* breakpoint covers (pc, bank), asking nothing and
225
// changing nothing. This is the one to use for "would the machine stop here?".
226
// A disabled breakpoint is still in the table -- see debug_bp_at() for a view
227
// that includes it.
228
bool debug_bp_is_set(int pc, uint8_t bank);
229
230
// ---- Conditions and counts -------------------------------------------------
231
// A breakpoint stops when its condition (if any) holds AND it has been hit more
232
// times than its ignore count. Counting is per breakpoint and starts the first
233
// time that breakpoint is reached, so a count exists even for one that was
234
// never given a condition. Only arrivals that satisfy the condition are
235
// counted, so "ignore the first 100" means 100 that would otherwise have
236
// stopped, not 100 that merely passed through.
237
//
238
// A count deliberately survives removing and re-adding the same breakpoint,
239
// since that is how a UI implements an enable/disable toggle and a toggle
240
// should not silently reset it. That applies to owner-scoped removal too, so a
241
// client re-sending its breakpoint list does not reset the counts it is about
242
// to ask about. The two calls that discard a count are debug_bp_forget(), for
243
// one breakpoint, and debug_bp_clear_all(), which is a delete of everything
244
// rather than a disable.
245
//
246
// A condition belongs to the address, not to whoever asked for it. Two owners
247
// wanting different conditions at one address is a real conflict with no
248
// correct answer, and per-owner conditions would mean per-owner evaluation on
249
// arrival, on the hot path. The last writer wins; a front end that replaces a
250
// breakpoint list should therefore state the condition it wants every time,
251
// including clearing it, rather than assuming removal cleared it.
252
void     debug_bp_set_condition(int pc, uint8_t bank, int x16Bank, int operand,
253
                                uint16_t operand_addr, int op, uint32_t value);
254
void     debug_bp_clear_condition(int pc, uint8_t bank, int x16Bank);
255
void     debug_bp_forget(int pc, uint8_t bank, int x16Bank);
256
void     debug_bp_set_ignore(int pc, uint8_t bank, int x16Bank, uint32_t ignore);
257
uint32_t debug_bp_get_hits(int pc, uint8_t bank, int x16Bank);
258
void     debug_bp_reset_hits(int pc, uint8_t bank, int x16Bank);
259
bool     debug_bp_get_condition(int pc, uint8_t bank, int x16Bank, int *has_cond, int *operand,
260
                                uint16_t *operand_addr, int *op, uint32_t *value,
261
                                uint32_t *ignore);
262
263
// Release everything. Only needed at shutdown or in tests.
264
void debug_core_free(void);
265
266
// ---- Memory write watchpoints ----------------------------------------------
267
// "Stop when something writes to this address" -- the counterpart to a
268
// breakpoint for data rather than code, and the tool for finding what is
269
// corrupting a variable when you have no idea which code is responsible.
270
//
271
// Checked by a linear scan on every CPU store, so the scan is skipped entirely
272
// while none are set: the cost is only paid during an active debugging session.
273
// The table is fixed-size because a scan per store has to stay small, and 64 is
274
// roomy enough to watch a whole structure a field at a time.
275
#define MAX_WATCHPOINTS 64
276
277
struct watchpoint {
278
	uint16_t addr;
279
	uint16_t len;       // 1 = a single byte, >1 = a range starting at addr
280
	int      x16Bank;   // bank selector, as for a breakpoint (see DEBUG_BANK_ANY)
281
	bool     active;
282
	bool     has_value; // when set, only fire if the written value compares true
283
	uint8_t  value;
284
	int      op;        // BPCMP_* comparison code
285
	uint16_t owners;    // bitmask of debug_owner_t; maintained by the core
286
};
287
288
// Watch `len` bytes from `addr` in bank `x16Bank` (see DEBUG_BANK_ANY).
289
// Identified by (addr, x16Bank), like a breakpoint, so the same address can be
290
// watched in more than one bank.
291
//
292
// Ownership works exactly as it does for breakpoints, and for the same reason:
293
// `-wp` on the command line, the debugger and a DAP client can all want one
294
// address, and the first remove must not disarm it for the others. A watch that
295
// already exists keeps its length and value filter; the new owner is asking to
296
// be told about the address, not to redefine someone else's watch.
297
debug_add_result_t debug_wp_add_for(uint16_t addr, uint16_t len, int x16Bank, debug_owner_t owner);
298
bool debug_wp_remove_for(uint16_t addr, int x16Bank, debug_owner_t owner);
299
int  debug_wp_clear_owner(debug_owner_t owner);
300
bool debug_wp_delete(uint16_t addr, int x16Bank);
301
bool debug_wp_has_owner(uint16_t addr, int x16Bank, debug_owner_t owner);
302
void debug_wp_clear_all(void);
303
int  debug_wp_count(void);
304
// Read-only access to watchpoint `index`, for listing them in a UI. NULL when
305
// the index is out of range. The pointer is into the live table, so it is only
306
// valid until the next add/remove.
307
const struct watchpoint *debug_wp_at(int index);
308
// Exact-identity test, the counterpart to debug_wp_covers(). Use this to decide
309
// whether a toggle should add or remove, so that the decision and the removal
310
// agree on the key.
311
bool debug_wp_exists(uint16_t addr, int x16Bank);
312
313
// Only fire when the value being written compares true against `value`, so
314
// "stop when this becomes zero" does not stop on every other write to it.
315
// Returns false if no watchpoint starts at (addr, x16Bank).
316
bool debug_wp_set_value(uint16_t addr, int x16Bank, int op, uint8_t value);
317
// Remove a value filter set by debug_wp_set_value(), so the watchpoint fires on
318
// any write again.
319
bool debug_wp_clear_value(uint16_t addr, int x16Bank);
320
321
// Temporarily stop a watchpoint firing without forgetting how it was set up.
322
bool debug_wp_set_active(uint16_t addr, int x16Bank, bool active);
323
324
// Does any watchpoint cover `addr`? Read-only; for a UI marking watched bytes.
325
bool debug_wp_covers(uint16_t addr);
326
327
// Read-only view of the watchpoint table, for UIs and the DAP server that need
328
// to list what is being watched. Returns NULL if `index` is out of range.
329
// Valid until the next call that adds or removes a watchpoint.
330
const struct watchpoint *debug_wp_at(int index);
331
332
// The CPU is writing `value` to `addr`: should execution stop? Honours the
333
// value filter. Called from the store path, so callers should test
334
// debug_wp_count() first and skip this entirely when nothing is being watched.
335
bool debug_wp_check_write(uint16_t addr, uint8_t value);
336
337
#ifdef __cplusplus
338
} // extern "C"
339
#endif
340
341
#endif // _DEBUG_CORE_H_