3.9.1 The backend API¶
Four headers in src/debug/ are the whole interface a frontend sees:
| Header | Declares |
|---|---|
debugger.h |
jnext::dbg::Debugger, and the session types: ClientKind, ClientInfo, Listener, Service, PumpBudget, ServiceHint, PausedInfo, StopPolicy, LoopDriver, LogLevel |
events.h |
the event vocabulary: EventKind and its sub-kinds, Event, EventFilter, Subscription, SubscriptionInfo, Condition, Handler, Action, Hit, ClientId, EventId |
inspect.h |
the inspection value types: MemSpace, SlotInfo, RegId, RunState, PauseReason, Time, MachineInfo, RewindRange, InputState, Layer, ClipLayer, PaletteId and the rest |
result.h |
Result, the closed set of refusals, and Expected<T> |
They are a frozen interface. A verb is added only by owner decision, and each
declaration says which capability id of doc/design/DEBUG-SUBSYSTEM-ARCHITECTURE.md
§4 it serves. doc/design/debug-subsystem/b0-cap-traceability.md maps each
capability id to its declarations, though not completely: rgb333_to_argb(),
for one, is missing from it.
Conventions¶
Every verb that can refuse returns a Result. The set is closed, and every
value is pinned by debug_types_check.cpp:
| Value | Result |
Meaning |
|---|---|---|
| 0 | Ok |
done |
| 1 | RefusedRunning |
needs a paused machine |
| 2 | RefusedPaused |
needs a running machine |
| 3 | RefusedCorrupt |
an unacknowledged corruption incident blocks it (CTL-11) |
| 4 | RefusedRzx |
an RZX is recording or playing |
| 5 | RefusedUnavailable |
benign: the thing is not there right now (empty rewind buffer, trace off, no loop driver, an unknown bookmark) |
| 6 | RefusedReadOnly |
a ROM target |
| 7 | InvalidPage |
a page number that names no page |
| 8 | NotAtFrameBoundary |
a frame-boundary-only verb, mid-frame, asked not to advance |
| 9 | NoFrame |
a deferred capture that never got its frame |
| 10 | Unsupported |
there is no such thing, for example a verb called from inside an event delivery that may not be |
result_name() gives each one stable lowercase spelling for logs and protocol
error strings, and ok(r) is the named test for success.
A verb that refuses and also yields data returns Expected<T>: a status
and a value in one object, testable with if (auto r = ...). On a refusal the
value is value-initialised, with one exception: peek and poke report how
many bytes did transfer even when they refuse.
A query that cannot refuse returns its value directly. The class is listed
in full in debugger.h's "DIRECT-VALUE QUERIES" banner, with per-group counts
so the list can be audited. Moving a query onto that list is a claim that it
cannot fail. pump() is in neither class: it is the loop owner's service call.
Every transition and every mutation is attributed. A verb that pauses,
resumes, steps, resets, loads or writes takes ClientId by as its first
argument, and the backend broadcasts transitions with it. Reads take none. The
backend itself acts as CLIENT_NONE.
No ownership token, no arbitration. One machine, any number of clients, and the last verb wins. The only thing owned is a subscription, which only the client that created it may change or remove.
Memory is addressed by MemSpace, and the index is part of the space.
MemSpace |
Addresses |
|---|---|
MemSpace::cpu() |
the live 64 KB CPU map, every overlay included (DivMMC, Multiface, Layer 2, the NR 0x8C alternate ROM) |
MemSpace::page(p) |
the physical 8 KB page p, numbered as NR 0x50-0x57 numbers it (0..223), regardless of any overlay. 0xFE and 0xFF, the ROM sentinels, answer InvalidPage |
MemSpace::rom(i) |
the 16 KB ROM image i (0..3), read-only |
MemSpace is also the answer to what supplied this byte: the read arbiter
(Mmu::route()) names its source with the same type, so it carries nine more
kinds that only the arbiter produces: boot_rom(), multiface_rom(),
multiface_ram(), divmmc_rom(), divmmc_ram(bank), alt_rom(i),
layer2(page), config(bank) and floating() (nothing answers). peek and
poke of one of them answer Unsupported and write nothing, until a consumer
adds the access with its own rows. The type lives in debug/mem_space.h
(standard headers only, so the core's memory/mmu.h can return it), and
FetchSource is its name in the role of an opcode fetch's source.
A client never composes a space from a slot's page number. On the Next a ROM
slot's effective_page is an un-shifted SRAM page index, outside page()'s
numbering. So mmu_slots() returns each slot's complete backing store as
SlotInfo::space plus SlotInfo::space_offset. rom_select() names the image
legacy paging selects, even while RAM is paged at 0x0000.
space is the slot's OWN source. SlotInfo::now is what a read of the slot
reaches right now, overlays included (DivMMC, Multiface, Layer 2, alt ROM,
config mode, boot ROM): Mmu::route(slot << 13).space, the live data route of
the slot's first byte. One value per slot is complete, because every arm of the
VHDL arbiter switches on cpu_a(15:13) boundaries or coarser. It is the DATA
view: the instruction at the PC may come from elsewhere, which is what
source_page(pc) answers.
The spelling of a space for a human or a file (the tags P0E, ROM1, DIVRAM3..., the physical
text and the AddressStyle) is debug/long_address.h. The tags are a stable format; they are described
in 7.1, with the slot that holds a page >= 0xE0
and reads nothing.
The capability families¶
Each family keeps its design id prefix, which is how the headers, the tests and the design documents refer to it.
Control — CAP-CTL¶
| Verb | What it does |
|---|---|
pause(by) |
stop at the next instruction boundary. Idempotent; a second pause re-attributes the stop to its caller |
run(by) |
resume. Still Ok on a running machine. Subject to the corruption gate |
step_into(by) |
one instruction, synchronously, frame-loop aware: a step at a HALT runs the halt out |
step_over(by), step_out(by) |
asynchronous: the stop arrives later as a Paused push |
run_to(by, addr), run_to_source_line(by, "file:line"), run_to_cycle(by, c), run_to_frame(by, f), run_to_end_of_frame(by), run_to_end_of_scanline(by) |
resume until a target. run_to_source_line arms a transient Execute group at every start of the line (each tied to its memory, subscribe_source_line); the first one reached stops the machine and the stop reads RunTo with that address. It is refused, the machine untouched, for a line with no code. A run-to issued while the machine RUNS first drops the caller's own transient targets (a second Run to Here replaces the first), never another client's |
step_back(by, n), rewind_to_frame(by, f) |
rewind, synchronously. A benign refusal is RefusedRzx or RefusedUnavailable (no ring or an empty one, a frame outside it or not recorded, the trace off or empty, or a step back that would replay across a debugger change), and logs its reason |
resume_blocked_by_corruption(), acknowledge_corruption(gen) |
the CTL-11 gate after a failed restore |
reset(by, Soft \| Hard) |
Soft is Emulator::soft_reset(). Hard is the reconstruct contract of 3.9.3 |
load(by, path) |
through the loop owner's LoopDriver::load; same contract when the load rebuilds the machine |
state() |
RunState: paused, step mode, pause_reason, cycle, frame, PC |
magic_breakpoint(), set_magic_breakpoint(on) |
the magic opcode switch |
armed(), persistent_breakpoints(), set_persistent_breakpoints(on) |
whether the hot loop consults the backend at all |
step_over() asks is_call_like() about the instruction at PC (CALL,
CALL cc, RST, DJNZ). If it is, it arms a transient Execute
subscription at the next instruction and resumes. Otherwise it is a
step_into().
Inspection and mutation — CAP-INS¶
Reads never perturb the machine. Writes are first-class, logged by the backend, and follow the contract of 3.9.2.
| Area | Reads | Writes |
|---|---|---|
| registers | registers() |
set_register(by, RegId, v), one register at a time. Setting PC clears halted |
| memory | peek(space, addr, n, buf), memory_reader() |
poke(by, space, addr, n, buf) |
| paging | mmu_slots(), paging_ports(), rom_select() |
set_mmu_slot(by, slot, page), through the NR 0x50+slot write handler |
| NextREG | nextreg_peek(reg), nextreg_selected() |
nextreg_write(by, reg, v), which runs the register's own handler |
| ports | — | port_in(by, port), port_out(by, port, v); both dispatch like the guest's IN/OUT and are perturbing by nature |
| time and raster | time(), raster(), machine() |
— |
| sprites | sprites(), sprite_attr_raw(i), pattern_ram(), sprite_palette_rgb333(bank, i), sprite_clip() |
set_sprite_attr_raw(...), write_pattern_ram(...) |
| Copper | copper() |
— |
| audio | ay_registers(chip), turbosound_enabled(), ay_mode(), stereo_mode(), audio_mute_mask() |
set_audio_mute_mask(by, m) (host-side, never in a snapshot or an RZX) |
| video | framebuffer(), render_layer(layer, vc, dst, stride), palette(id), active_ula_palette_bank(), ula_screen_regs(), clip_window(layer) |
set_palette(...), set_border(by, colour) |
| disassembly | disassemble(addr, n, symbols), instruction_length(addr), is_call_like(addr) |
— |
| call stack and trace | call_stack(), trace_entries(), trace_export(path) |
set_call_stack_enabled(by, on) (a per-client request, 7.10); C variables: c_frames(), c_locals(frame), c_globals(), read_c_value(frame, path), c_variables_status() (all const, none bumps the revision), set_trace_enabled, trace_clear, trace_resize |
| coverage | coverage(), coverage_enabled(); INS-21: mem_coverage_enabled(), mem_coverage(space, addr, n, out), mem_coverage_fetch(addr), mem_coverage_spaces(), executed_source_lines(file) |
coverage_enable, coverage_clear; INS-21: mem_coverage_enable, mem_coverage_clear |
| input | input_state() |
see input injection below |
| the event model | subscriptions(include_transient), events_fired_since(seq) |
see 3.9.2 |
Three of these are worth understanding before using them.
peek(MemSpace::cpu()) is Mmu::peek(). Mmu::read() is the guest's
read: it captures the byte into the +3 floating-bus latch on a contended access
and raises the data-breakpoint latch on a READ watchpoint. Mmu::peek() wraps
read() under DebugState::InspectionScope and puts the floating-bus byte
back. It wraps rather than copies, so the overlay arbitration (boot ROM,
Multiface, DivMMC, Layer 2, alternate ROM, config mode) cannot drift between
the two. The trace's word at SP reads the same way.
The frame number is a tag, not the raw counter. Emulator::frame_num() is
post-incremented at the start of each frame, so during frame K it reads K+1.
time().frame and state().frame report frame_num() - 1. That is the tag
the rewind slot for the frame carries, and the frame --delayed-keypress-frames N
lands on. A rewind lands on a frame start the ring has already counted;
RewindRange::at_restored_frame_start says when the machine sits on one, and
run_to_frame() takes its base from it.
raster() and time() of a paused machine are snapshots taken at the
query. While the machine runs they report the live beam from the clock.
render_layer() (src/debug/debugger_render.cpp) draws one of the eight
Layer views of the paused frame: composite, ULA primary and shadow, Layer 2
active and shadow, sprites, tilemap, and the NR 0x4A background. It fills rows
0..vc of a RENDER_WIDTH-wide buffer with 0x00000000, so alpha 0 means
transparent. It then replays every per-scanline change log the way
Renderer::render_frame does, so a raster split shows as on screen. The render
is state-preserving: the replay walks each log to its end, and the port 0x303B
sprite status bits are saved and restored. debugger_backend_test INS-14-08
compares the whole serialised machine state around each view.
Events — CAP-EVT¶
| Verb | What it does |
|---|---|
subscribe(by, Subscription) |
register a subscription owned by by; returns its EventId |
unsubscribe(by, id), set_enabled(by, id, on) |
owner only |
subscribe_source_line(by, "file:line", like) |
a source breakpoint: resolves the line in the live source map and subscribes one Execute per start, each a copy of like with lo = hi = start.address and space = start.space, all under one new GroupId. All or nothing; refused with no map, no code at the line, or a like that is not Execute |
unsubscribe_group(by, group), set_group_enabled(by, group, on) |
every member of a source breakpoint; owner only. SubscriptionInfo::group says which group a listed subscription belongs to (GROUP_NONE for a plain one) |
master_enabled(), set_master_enabled(on) |
suspend every non-transient subscription without touching its own flag |
client_enabled(cid), set_client_enabled(cid, on) |
the per-client switch |
probe_execute(pc) |
the ids of every live Execute subscription that would fire at pc, with its condition evaluated. A pure query, for step loops |
raise_host_event(by, name) |
raise a Host event, script1..script8 or any name up to MAX_HOST_EVENT_NAME |
3.9.2 describes the subscription model and the delivery contract.
Input injection and capture — CAP-IN, CAP-CAP¶
Pulses append. press_key(by, name | MatrixKey, hold_frames) queues a
pulse on Keyboard's auto-type queue. Keyboard::queue_auto_type() appends
for every producer: the phantom typist, the tape LOAD "" sites,
--delayed-keypress and the backend. An append behind an entry in flight
leaves that entry's counters alone, so a held key does not restart its hold.
Keyboard::MAX_AUTO_TYPE_KEYS (16) caps the union of what every producer
queued. A pulse that does not fit is refused with RefusedUnavailable and the
count that was queued.
Every injection lands at the frame edge. A pulse or a level set
(set_key, set_extended_key) issued during frame N — from a handler, from a
remote command in a pump, from a CLI countdown — is applied at the end of frame
N and is visible to the guest from frame N+1. The level sets are queued on
Keyboard and applied first thing in the edge's tick_auto_type(), so a
handler cannot change what the rest of its own frame reads. end_of_frame()
delivers the Frame event before that tick, so an on frame N handler's pulse
is pressed by frame N's own tick. set_joystick and press_nmi are immediate.
Screenshots wait for the next rendered frame. screenshot(by, path,
layer_mask, format) only queues. pump() writes every capture whose frame has
been rendered since it was queued (Emulator::rendered_frames() moved), before
any command of that pump can change the machine. While a capture waits, its
layer mask is armed on the renderer and a force-render bit makes sure the next
frame is rendered whatever the frontend's render-skip hint says. A paused
machine renders nothing, so the capture is held, with one warning.
flush_captures(by) is the exit bound. It answers NoFrame if any of by's
captures is still pending (they are dropped), RefusedUnavailable if one failed
to write since the last call, and Ok otherwise. A capture survives its
requester's detach and every machine rebuild. The PNG and .SCR writers live in
src/core/screenshot.*, below the platform layer.
ula_screen_dump() returns the ULA screen as a .SCR image. Layer 2, tilemap
and pattern RAM come out of peek(MemSpace::page(...)).
State and rewind — CAP-ST¶
Saves share one rule. save_state_bytes, bookmark_save and
save_snapshot reach a frame boundary through one helper. At a boundary they
save. Mid-frame, SaveStateMode::RefuseMidFrame answers NotAtFrameBoundary,
and AdvanceToBoundary runs the frame out under SuspendScope and logs a
MUTATE clock … by <client> line, because emulated time moved under every other
client. save_snapshot always advances, as --delayed-snapshot does, and
writes the file by extension through save_snapshot_file() in src/core/.
A window inside the frame edge is not a boundary. Between the Frame delivery
and the end of tick_auto_type() the machine is not the state run_frame()
hands back, so a save from a Frame handler is refused, or answers
NotAtFrameBoundary.
Bookmarks are named, in memory and per client: bookmark_save,
bookmark_restore, bookmarks(cid). A client holds at most 8; a ninth new name
is refused and re-saving a name replaces it. They are freed at detach and
survive a hard reconstruct. A restore into a machine of another type or
snapshot width is refused before load_state runs, so nothing is latched.
DZRP's READ_STATE/WRITE_STATE and ZRCP's snapshot-save/snapshot-load
are bookmarks.
load_state_bytes restores in-process bytes. A failure latches corruption,
which the CTL-11 gate then holds every resume on. The rewind controls are
rewind_enabled, set_rewind_enabled, rewind_range(), resize_rewind_buffer
and rewind_blocked(), which returns the Result a rewind would refuse with,
for greying a control before it is clicked. It knows only the refusals that do
not depend on the target: an RZX, no ring, an empty ring. The rest — no trace
entry, a frame outside the ring or not recorded, a replay that would cross a
debugger change (3.9.2) — are known only
when the verb runs.
A refused rewind says why. On every RefusedRzx or RefusedUnavailable,
step_back and rewind_to_frame log one Warn line to every listener:
REWIND REFUSED: Step Back refused: the instruction trace is off - turn on Debug > Trace > Enable Trace and run forward. See the user guide: Debugger > Functions > Backward execution (rewind)
The verb is Step Back or Rewind to frame N, and the reason is read from the
machine after the refusal (rewind_refusal_reason() in
debugger_control.cpp). The prefix is what the Qt debugger matches to put the
reason in its status bar. RefusedCorrupt logs no reason line: the corruption
incident is reported on its own.
Symbols — CAP-SYM¶
There is one symbol table, and the backend owns it; the Qt panels, the DSL's @name, the GDB
server's monitor sym and --map all read it, so a symbol loaded by one client shows in every other.
| Verb | Does |
|---|---|
load_map(path, format, why) |
loads a symbol file into the table: MapFormat::Z88dk, Simple (SYMBOL = $ADDR), NextBuild (Memory.txt), SjasmplusEqu (--sym / --exp), or Z88dkOrSjasmplus (--map: the first line decides, one read, a pipe works). Returns the symbol count, or RefusedUnavailable (value 0) with the table and its owner untouched when refused; why says why a file was refused; for a file that cannot be opened it stays empty, except NextBuild, which says "could not open Memory.txt file" (7.3) |
clear_symbols() |
forgets them all |
lookup(addr) |
the name of addr in the memory mapped there now (source_page(addr)); a label is paged or unpaged |
lookup_name(name) |
the address of a name; a name-only value (a size, a section bound) resolves here and never names an address |
symbols() |
the table itself, for disasm_text::apply_symbols() and for SymbolTable::resolve(), which turns an address field's text into an address (hex first, else a symbol; @name is always the symbol) |
How the table is keyed, bound and merged, and what each format puts in it, is 7.4 ("The symbol table the formats fill"); the z88dk placement rules and C names are 7.4.3; the three other loaders are 7.4.1, 7.4.2 and 7.4.5.
User labels and comments — CAP-ANN¶
A second layer beside the toolchain's symbols: the user's labels and comments, keyed by memory and
address. symbols(), lookup() and lookup_name() return the composition of the two layers, the user's first,
so no consumer merges them. The store has no lifetime: loads, resets, state loads and rewinds leave it alone, and
it is not in .jns.
| Verb | Does |
|---|---|
set_user_label(space, addr, name, why) |
names addr in space (a rename when the key has a label); Unsupported with why for the Cpu space, a memory the file cannot name, a bad name, a name in use, or a name the loaded symbols give another address or memory |
remove_user_label(space, addr) |
removes the user's label; RefusedUnavailable when there is none (the toolchain's is untouched) |
set_user_comment(space, addr, text, why) |
sets the comment; empty text removes it; CRLF and CR become LF; Unsupported for the Cpu space, a memory the file cannot name, or text that is not UTF-8 |
annotations(), annotations_revision() |
the store, and a counter that rises with each successful change |
load_annotations(path, why), save_annotations(path, why) |
the .jdp file: a load replaces the whole store or leaves it as it was (Unsupported, line N: ...; RefusedUnavailable for a file that cannot be read); a save writes PATH.part and renames it, and is RefusedUnavailable when it cannot write |
replace_annotations(a) |
replaces the whole store (what an import builds offline) |
toolchain_symbols() |
the loaded layer alone (the Load MAP messages count this) |
The verbs touch no machine state (allowed while running) and each success bumps state_revision(). The file,
the composed reader, where it is recomposed and why the attachment cache holds the toolchain layer only are
7.4.6.
Source maps — CAP-SRC¶
The backend also owns one SourceMap (src/debug/source_map.*), keyed by the FetchSource that
supplies the code, filled by a format adapter and replaced only by a complete, successful load.
| Verb | Does |
|---|---|
load_source_map(path, accept_identity_mismatch, verify_at_first_execution, dot_only) |
the hand load: an sjasmplus SLD or a z88dk -debug map, told by content. Refused with the store untouched on a parse error, a mismatch (unless accepted), or (with dot_only) a map that is not a dot command's. Returns a SourceMapLoad (count, labels, listings, identity, identity_note, listing_note, pending, not_dot, error) |
clear_source_map() |
forgets the user's map |
source_map() |
the store: lookup(), lookup_all(), resolve("file:line"), file_ranges(), embedded_text(); empty while the user's map is stale |
source_map_status() |
None, Attached or Stale, with the file, the reason, and the pending and dot flags |
source_location(addr), source_location() |
the primary position of addr (or of the PC) as it is mapped NOW: a pointer into the store, valid until the next load or clear, so copy what you keep |
source_page(addr) |
what an instruction fetch at addr would be supplied from now (Emulator::predict_fetch()); the identity every lookup uses |
effective_page(addr) |
the physical 8K page the MMU maps behind a logical address |
load_program_sidecars(path) |
attaches <stem>.Memory.txt, <stem>.sld and <stem>.map of a program just loaded; returns a SidecarLoad (symbols, sources, symbols_unreadable, sources_pending, problem) |
subscribe_source_line(by, "file:line", like) |
a source breakpoint: a group of Execute subscriptions, one per start of the line, each with the space it was compiled for (CAP-EVT, above) |
run_to_source_line(by, "file:line") |
resume until any start of the line is reached (every start is armed; the first one reached stops the machine) (CAP-CTL, above) |
source_step(by, kind) |
steps by statement: Into, Over, Out, Back, ReverseContinue; limits SOURCE_STEP_LIMIT and SOURCE_STEP_FRAME_LIMIT |
executed_source_lines(file) |
the lines of a file any of whose ranges the guest executed (the coverage section, below) |
Where the mechanism lives, in chapter 7:
| Subject | Page |
|---|---|
What source_page() predicts, what the machine records (Mmu::last_m1(), TraceEntry::fetch_space(), CallFrame), EventFilter::space and the legacy page |
7.1 |
Ranges, the primary, lookup_all(), resolve(), aliases, embedded texts, the program identity and verify_program() |
7.2 |
The adapter contract, content dispatch, --source-map, the sidecar search and precedence, ownership, SidecarLoad::problem |
7.3 |
| Each file format and its producer | 7.4 |
subscribe_source_line(), run_to_source_line(), source_step(), Back and Reverse Continue, the call stack's frame rule |
7.5 |
| The load generation, resets, snapshots, rewind, tapes, dot commands | 7.6 |
| Which frontend reads what, the Source tab | 7.7 |
Coverage and the trace¶
INS-20 coverage is recorded in step_one_instruction(), the one body that
run_frame(), the debugger's Step and execute_single_instruction() share,
and only for a slot that fetched the opcode at PC. An NMI or INT acknowledge
does not run it. Switched off it costs one pointer test per instruction.
INS-21, memory coverage by execute, read and write (CAP-INS-21) is
the physical-memory counterpart, with a separate switch: INS-20 holds 16-bit
logical PCs (ZRCP cpu-code-coverage returns them, and the logical address a page
ran at cannot be rebuilt from a physical byte), INS-21 holds one byte of flags
(dbg::COVERAGE_EXEC | COVERAGE_READ | COVERAGE_WRITE) per byte of every
MemSpace, in lazily allocated 8 KB chunks (dbg::MemCoverage,
debug/mem_coverage.h; a 16 KB store, Rom, AltRom or Config, is two chunks).
Mmu writes it from the one arbiter (see 3.2): X is the first
opcode byte of an executed instruction (read_m1(); a prefix's inner M1 is no
mark), R every other read including the DMA's, W a write that landed, each at the
space and offset the route names and only for guest accesses (inside
GuestExecutionScope, not InspectionScope: exactly what a watchpoint can see, so
peek/poke, the trace's captures and loaders mark nothing). The set and its
switch are backend state, published to DebugState::mem_coverage_sink() by
Impl::publish_mem_coverage() and retired by ~Debugger() like INS-20's, and
re-published at the one re-application, so a rewind, reset, state load or cold boot
keeps the bits; a rewind's replay sets the same bits again, so nothing is counted
twice. Off clears; mem_coverage_clear() empties and recording goes on.
mem_coverage(space, addr, n, out) addresses like peek() (Cpu through the live
data route, any other space by offset in its store);
mem_coverage_fetch(addr) is the byte an M1 at addr would fetch now
(source_page(addr) plus space_offset()), which is what the Disassembly paints;
mem_coverage_spaces() lists the marked spaces sorted by (kind, index), for an
exporter to walk. executed_source_lines(file) is the Source tab's: the lines of a
file any of whose ranges (SourceMap::file_ranges()) holds an X byte. The
front-ends add --coverage (EmulatorConfig::coverage, applied by the three loop
owners through apply_startup_coverage()) and the Qt window's Debug ▸ Coverage.
There is no DZRP, ZRCP, GDB or DSL verb.
Each trace entry carries the registers (including I, R, IM, IFF1 and IFF2), the
word at SP read with peek(), the eight MMU pages and rom_slots, a bit per
slot that held ROM, and the identity of the memory its instruction was fetched
from (fetch_kind, fetch_index). trace_export() writes all of them; the ROM
mask is the ROM= column after the pages and the identity the FETCH=<tag>
column after it (space_tag() of fetch_space(), never the GUI's address style).
Reverse-engineering exports — CAP-REX¶
export_skoolkit(dir, report, why) and export_ghidra(dir, report, why) write the
labels the disassembly shows, and for skoolkit the mem_coverage() code/data map, into a
directory, as skoolkit control files and Ghidra symbol files. They are reads (peek(), coverage
and the symbol table are side-effect free): no machine state changes, and they are allowed while
the machine runs. ReReport carries the counts and notes. The formats, the writers, the directory
rules and the foreign-reader tests are in 7.4.7.
Session — CAP-SES¶
attach, detach, set_listener, add_service, remove_service, pump,
stop_policy, set_stop_policy, set_live_raster, live_raster, attached,
log, set_loop_driver, on_cold_boot_begin and on_cold_boot_done.
3.9.3 describes them.
Behind the facade¶
Debugger holds one unique_ptr<Impl> and nothing else, so adding state never
edits a header five frontends compile against. Impl is defined in the internal
src/debug/debugger_impl.h, which is also where core/emulator.h is included.
No path through run_frame() calls a Debugger method: the hook sites read the
internals directly, through the pointers the constructor publishes into
DebugState.
| File | Holds |
|---|---|
debugger.cpp |
construction and destruction, the MUTATE log, symbols, state(), the save and rewind-buffer verbs |
debugger_control.cpp |
CAP-CTL: pause, run, the steps, the run-to family, rewind, soft reset |
debugger_inspect.cpp |
CAP-INS reads and writes, the trace and coverage verbs |
debugger_input.cpp |
input injection |
debugger_events.cpp |
CAP-EVT, the pre-instruction Execute gate and the boundary drain |
debugger_session.cpp |
clients, listeners, services, pump(), the stop policy |
debugger_reconstruct.cpp |
the loop driver, reset(Hard), load(), the cold-boot notifications and the reconstruct contract |
debugger_capture.cpp |
screenshots, bookmarks, save_snapshot |
debugger_source.cpp |
CAP-SRC: the source map, program sidecars, source_step() |
debugger_reinterop.cpp |
CAP-REX: export_skoolkit() and export_ghidra(); the writers are skoolkit_ctl.* and ghidra_symbols.* (7.4.7) |
debugger_render.cpp |
render_layer() |
event_table.* |
EventTable: subscriptions, cheap filters, the latch ring, the delivery history |
inspect.cpp, result.cpp |
the free functions of inspect.h (key_name_to_matrix(), rrrgggbb_to_argb(), rgb333_to_argb()) and result_name() |
debugger_backend_test is the backend's suite. It is headless and Qt-free, so
it runs in both test configurations. Its row families follow the capability
ids: CTL-*, INS-*, EVT-*, SES-*, CAP-*, ST-*, HOST-*, LIFE-*.
Each control verb has a wiring row (arm it through the facade, run, assert the
machine stopped where the verb promises) and a control row, where the same
program runs straight past.