nx_gdbstub.nx
buildroot/runtime/nx_gdbstub.nx
about
nx_gdbstub.nx -- GDB REMOTE SERIAL PROTOCOL stub over the sovereign RV64 emulator (rv64im_min_sim).
THE RUNG: /compare/nishios NO1 "GDB remote-serial stub", watch symbol gdbstub_serve (ver 0.1 N0,
ranked #1 2026-08-23). Its accept rule is an EXTERNAL ORACLE: a STOCK gdb attaches to a guest
running on our emulator, breaks at an address our own decoder resolved, steps, reads a register
whose value an independent sim run derived, and detaches leaving the guest running -- plus the
negative control that a breakpoint at a never-executed address does NOT stop. That oracle lives
in nx_gdbstub_gate (gdb-multiarch in batch mode); this file is the stub it attaches to.
WHAT IT SPEAKS (GDB Remote Serial Protocol, the subset a stock gdb needs for a bare-metal target):
framing $<payload>#<2-hex checksum>, '+'/'-' acks until QStartNoAckMode, 0x03 = interrupt
qSupported PacketSize + QStartNoAckMode + qXfer:features:read (we SERVE target.xml, so gdb
never has to guess the register file: riscv:rv64, x0..x31 + pc, 64-bit)
? g G p P m M Z0 z0 s c D k vKill H T qC qAttached qfThreadInfo qsThreadInfo
stop replies: S05 (trap: breakpoint or single-step), S02 (interrupted by 0x03),
W<code> (guest halted through the finisher / exit ecall), X04 (illegal instruction)
everything else answers the empty packet, which is the protocol's "unsupported" and makes gdb
fall back (X -> M, vCont -> s/c) rather than fail.
WHAT IT DELIBERATELY DOES NOT DO:
- memory packets (m/M) touch RAM ONLY (mem_base..mem_base+mem_size): a debugger read of device
MMIO would run the device model's side effects (a UART read clears state), and a debugger must
not change what it observes. Device windows answer E14. Addresses are translated through the
sim's own nx_rv64im_xlate, so in S/U-mode under Sv39 gdb sees the guest's virtual space, same as
the guest does; a walk fault answers E14 and the fault flag is cleared, never left for the guest.
(Documented imprecision: the sim's xlate sets the PTE A bit on a successful walk, so an S/U-mode
'm' read can set A on a page the guest had not yet touched.)
- no RLE in replies, no binary X writes, no threads beyond the one hart (qC/H answer thread 1).
SIZES ARE DERIVED, NOT PICKED:
GDB_G_HEX = (NX_RV64IM_RF_N_REGS + 1 pc) * (NX_RV64IM_RF_WIDTH / 4) hex chars = the 'g' reply
GDB_PACKETSIZE (advertised) = GDB_G_HEX: gdb bounds every m/M/qXfer chunk by this, so a reply
never exceeds the 'g' reply and a request payload never exceeds it either.
GDB_BUF = 2*GDB_PACKETSIZE + framing: payload bound + command prefix (shorter than a payload
by construction) + '$' '#' and two checksum chars.
breakpoints = a BITMAP over RAM, one bit per halfword (the RVC granularity), sized from
sim.mem_size -- no breakpoint cap to guess, O(1) test in the hot step loop.
GDB_POLL_STEPS = the step count between interrupt polls while continuing, = one millisecond at
the 20 MIPS interpreted rate nishios.plan publishes, so Ctrl-C latency is bounded
to ~1 ms without a syscall per step.
dependencies 3 imports · 0 importers
imports: nx_syscalls.nxnx_http_server.nxnx_bootcap.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| 190 | struct NxGdbStub |
consts
| 58 | const GDB_CH_DOLLAR: i64 = 36 // '$' frame start |
| 59 | const GDB_CH_HASH: i64 = 35 // '#' frame end |
| 60 | const GDB_CH_PLUS: i64 = 43 // '+' ack |
| 61 | const GDB_CH_MINUS: i64 = 45 // '-' nak |
| 62 | const GDB_CH_INT: i64 = 3 // 0x03 interrupt (gdb Ctrl-C) |
| 63 | const GDB_CH_COMMA: i64 = 44 |
| 64 | const GDB_CH_COLON: i64 = 58 |
| 65 | const GDB_CH_SEMI: i64 = 59 |
| 66 | const GDB_CH_EQ: i64 = 61 |
| 69 | const GDB_HEX_CHARS_PER_BYTE: i64 = 2 |
| 70 | const GDB_BITS_PER_BYTE: i64 = 8 |
| 71 | const GDB_HEX_PER_REG: i64 = NX_RV64IM_RF_WIDTH / GDB_BITS_PER_BYTE * GDB_HEX_CHARS_PER_BYTE // 64-bit reg -> 16 hex chars |
| 72 | const GDB_PC_REGNUM: i64 = NX_RV64IM_RF_N_REGS // gdb's riscv numbering: x0..x31 then pc |
| 73 | const GDB_N_REGS: i64 = NX_RV64IM_RF_N_REGS + 1 // the 'g' packet: every GPR + pc |
| 74 | const GDB_G_HEX: i64 = GDB_N_REGS * GDB_HEX_PER_REG // the 'g' reply length |
| 75 | const GDB_PACKETSIZE: i64 = GDB_G_HEX // advertised bound on every payload |
| 76 | const GDB_FRAMING: i64 = 4 // '$' + '#' + 2 checksum chars |
| 77 | const GDB_BUF: i64 = GDB_PACKETSIZE * 2 + GDB_FRAMING // payload + command prefix + framing |
| 78 | const GDB_MIPS_PUBLISHED: i64 = 20 // interpreted rate, nishios.plan pos| (measured) |
| 79 | const GDB_STEPS_PER_MS_AT_1MIPS: i64 = 1000 |
| 80 | const GDB_POLL_STEPS: i64 = GDB_MIPS_PUBLISHED * GDB_STEPS_PER_MS_AT_1MIPS // = 1 ms of guest time between interrupt polls |
| 81 | const GDB_LISTEN_BACKLOG: i64 = 1 // one debugger at a time, by design |
| 82 | const GDB_BYTE_MASK: i64 = 255 |
| 83 | const GDB_SOCKADDR_LEN: i64 = 16 |
| 84 | const GDB_SYS_GETSOCKNAME_RV64: i64 = 204 // rv64 number; x86ctx translates 204 -> 51 |
| 87 | const GDB_EXIT_OK: i64 = 0 |
| 88 | const GDB_EXIT_USAGE: i64 = 2 |
| 89 | const GDB_EXIT_NOIMAGE: i64 = 3 |
| 90 | const GDB_EXIT_LISTEN: i64 = 4 |
| 91 | const GDB_EXIT_KILLED: i64 = 5 |
| 215 | const GDB_STUB_BYTES: i64 = 256 // 24 fields * 8 = 192; room for 8 more -- sized like NX_RV64IM_SIM_BYTES |
functions
| 94 | func gs_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 95 | func gs_puts(s: *u8) -> i64 { sys_write(1, s, gs_len(s)); return 0 } |
| 96 | func gs_putn(v: i64) -> i64 |
| 114 | func gs_hexnib(n: i64) -> i64 { if n < 10 { return 48 + n } return 87 + n } // '0'..'9', 'a'..'f' |
| 115 | func gs_hexval(c: i64) -> i64 |
| 122 | func gs_put_le64(b: *u8, o: i64, v: i64) -> i64 |
| 135 | func gs_get_le64(b: *u8, o: i64, err: *i64) -> i64 |
| 151 | func gs_parse_hex(b: *u8, o: i64, n: i64, endp: *i64, err: *i64) -> i64 |
| 168 | func gs_cat(d: *u8, o: i64, s: *u8) -> i64 |
| 174 | func gs_starts(b: *u8, n: i64, s: *u8) -> i64 |
| 183 | func gs_eq(b: *u8, n: i64, s: *u8) -> i64 |
| 218 | func gs_xcat(d: *u8, o: i64, measure_only: i64, s: *u8) -> i64 |
| 222 | func gs_xcatn(d: *u8, o: i64, measure_only: i64, v: i64) -> i64 |
| 237 | func gs_abi_name(i: i64) -> *u8 called by 1: gs_xml_emit |
| 274 | func gs_xml_emit(x: *u8, measure_only: i64) -> i64 |
| 295 | func gs_build_xml(st: *NxGdbStub) -> i64 |
| 304 | func gs_new(sim: *NxRv64imSim) -> *NxGdbStub |
| 333 | func gs_bp_ok(st: *NxGdbStub, addr: i64) -> i64 |
| 338 | func gs_bp_test(st: *NxGdbStub, addr: i64) -> i64 |
| 346 | func gs_bp_set(st: *NxGdbStub, addr: i64, on: i64) -> i64 |
| 363 | func gs_fill(st: *NxGdbStub) -> i64 |
| 370 | func gs_consume(st: *NxGdbStub, n: i64) -> i64 |
| 379 | func gs_send(st: *NxGdbStub, payload: *u8, n: i64) -> i64 called by 8: gs_send_strgs_send_emptygs_reply_haltgs_do_ggs_do_pgs_do_m+2 calls 5: gs_hexnibsys_writegs_fillgs_consumegs_puts |
| 420 | func gs_send_str(st: *NxGdbStub, s: *u8) -> i64 { return gs_send(st, s, gs_len(s)) } |
| 421 | func gs_send_empty(st: *NxGdbStub) -> i64 { return gs_send(st, st.tx, 0) } |
| 426 | func gs_recv(st: *NxGdbStub, outlen: *i64) -> i64 |
| 484 | func gs_reply_halt(st: *NxGdbStub) -> i64 |
| 496 | func gs_reply_trap(st: *NxGdbStub) -> i64 { return gs_send_str(st, "S05" as *u8) } |
| 497 | func gs_reply_int(st: *NxGdbStub) -> i64 { return gs_send_str(st, "S02" as *u8) } |
| 500 | func gs_poll_int(st: *NxGdbStub) -> i64 |
| 534 | func gs_resume(st: *NxGdbStub, single_step: i64) -> i64 |
| 558 | func gs_ram_off(st: *NxGdbStub, vaddr: i64) -> i64 |
| 569 | func gs_do_g(st: *NxGdbStub) -> i64 |
| 577 | func gs_do_G(st: *NxGdbStub, n: i64) -> i64 |
| 593 | func gs_do_p(st: *NxGdbStub, n: i64) -> i64 |
| 606 | func gs_do_P(st: *NxGdbStub, n: i64) -> i64 |
| 620 | func gs_do_m(st: *NxGdbStub, n: i64) -> i64 |
| 644 | func gs_do_M(st: *NxGdbStub, n: i64) -> i64 |
| 676 | func gs_do_Z(st: *NxGdbStub, n: i64, on: i64) -> i64 |
| 687 | func gs_do_qSupported(st: *NxGdbStub) -> i64 |
| 700 | func gs_do_qXfer(st: *NxGdbStub, n: i64) -> i64 |
| 724 | func gs_session(st: *NxGdbStub) -> i64 |
| 773 | func gdbstub_serve(sim: *NxRv64imSim, port: i64, readyfile: *u8) -> i64 called by 1: main calls 12: gs_newsys_mmapnx_http_server_addr_loopbacknx_http_server_listengs_putsgs_putn+6 |
| 827 | func main(argc: i64, argv: *i64) -> i64 |