Introduction
RISC OS Pyromaniac includes a GDB Remote Serial Protocol (RSP) stub, so a
real gdb (or arm-none-eabi-gdb/gdb-multiarch) can attach to a running
Pyromaniac instance and drive it with standard GDB commands - break,
continue, step, info registers, x, watch, and so on - instead of
(or alongside) the sysrq-based tools (docs/PYRODEBUG.md, docs/PYROMCP.md).
The stub is a second, independent client of the same sysrq substrate those
other tools use: everything it does is translated to and from ro.sysrq(...)
calls (regs-list/regs-set, memory-read-bytes/memory-write-bytes,
debug-resume/debug-step, watchpoints-add/tracepoints-add, ...) rather
than talking to the emulator directly. See docs/FEATURES.md's GDBStub
section for the feature summary, and PLANGDB.md for the staged design this
was built from, including notes on what could and couldn't be verified
against a real gdb client while it was being built.
RSP is a documented, open wire protocol, not GDB's own source code, so implementing a server that speaks it carries no GPL obligation.
Requirements
- Any
gdbbuild that understands ARM:gdb,arm-none-eabi-gdb, orgdb-multiarch(withset architecture armif it supports more than one target). - A Pyromaniac instance with the stub enabled (see below). No special build of Pyromaniac itself is needed.
Starting a session
There are two ways to get a Pyromaniac instance for GDB to talk to, built from independent existing configuration options rather than a single stub-specific mode:
Attach to an already-running instance
Start Pyromaniac with the stub enabled and something to run - a persistent
instance (pyro_server.py) is usually more convenient than a one-shot
pyro.py invocation, since the process needs to still be there when gdb
connects:
$ scripts/pyro-server --config gdbstub.enable=true
Then, from another terminal:
$ gdb
(gdb) target remote :1234
You can debug-pause (via docs/PYRODEBUG.md's CLI, the raw sysrq socket,
or MCP) before or after connecting - RSP assumes the target is already
halted, so GDB's first ? query will report "no signal" until something has
actually paused execution.
Launch already halted at boot
To have RISC OS start already halted - before any RISC OS code, including
resident module initialisation, has run - add emulation.pause_at_boot=true:
$ scripts/pyro-server --config gdbstub.enable=true --config emulation.pause_at_boot=true
The stub's socket is already listening by the time the halt takes effect (resources are provisioned before that point is reached), so you can connect and set breakpoints before the first instruction executes:
$ gdb
(gdb) target remote :1234
(gdb) break *0x8000
(gdb) continue
Configuration options
| Option | Default | Meaning |
|---|---|---|
gdbstub.enable |
false |
Start the GDB RSP stub. |
gdbstub.port |
1234 |
TCP port the stub listens on (GDB's own conventional default). |
emulation.pause_at_boot |
false |
Start already halted, before any RISC OS code runs - equivalent to an immediate debug-pause issued before boot. |
Any of these can also go in a --config-file instead of individual
--config switches - see docs/CONFIGURATION.md.
What the stub implements
g/G- the whole ARM register file, viaregs-list/regs-set. GDB is told nothing about the target's registers beyond the classic 168-byte layout it assumes by default (16 GPRs, eight legacy FPA registers and an FPA status word - zero-filled, since Pyromaniac has no FPA unit - thencpsr), so an unmodifiedgdbworks without this stub also implementingqXfer:features:read.m/M- memory read/write, viamemory-read-bytes/memory-write-bytes. A failed or wrong-length read/write is reported as a GDB error (Exx), never silently truncated data.c/s- continue/step, viadebug-resume/debug-step. Unlike every other command, these only reply once execution actually stops again - a breakpoint/watchpoint firing, a step completing, or Ctrl-C (see below). Both accept an optional resume address (c8000/s8000, hex, no separator - GDB'sc [addr]/s [addr]forms), moving the PC there first viaregs-setbefore resuming/stepping.Z/z- insert/remove breakpoints (kind0/1- both map onto the same break-tagged tracepoint, since Pyromaniac has no distinct hardware breakpoint mechanism) and watchpoints (kind2/3/4- write/read/access).monitor <text>(qRcmd) - passes<text>through to the sysrq dispatch used by the raw sysrq socket, somonitor <any registered sysrq>works, including ones with no dedicated GDB verb (monitor debug-backtrace,monitor debug-trace-log).monitor where <address>andmonitor find <pattern>are aliases ontomemory-describe/memory-find-names- useful since RISC OS modules/AOF binaries have no conventional path into GDB's own symbol table, so plainbt/info symbolwould otherwise show raw addresses only.qRegisterInfo<hex-index>- an LLDB-only RSP extension (plaingdbnever sends it, so implementing this cannot changegdb's own behaviour) describing each of r0-r15/cpsr by name, size, byte offset into theg/Gblob above, and role (generic:sp/ra/pc/flags) - seepyromaniac/gdbstub/registers.py. Deliberately a standalone register description rather than reusingpyromaniac/registers.py'sRegisters.archregs, since that module importsunicorn, which the root-level gdbstub unit tests must not depend on even transitively. This is what letslldb(see "Using LLDB instead" below) show and modify registers without aqXfer:features:readtarget description.
Example session
$ scripts/pyro-server --config gdbstub.enable=true --config emulation.pause_at_boot=true
$ gdb
(gdb) set architecture arm
(gdb) target remote :1234
Remote debugging using :1234
(gdb) info registers
r0 0x0 0
...
pc 0x8000 0x8000
(gdb) x/4i $pc
(gdb) break *0x80a0
(gdb) continue
(gdb) stepi
(gdb) watch *(int *) 0x8030
(gdb) continue
^C
Program received signal SIGINT, Interrupt.
(gdb) monitor debug-backtrace
(gdb) monitor where 8030
(gdb) monitor watchpoints-list
(gdb) monitor debug-trace-log
(gdb) detach
Notes on the commands above:
break *0x80a0sets a breakpoint at a raw address - the usual form to use here, since RISC OS modules/AOF binaries have no symbol table GDB knows about (usemonitor where/monitor find, or the module's own*Help/map file, to find the address you want first).watch *(int *) 0x8030maps onto GDB's own write-watchpoint (Z2);rwatch/awatchmap onto read (Z3) and access (Z4) watchpoints the same way.- Ctrl-C (
^Cabove) sends RSP's out-of-band interrupt byte to stop a runawaycontinue- GDB reports it asSIGINT, distinct from theSIGTRAPa breakpoint/watchpoint/step reports, so it's clear which kind of stop just happened. monitor <sysrq-name>reaches the same sysrqs the raw socket does (watchpoints-list,tracepoints-list,switraps-list,debug-trace-log,debug-status, ...) - runmonitor helpfor the full list.
Using LLDB instead
LLDB's remote-debugging transport is the same GDB Remote Serial Protocol
gdb uses (with its own additional packets on top), not a competing wire
format, so lldb can attach to the same stub directly - there is nothing
LLDB-specific to enable on the Pyromaniac side:
$ lldb
(lldb) gdb-remote localhost:1234
Register access works because the stub answers LLDB's qRegisterInfo
queries (see above); memory (m/M), execution (c/s), and
breakpoints/watchpoints (Z/z) are all plain RSP and work the same as
they do for gdb. As with gdb, there is no symbol table, so set
breakpoints by raw address (breakpoint set --address 0x80a0 or
b *0x80a0) rather than by name.
Limitations
- No separate hardware breakpoint type. GDB's
Z0(software) andZ1(hardware) both map onto the same tracepoint-based mechanism - there is no distinct hardware breakpoint facility to expose. - No target description (
qXfer:features:read). The classic register layout is used instead; this is deliberate (seePLANGDB.md), but it means the FPA registers/status GDB shows are always zero rather than reflecting real hardware state (Pyromaniac has no FPA unit). LLDB gets real register names/roles a different way, throughqRegisterInfo(see above) rather than a target.xml. - No symbol table. Pyromaniac doesn't hand GDB any symbol information, so
break <function-name>,bt, andinfo symbolwon't resolve names - use raw addresses, ormonitor where/monitor findto look names up on the RISC OS side instead. The same applies tolldb. - Manual verification against a real
lldbis still outstanding - like the rest of this stub (seePLANGDB.md), there is nolldbbinary in the environment this was built in, soqRegisterInfosupport has only been checked at the unit-test level against the reply format documented at https://lldb.llvm.org/resources/lldbgdbremote.html, not against a livelldbsession. Other LLDB-only queries it might expect (qHostInfo,qProcessInfo, thread-related packets) are not implemented; add them if a real session shows they're needed.
Relationship to other clients
The GDB stub, scripts/pyro-debug/pyro_debug.py (docs/PYRODEBUG.md), and
the MCP server (docs/PYROMCP.md) are three independent clients of the same
underlying sysrq substrate, useful in different contexts: GDB (or LLDB, see
above) for register/memory/breakpoint work through a real debugger's UI, the
MCP server for AI agent tool integration, pyro_debug.py for a quick
interactive session from a terminal. Nothing one of them does affects
whether the others are available - gdbstub.enable, sysrqserver.enable,
and the MCP server's own transport can all be turned on at once against the
same instance.