3.10.7 Extending the debug subsystem¶
Three kinds of change come up: a new frontend, a new verb on the facade, and a new event kind. Each has a fixed path through the tree, and each path ends in tests that a reviewer will look for.
Adding a frontend¶
1. Decide its shape. An in-process frontend (like the Qt debugger or the
script engine) holds the Debugger& and calls it from its own code on the
emulation thread. A socket frontend implements remote::Protocol and lets a
remote::Server own the listener (3.10.1). Put
it in its own directory — src/remote/<proto>/ for a protocol server — with
no toolkit dependency unless it is a GUI, so it builds in every configuration.
2. Become a client. attach() with a ClientInfo whose name is what the
log and the client lists will show. ClientKind is a closed enum in the frozen
debugger.h. The backend does not branch on it — it only logs it
(ATTACH client N "<name>" kind=K) — but a new frontend gets its own
enumerator, which is a change to the frozen header (see below). Decide whether
the client arms the machine. Most do; a client that must exist without arming
anything (as the Qt GUI's breakpoint owner does) attaches with
ClientInfo::observer.
3. Implement a Listener. All seven methods are pure virtual, so the
compiler lists them. A listener records and returns. A socket server builds
its packets later, in Protocol::on_notify(), which the transport calls after
the pump's drain; the Qt adapter acts on its own tick.
4. Map the protocol onto verbs, never onto the machine. Read the Result
of every verb and turn it into the protocol's error. Use the facade's own
answers instead of computing them: SlotInfo::space and rom_select() rather
than a page number turned into a MemSpace, probe_execute() rather than your
own breakpoint table, transient subscriptions rather than a hand-made step-off.
Keep the protocol's model — slot tables, sequence numbers, bank bytes, register
packing — in your directory.
5. Detach on every way out. A session end, a protocol close, a dropped
socket: all of them call detach(), which removes everything the client owns
and releases or hands on its pause. A frontend that can crash and leave a
subscription behind is a frontend that can leave the machine hung.
6. Wire it into the loop owners. For a socket server: a --<proto>-port
option in the src/core/cli_options.h table and in doc/man/jnext.1.md
(make cli-check diffs the two both ways), a port in EmulatorConfig, and a
block in DebugServers::start() that opens the server and calls
add_service(). The three loop owners already call DebugServers::start(), so
nothing else in src/platform/ or src/gui/ changes. --debug-listen-address
is refused without a server port; add yours to that check in main.cpp.
7. Tests.
- A unit suite under
test/remote/(or beside the frontend) that runs the productionServeroverFakeListener/FakeTransport/FakePeer, on a realEmulatorandDebugger, driven throughpump(). Assert the bytes on the wire and the machine state. Register it in the testCMakeLists.txt, declare it with its exact row count intest/unit-tests.confunder# gate: noneso it runs in bothmake unit-testandmake unit-test-sdl, and give every row a literal, globally unique ID (4.2). - Regression rows under
test/00regression/scripts/, declared infunctional_tests.conf, that start the real binary with the port set to 0, read the bound port from thelistening onlog line, and drive it with an independently written client — ideally the protocol's own client, otherwise a small peer script intest/00regression/. Cover all three frontends (CLI headless, SDL, Qt); see thedzrp-*-func,gdb-*-funcandzrcp-*-funcrows.
8. Document it. A page in this section, a row in the capability matrix of 3.10, and a user-guide page under chapter 6.
Adding a verb to the facade¶
The four published headers are frozen. A new declaration is made only by owner
decision, and recorded in three places: at the declaration itself, in
doc/design/debug-subsystem/b0-cap-traceability.md under its capability id, and
in doc/design/debug-subsystem/backend.md. on_cold_boot_begin(),
flush_captures() and rom_select() followed that path. rgb333_to_argb() in
inspect.h did not: it is recorded at its declaration and in backend.md, but
the traceability table names only its sibling rrrgggbb_to_argb(). The rules
the verb has to follow:
- Return type.
Resultif it acts,Expected<T>if it acts and yields data, a plain value only if it is a query that cannot refuse — and then it goes ondebugger.h's "DIRECT-VALUE QUERIES" list, with the bucket count updated. - Attribution.
ClientId byfirst if it transitions or mutates anything. - Deliveries. If it executes, rewinds, restores, resets or replaces the
machine, or changes its run state, it starts with
impl_->refuse_inside_delivery("<verb>"). - Mutation. If it writes, it emits a
MUTATEline throughlog_mutate()orlog_mutate_range(), refuses withRefusedRzxwhile an RZX records or plays, and runs its write underDebugState::InspectionScopeso it fires no event. If it changes guest state a replay would not reproduce, it also callsimpl_->note_mutation()beside itsMUTATEline, so a rewind cannot replay across the change (3.9.2). A host-side setting, such as the audio mute mask, does neither. - State. New state goes into
Debugger::Impl, never into the header. If it must survive a hard reset and it lives on theEmulator, record the request onImpland re-apply it inreapply_after_machine_rebuild(). - Includes. A published header may not reach
Emulatoror the other forbidden headers:test/lint-debug-headers.sh(regression preflight row 5) fails the run if it does. A new enum that mirrors an internal one gets a value-by-valuestatic_assertindebug_types_check.cpp.
Its tests are debugger_backend_test rows under the capability id: a wiring row
that proves the verb does what it promises, and a control row that proves the
same program does something else without it.
Adding an event kind¶
The backend comes first; the frontends follow.
1. src/debug/events.h. Add the enumerator to EventKind immediately
before Count, so the count and every mask derived from it move.
EventKindMask is 32 bits wide. Add the EventFilter fields its cheap filter
needs and the Event payload fields its subscribers need; Event is one flat,
trivially copyable struct, so a payload field is a plain member. Document at the
enumerator where the kind is latched and when it is delivered, including
whether it is late.
2. src/debug/debug_types_check.cpp. Update the assertions that pin the last
kind's bit and the count.
3. src/debug/event_table.*. Add the kind's arm to
EventTable::filter_matches(). If the latch needs fields LatchEntry does not
have, add them there.
4. The latch site, in the core. Gate the site so a run with no subscriber pays nothing:
- a site that fires at most a few times a frame tests the table's kind bit
(
has_kind()), as theFrame,IntAck,Nmi,ResetandMagicsites do; - a hot site carries a plain bool on its own subsystem, set by
Debugger::Impl::gates_changed(), as the Copper and DMA sites do — one bool per sub-kind if it has sub-kinds.
Then fill a LatchEntry and call DebugState::latch_event(). The stamper the
Emulator installs fills the common header (cycle, frame, vc, hc, PC), and
the ring takes care of overflow. A site in the device cluster
(tick_devices_after_instruction()) is delivered one instruction late; say so.
5. src/debug/debugger_events.cpp. Add the kind's arms to build_event()
(latch entry to Event) and log_line_for() (its Log action's line). Decide
what a Stop on it reports in note_event_stop(): the default is
PauseReason::Kind::Script.
jnext_debug and jnext_script are built with -Werror=switch, so a switch
without a default that misses the new kind fails the build. Not every switch
is like that: the Qt BreakpointModel's kind-name switch has a default, and
src/remote/ is not built with the flag, so grep for EventKind::Dma and visit
every hit.
6. Tests. debugger_backend_test gets an EVT-<KIND>-* family: the filter
on both sides of every bound, the delivery point (which boundary, and the
payload's PC), once, a Stop and its pause_reason, and a control row with
no subscriber. If the site is hot, add it to the make bench-hotlatch A/B or
measure it the same way. Update the suite's count in test/unit-tests.conf.
7. The frontends. The script language is the frontend that reaches every
kind; its steps — the keyword, the EventType, the parser, the payload table,
the engine's subscription and the SCRIPT-EV rows — are in
3.10.6. The Qt
breakpoint list needs a name for the kind in breakpoint_model.cpp, and the
GDB server's monitor listing one in rsp_server.cpp.
Adding a source format¶
A new assembler, compiler or linker output that should feed the symbol table or the source map is a producer, not a verb or an event: it is an adapter behind the loaders. The contract, the checklist (ground truth first, the extent and space rules before code, qualified ranges only, look before opening, dispatch by content, the sidecar search, identity, rows, docs) and the fixtures are in 7.8; the page each format gets is laid out in 7.4.