code wiki / _hdl_build / nx_gdbstub_gate.nx
nx_gdbstub_gate.nx source
↩ module page · 490 lines · 26859 B
1// nx_gdbstub_gate.nx -- the VERDICT gate for NO1 (nx_gdbstub, watch symbol gdbstub_serve). NO mocks.
2//
3// THE ACCEPT RULE (nishios.plan NO1, verbatim): a STOCK gdb attaches to a running guest, breaks at an
4// address the disassembler independently resolved, steps, reads a register whose value a separate path
5// derived, and detaches leaving the guest running -- plus a NEG-CONTROL that a breakpoint at an address
6// never executed does NOT stop. The oracle is a third-party debugger, so this rung cannot be self-graded.
7//
8// HOW EACH EXPECTATION IS DERIVED INDEPENDENTLY OF THE STUB (the stub is never its own oracle):
9// bp address = the pc of the FIRST store-kind instruction the K-R0 boot image executes, found by
10// running a SECOND, stub-free sim in this process and DECODING each fetched word with
11// rv64im_min_decoder (nx_rv64im_decode_kind == NX_RV64IM_OP_STORE).
12// register = that store's rs2 (decoded) read from the reference sim's regfile at that pc.
13// next pc = bp + 4, after asserting the word is a 32-bit encoding ((w & 3) == 3) and a store never branches.
14// memory words = the first two little-endian words of the IMAGE FILE, not of any sim.
15// never-addr = the lowest 4-aligned RAM address at/after the image end that the reference run NEVER
16// fetched (a bitmap of every executed pc over the WHOLE boot, not a guess).
17// golden = <image>.gold, the transcript the boot rulers already certify.
18//
19// SESSIONS (each on a FRESH stub process, port 0 = kernel-chosen, read back from the stub's ready file):
20// A stock gdb: attach, break *bp, continue, p/x $xN, stepi, x/2xw mem_base, detach -> T1..T7
21// B stock gdb: break *never-addr, continue -> the guest must run to EXIT, never a breakpoint stop -> T8
22// C sovereign RSP client (no gdb): a frame with a WRONG checksum must be NAK'd; a good '?' must be
23// ACK'd and answered S05; 'D' must be answered OK; the stub must count the bad frame -> T9..T12
24// If no stock gdb is on this host, sessions A/B are a MISSING PRECONDITION (gv_need -> SKIP), never a
25// pass and never a RED: the oracle leg is reported UNWITNESSED, C still runs. (NAS: no gdb; laptop WSL:
26// gdb-multiarch 15.1 installed 2026-08-23 as a third-party ORACLE only.)
27//
28// Fixtures live in /tmp/nx_gdbstub_gate/ (created at setup; a crashed run cannot leave a fixture beside a
29// production beat). Every tooth prints the values it compared, not only PASS/FAIL.
30// license_tier: ORIGINAL (the debugger is an oracle; nothing of it ships)
31import "nx_syscalls.nx"
32import "nx_gate_verdict.nx"
33import "nx_gatekit_lib.nx"
34import "nx_http_server.nx"
35import "nx_bootcap.nx"
36import "rv64im_min_decoder.nx"
37
38const GG_DIR: *u8 = "/tmp/nx_gdbstub_gate"
39const GG_IMG: *u8 = "runtime/_hdl_build/_boot_nishi_virt.bin"
40const GG_IMG_ALT: *u8 = "_boot_nishi_virt.bin"
41const GG_OUT_CAP: i64 = 1048576 // gdb's batch output for a 1,164-step boot is a few KB; 1 MiB = the gatekit capture bound
42// A stub that has not written its ready file within this budget never listened (the measured start is
43// ~0.3 s: load a 2 KB image, listen, write one line). Announced when it fires; never silent.
44const GG_READY_MS: i64 = 5000
45const GG_READY_POLL_MS: i64 = 50
46// A gdb session over the K-R0 image (1,164 guest steps) completes in well under a second; a minute is a
47// hang, and a hung oracle must become a failed tooth, not a hung gate (the gatekit watchdog kills it).
48const GG_GDB_MS: i64 = 60000
49const GG_IO_MS: i64 = 5000 // session C: one reply should arrive within this; a stub that answers nothing fails the tooth
50const GG_RV_INSTR_BYTES: i64 = 4
51const GG_EXIT_CODE_RD: i64 = 0
52
53func gg_p(s: *u8) -> i64 { return gv_puts(s) }
54func gg_n(v: i64) -> i64 { return gv_num(v) }
55func gg_hex(v: i64) -> i64 {
56 let b: *u8 = sys_mmap(16)
57 var i: i64 = 0
58 while i < 16 {
59 let nib: i64 = (v >> ((15 - i) * 4)) & 15
60 if nib < 10 { b[i] = (48 + nib) as u8 } else { b[i] = (87 + nib) as u8 }
61 i = i + 1
62 }
63 sys_write(1, b, 16)
64 sys_munmap(b, 16)
65 return 0
66}
67// find needle in buf[from..n); returns index or -1
68func gg_find(buf: *u8, n: i64, from: i64, needle: *u8) -> i64 {
69 let nl: i64 = gk_len(needle)
70 if nl == 0 { return 0 - 1 }
71 var i: i64 = from
72 while i + nl <= n {
73 var j: i64 = 0
74 var ok: i64 = 1
75 while j < nl { if buf[i + j] != needle[j] { ok = 0; j = nl } j = j + 1 }
76 if ok == 1 { return i }
77 i = i + 1
78 }
79 return 0 - 1
80}
81func gg_hexval(c: i64) -> i64 {
82 if c >= 48 { if c <= 57 { return c - 48 } }
83 if c >= 97 { if c <= 102 { return c - 87 } }
84 if c >= 65 { if c <= 70 { return c - 55 } }
85 return 0 - 1
86}
87// parse hex digits at buf[i..]; returns the value, digits counted into ndig[0]
88func gg_parse_hex(buf: *u8, n: i64, i: i64, ndig: *i64) -> i64 {
89 var v: i64 = 0
90 var k: i64 = i
91 var d: i64 = 0
92 var go: i64 = 1
93 while go == 1 {
94 if k >= n { go = 0; continue }
95 let h: i64 = gg_hexval(buf[k] as i64)
96 if h < 0 { go = 0; continue }
97 v = (v << 4) | h
98 d = d + 1
99 k = k + 1
100 }
101 ndig[0] = d
102 return v
103}
104// value printed after the k-th occurrence (1-based) of `needle` followed by "0x"
105func gg_value_after(buf: *u8, n: i64, needle: *u8, k: i64, found: *i64) -> i64 {
106 var from: i64 = 0
107 var hits: i64 = 0
108 var at: i64 = 0 - 1
109 var go: i64 = 1
110 while go == 1 {
111 at = gg_find(buf, n, from, needle)
112 if at < 0 { go = 0; continue }
113 hits = hits + 1
114 if hits == k { go = 0; continue }
115 from = at + 1
116 }
117 if at < 0 { found[0] = 0; return 0 }
118 if hits < k { found[0] = 0; return 0 }
119 let px: i64 = gg_find(buf, n, at + gk_len(needle), "0x" as *u8)
120 if px < 0 { found[0] = 0; return 0 }
121 let nd: *i64 = sys_mmap(8) as *i64
122 let v: i64 = gg_parse_hex(buf, n, px + 2, nd)
123 if nd[0] == 0 { found[0] = 0; return 0 }
124 found[0] = 1
125 return v
126}
127
128// ---- resolve the subject (the deployed stub) and the oracle (a stock gdb): say which one was used
129func gg_resolve(out: *u8, a: *u8, b: *u8, c: *u8) -> i64 {
130 if gk_exists(a) == 1 { gk_cat(out, 0, a); out[gk_len(a)] = 0 as u8; return 1 }
131 if gk_exists(b) == 1 { gk_cat(out, 0, b); out[gk_len(b)] = 0 as u8; return 1 }
132 if gk_exists(c) == 1 { gk_cat(out, 0, c); out[gk_len(c)] = 0 as u8; return 1 }
133 out[0] = 0 as u8
134 return 0
135}
136
137// ---- spawn the stub in the background, stdout+stderr -> logpath. Returns pid.
138func gg_spawn(elf: *u8, a1: *u8, a2: *u8, a3: *u8, logpath: *u8) -> i64 {
139 let argv: *i64 = sys_mmap(64) as *i64
140 argv[0] = elf as i64
141 argv[1] = a1 as i64
142 argv[2] = a2 as i64
143 argv[3] = a3 as i64
144 argv[4] = 0
145 let envp: *i64 = sys_mmap(16) as *i64
146 envp[0] = 0
147 let pid: i64 = sys_fork()
148 if pid < 0 { return 0 - 1 }
149 if pid == 0 {
150 let lf: i64 = sys_openat_wr(logpath, 420)
151 if lf >= 0 { sys_dup3(lf, 1, 0); sys_dup3(lf, 2, 0) }
152 sys_execve(elf, argv, envp)
153 sys_exit(127)
154 }
155 return pid
156}
157// wait for the ready file and parse "port=N"; returns the port or -1
158func gg_wait_port(readyfile: *u8) -> i64 {
159 var waited: i64 = 0
160 let buf: *u8 = sys_mmap(256)
161 while waited < GG_READY_MS {
162 let n: i64 = gk_read(readyfile, buf, 255)
163 if n > 5 {
164 var v: i64 = 0
165 var i: i64 = 5
166 var digits: i64 = 0
167 while i < n { let c: i64 = buf[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); digits = digits + 1 } } i = i + 1 }
168 if digits > 0 { return v }
169 }
170 sys_sleep_ms(GG_READY_POLL_MS)
171 waited = waited + GG_READY_POLL_MS
172 }
173 return 0 - 1
174}
175// bounded wait for a child: returns its exit code, or -1 if it had to be killed (announced)
176func gg_reap(pid: i64, budget_ms: i64) -> i64 {
177 let st: *i64 = sys_mmap(16) as *i64
178 var waited: i64 = 0
179 while waited < budget_ms {
180 st[0] = 0
181 let r: i64 = sys_wait4(pid, st, WNOHANG)
182 if r == pid { return gk_wait_code(st[0]) }
183 sys_sleep_ms(GG_READY_POLL_MS)
184 waited = waited + GG_READY_POLL_MS
185 }
186 gg_p(" WATCHDOG: stub pid did not exit within budget -- killed (this is a failed tooth, not a pass)\n" as *u8)
187 nx_kill(pid, 9)
188 sys_wait4(pid, st, 0)
189 return 0 - 1
190}
191
192// ---- session C: a sovereign RSP client that sends raw frames and reads raw replies
193func gg_rsp_connect(port: i64) -> i64 {
194 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
195 if fd < 0 { return 0 - 1 }
196 let sa: *u8 = sys_mmap(16)
197 nx_http_server_addr_loopback(sa, port)
198 let rc: i64 = sys_connect(fd, sa, 16)
199 if rc < 0 { sys_close(fd); return 0 - 1 }
200 return fd
201}
202// read until `want` bytes are present or the budget expires; returns bytes read
203func gg_rsp_read(fd: i64, buf: *u8, cap: i64, want: i64) -> i64 {
204 var n: i64 = 0
205 var waited: i64 = 0
206 while n < want {
207 let pfd: *u8 = sys_mmap(8)
208 pfd[0] = (fd & 255) as u8; pfd[1] = ((fd >> 8) & 255) as u8; pfd[2] = ((fd >> 16) & 255) as u8; pfd[3] = ((fd >> 24) & 255) as u8
209 pfd[4] = 1 as u8; pfd[5] = 0 as u8; pfd[6] = 0 as u8; pfd[7] = 0 as u8
210 let r: i64 = sys_poll(pfd, 1, GG_IO_MS)
211 sys_munmap(pfd, 8)
212 if r <= 0 { return n }
213 let got: i64 = sys_read(fd, ((buf as i64) + n) as *u8, cap - n)
214 if got <= 0 { return n }
215 n = n + got
216 waited = waited + 1
217 }
218 return n
219}
220
221func main() -> i64 {
222 gv_head("nx_gdbstub gate -- a STOCK gdb over the sovereign rv64 emulator; expectations derived by a stub-free sim + the decoder + the image file; neg-controls for a never-executed breakpoint and a bad checksum" as *u8)
223 let ctr: *i64 = gv_ctr()
224 gk_mkdir(GG_DIR)
225
226 // ---- subject + oracle resolution (announce what was measured)
227 let elf: *u8 = sys_mmap(512)
228 let have_elf: i64 = gg_resolve(elf, "_offc/nx_gdbstub.elf" as *u8, "./nx_gdbstub.elf" as *u8, "_build/nx_gdbstub.sov.elf" as *u8)
229 gg_p(" subject: " as *u8); if have_elf == 1 { gg_p(elf) } else { gg_p("ABSENT (looked in _offc/, ./, _build/)" as *u8) } gg_p("\n" as *u8)
230 let gdb: *u8 = sys_mmap(512)
231 let have_gdb: i64 = gg_resolve(gdb, "/usr/bin/gdb-multiarch" as *u8, "/usr/bin/gdb" as *u8, "/usr/local/bin/gdb-multiarch" as *u8)
232 gg_p(" oracle: " as *u8); if have_gdb == 1 { gg_p(gdb) } else { gg_p("ABSENT -- stock-gdb leg UNWITNESSED on this host" as *u8) } gg_p("\n" as *u8)
233 let lenp: *i64 = sys_mmap(16) as *i64
234 let pathp: *i64 = sys_mmap(16) as *i64
235 let img: *u8 = bootcap_load(GG_IMG, GG_IMG_ALT, lenp, pathp)
236 let ilen: i64 = lenp[0]
237 gg_p(" image: " as *u8); gg_p(pathp[0] as *u8); gg_p(" bytes=" as *u8); gg_n(ilen); gg_p("\n" as *u8)
238 // golden
239 let gpath: *u8 = sys_mmap(512)
240 var gi: i64 = gk_cat(gpath, 0, pathp[0] as *u8)
241 gi = gk_cat(gpath, gi, ".gold" as *u8)
242 gpath[gi] = 0 as u8
243 let glenp: *i64 = sys_mmap(16) as *i64
244 let gold: *u8 = sys_read_file(gpath, glenp)
245 let glen: i64 = glenp[0]
246 gv_need("deployed nx_gdbstub.elf (the subject)" as *u8, have_elf, ctr)
247 var have_img: i64 = 0
248 if ilen > 0 { have_img = 1 }
249 gv_need("K-R0 boot image _boot_nishi_virt.bin" as *u8, have_img, ctr)
250 var have_gold: i64 = 0
251 if glen > 0 { have_gold = 1 }
252 gv_need("golden transcript <image>.gold" as *u8, have_gold, ctr)
253 if have_elf == 0 { return gv_verdict("GDBSTUB-GATE" as *u8, ctr, "subject absent" as *u8) }
254 if have_img == 0 { return gv_verdict("GDBSTUB-GATE" as *u8, ctr, "image absent" as *u8) }
255
256 // ---- reference derivation: a SECOND sim, no stub, decoder-resolved breakpoint + executed-pc bitmap
257 let reftx: *u8 = sys_mmap(BOOTCAP_MEM_SIZE)
258 let ref: *NxRv64imSim = bootcap_machine(img, ilen, reftx, BOOTCAP_MEM_SIZE)
259 let exec_bits: *u8 = sys_mmap((ref.mem_size / 4 / 8) + 1) // one bit per 4-aligned RAM address
260 var bp_addr: i64 = 0
261 var bp_rs2: i64 = 0
262 var exp_reg: i64 = 0
263 var exp_next: i64 = 0
264 var bp_found: i64 = 0
265 var bp_word_is32: i64 = 0
266 var steps: i64 = 0
267 while steps < BOOTCAP_MAX_STEPS {
268 if ref.halted == 1 { steps = BOOTCAP_MAX_STEPS; continue }
269 let pc: i64 = ref.pc
270 if pc >= ref.mem_base { if pc < ref.mem_base + ref.mem_size {
271 let idx: i64 = (pc - ref.mem_base) / GG_RV_INSTR_BYTES
272 exec_bits[idx / 8] = ((exec_bits[idx / 8] as i64) | (1 << (idx % 8))) as u8
273 if bp_found == 0 {
274 let w: i64 = nx_rv64im_sim_load32(ref, pc)
275 if nx_rv64im_decode_kind(w) == NX_RV64IM_OP_STORE {
276 bp_found = 1
277 bp_addr = pc
278 bp_rs2 = nx_rv64im_rs2(w)
279 exp_reg = nx_rv64im_rf_read(ref.rf, bp_rs2)
280 if (w & 3) == 3 { bp_word_is32 = 1 }
281 exp_next = pc + GG_RV_INSTR_BYTES
282 }
283 }
284 } }
285 nx_rv64im_sim_step(ref)
286 steps = steps + 1
287 }
288 // never-executed RAM address: lowest 4-aligned address at/after the image end with a clear bit
289 var never_addr: i64 = 0 - 1
290 var cand: i64 = ref.mem_base + ((ilen + GG_RV_INSTR_BYTES - 1) / GG_RV_INSTR_BYTES) * GG_RV_INSTR_BYTES
291 while cand < ref.mem_base + ref.mem_size {
292 if never_addr < 0 {
293 let idx2: i64 = (cand - ref.mem_base) / GG_RV_INSTR_BYTES
294 if (((exec_bits[idx2 / 8] as i64) >> (idx2 % 8)) & 1) == 0 { never_addr = cand }
295 }
296 cand = cand + GG_RV_INSTR_BYTES
297 }
298 gg_p(" reference: halted=" as *u8); gg_n(ref.halted); gg_p(" code=" as *u8); gg_n(ref.halt_code); gg_p(" steps=" as *u8); gg_n(ref.steps)
299 gg_p(" first-store pc=0x" as *u8); gg_hex(bp_addr); gg_p(" rs2=x" as *u8); gg_n(bp_rs2); gg_p(" value=0x" as *u8); gg_hex(exp_reg)
300 gg_p(" next=0x" as *u8); gg_hex(exp_next); gg_p(" never-executed=0x" as *u8); gg_hex(never_addr); gg_p("\n" as *u8)
301 var t0: i64 = 0
302 if bp_found == 1 { if bp_word_is32 == 1 { if ref.halted == 1 { if ref.halt_code == 0 { if never_addr > 0 { if bp_addr > ref.mem_base { t0 = 1 } } } } } }
303 gv_check("T0 fixture-reached-the-condition: reference boot halts clean, a 32-bit store past reset was decoded, a never-executed RAM address exists" as *u8, t0, ctr)
304 // image words (from the FILE)
305 let w0: i64 = (img[0] as i64) | ((img[1] as i64) << 8) | ((img[2] as i64) << 16) | ((img[3] as i64) << 24)
306 let w1: i64 = (img[4] as i64) | ((img[5] as i64) << 8) | ((img[6] as i64) << 16) | ((img[7] as i64) << 24)
307
308 let out: *u8 = sys_mmap(GG_OUT_CAP)
309 let outlen: *i64 = sys_mmap(16) as *i64
310 let slog: *u8 = sys_mmap(GG_OUT_CAP)
311 let found: *i64 = sys_mmap(16) as *i64
312
313 // =========================== SESSION A: the stock-gdb oracle ===========================
314 if gv_need("stock gdb on PATH (/usr/bin/gdb-multiarch | /usr/bin/gdb | /usr/local/bin/gdb-multiarch)" as *u8, have_gdb, ctr) == 1 {
315 let readyA: *u8 = "/tmp/nx_gdbstub_gate/readyA" as *u8
316 let logA: *u8 = "/tmp/nx_gdbstub_gate/stubA.log" as *u8
317 gk_rm(readyA)
318 let pidA: i64 = gg_spawn(elf, pathp[0] as *u8, "0" as *u8, readyA, logA)
319 let portA: i64 = gg_wait_port(readyA)
320 gg_p(" session A: stub pid=" as *u8); gg_n(pidA); gg_p(" port=" as *u8); gg_n(portA); gg_p("\n" as *u8)
321 var a_ok: i64 = 0
322 if portA > 0 { a_ok = 1 }
323 gv_check("T1 stub listens on a kernel-chosen loopback port announced through its ready file" as *u8, a_ok, ctr)
324 if a_ok == 1 {
325 // gdb batch script, port + addresses substituted
326 let scr: *u8 = sys_mmap(4096)
327 var o: i64 = gk_cat(scr, 0, "set pagination off\nset confirm off\nset architecture riscv:rv64\ntarget remote 127.0.0.1:" as *u8)
328 o = gk_catn(scr, o, portA)
329 o = gk_cat(scr, o, "\ninfo registers pc\nbreak *0x" as *u8)
330 let hb: *u8 = sys_mmap(32)
331 var hi: i64 = 0
332 while hi < 16 { let nib: i64 = (bp_addr >> ((15 - hi) * 4)) & 15; if nib < 10 { hb[hi] = (48 + nib) as u8 } else { hb[hi] = (87 + nib) as u8 } hi = hi + 1 }
333 var hj: i64 = 0
334 while hj < 16 { scr[o] = hb[hj]; o = o + 1; hj = hj + 1 }
335 o = gk_cat(scr, o, "\ncontinue\ninfo registers pc\np/x $x" as *u8)
336 o = gk_catn(scr, o, bp_rs2)
337 o = gk_cat(scr, o, "\nstepi\ninfo registers pc\nx/2xw 0x" as *u8)
338 var hk: i64 = 0
339 while hk < 16 { let nib2: i64 = (ref.mem_base >> ((15 - hk) * 4)) & 15; if nib2 < 10 { scr[o] = (48 + nib2) as u8 } else { scr[o] = (87 + nib2) as u8 } o = o + 1; hk = hk + 1 }
340 o = gk_cat(scr, o, "\ndetach\nquit\n" as *u8)
341 scr[o] = 0 as u8
342 gk_write("/tmp/nx_gdbstub_gate/a.gdb" as *u8, scr)
343 outlen[0] = 0
344 let grc: i64 = gk_run_capture_ms(gdb, "-q" as *u8, "-batch" as *u8, "-x" as *u8, "/tmp/nx_gdbstub_gate/a.gdb" as *u8, GG_GDB_MS, out, GG_OUT_CAP, outlen)
345 let on: i64 = outlen[0]
346 gg_p(" gdb rc=" as *u8); gg_n(grc); gg_p(" output_bytes=" as *u8); gg_n(on); gg_p("\n" as *u8)
347 let src: i64 = gg_reap(pidA, GG_GDB_MS)
348 let sl: i64 = gk_read(logA, slog, GG_OUT_CAP - 1)
349 gg_p(" stub rc=" as *u8); gg_n(src); gg_p(" log_bytes=" as *u8); gg_n(sl); gg_p("\n" as *u8)
350 // T2 attach: first pc read == reset pc (mem_base)
351 let pc1: i64 = gg_value_after(out, on, "pc " as *u8, 1, found)
352 var t2: i64 = 0
353 if found[0] == 1 { if pc1 == ref.mem_base { t2 = 1 } }
354 gg_p(" attach pc=0x" as *u8); gg_hex(pc1); gg_p(" expected=0x" as *u8); gg_hex(ref.mem_base); gg_p("\n" as *u8)
355 gv_check("T2 stock gdb attached and read pc == reset vector" as *u8, t2, ctr)
356 // T3 breakpoint hit at the decoder-resolved address
357 let bph: i64 = gg_value_after(out, on, "Breakpoint 1, " as *u8, 1, found)
358 var t3: i64 = 0
359 if found[0] == 1 { if bph == bp_addr { t3 = 1 } }
360 gg_p(" breakpoint-hit pc=0x" as *u8); gg_hex(bph); gg_p(" expected=0x" as *u8); gg_hex(bp_addr); gg_p("\n" as *u8)
361 gv_check("T3 continue stops at the breakpoint the decoder resolved (first executed store)" as *u8, t3, ctr)
362 // T4 register value == independent sim's value
363 let rv: i64 = gg_value_after(out, on, "$1 = " as *u8, 1, found)
364 var t4: i64 = 0
365 if found[0] == 1 { if rv == exp_reg { t4 = 1 } }
366 gg_p(" gdb $x" as *u8); gg_n(bp_rs2); gg_p("=0x" as *u8); gg_hex(rv); gg_p(" reference=0x" as *u8); gg_hex(exp_reg); gg_p("\n" as *u8)
367 gv_check("T4 register read through gdb == value the stub-free reference sim derived" as *u8, t4, ctr)
368 // T5 stepi -> pc advanced by one 32-bit instruction (third pc line)
369 let pc3: i64 = gg_value_after(out, on, "pc " as *u8, 3, found)
370 var t5: i64 = 0
371 if found[0] == 1 { if pc3 == exp_next { t5 = 1 } }
372 gg_p(" after-stepi pc=0x" as *u8); gg_hex(pc3); gg_p(" expected=0x" as *u8); gg_hex(exp_next); gg_p("\n" as *u8)
373 gv_check("T5 stepi advances pc to the next instruction" as *u8, t5, ctr)
374 // T6 memory words == image file words
375 let mx: i64 = gg_find(out, on, 0, ":\t0x" as *u8)
376 var t6: i64 = 0
377 var mw0: i64 = 0
378 var mw1: i64 = 0
379 if mx >= 0 {
380 let nd: *i64 = sys_mmap(8) as *i64
381 mw0 = gg_parse_hex(out, on, mx + 4, nd)
382 let p2: i64 = gg_find(out, on, mx + 4, "0x" as *u8)
383 if p2 >= 0 { mw1 = gg_parse_hex(out, on, p2 + 2, nd) }
384 if mw0 == w0 { if mw1 == w1 { t6 = 1 } }
385 }
386 gg_p(" memory words via gdb=0x" as *u8); gg_hex(mw0); gg_p(",0x" as *u8); gg_hex(mw1); gg_p(" image file=0x" as *u8); gg_hex(w0); gg_p(",0x" as *u8); gg_hex(w1); gg_p("\n" as *u8)
387 gv_check("T6 memory read through gdb == the image file's own bytes" as *u8, t6, ctr)
388 // T7 detach left the guest running: stub log says after-detach halted clean AND transcript == golden
389 var t7: i64 = 0
390 if gg_find(slog, sl, 0, "session-end detached=1 killed=0" as *u8) >= 0 {
391 if gg_find(slog, sl, 0, "after-detach halted=1 code=0" as *u8) >= 0 {
392 let tb: i64 = gg_find(slog, sl, 0, "transcript=[" as *u8)
393 if tb >= 0 { if have_gold == 1 {
394 let ts: i64 = tb + 12
395 var same: i64 = 1
396 var q: i64 = 0
397 while q < glen { if ts + q >= sl { same = 0 } else { if slog[ts + q] != gold[q] { same = 0 } } q = q + 1 }
398 if ts + glen < sl { if (slog[ts + glen] as i64) != 93 { same = 0 } } // ']' right after the golden
399 t7 = same
400 } }
401 }
402 }
403 gv_check("T7 detach leaves the guest RUNNING: it completes the boot and its transcript == golden" as *u8, t7, ctr)
404 }
405 // =========================== SESSION B: neg-control, never-executed breakpoint ===========================
406 let readyB: *u8 = "/tmp/nx_gdbstub_gate/readyB" as *u8
407 let logB: *u8 = "/tmp/nx_gdbstub_gate/stubB.log" as *u8
408 gk_rm(readyB)
409 let pidB: i64 = gg_spawn(elf, pathp[0] as *u8, "0" as *u8, readyB, logB)
410 let portB: i64 = gg_wait_port(readyB)
411 gg_p(" session B: stub pid=" as *u8); gg_n(pidB); gg_p(" port=" as *u8); gg_n(portB); gg_p("\n" as *u8)
412 var t8: i64 = 0
413 if portB > 0 {
414 let scrb: *u8 = sys_mmap(4096)
415 var ob: i64 = gk_cat(scrb, 0, "set pagination off\nset confirm off\nset architecture riscv:rv64\ntarget remote 127.0.0.1:" as *u8)
416 ob = gk_catn(scrb, ob, portB)
417 ob = gk_cat(scrb, ob, "\nbreak *0x" as *u8)
418 var hn: i64 = 0
419 while hn < 16 { let nib3: i64 = (never_addr >> ((15 - hn) * 4)) & 15; if nib3 < 10 { scrb[ob] = (48 + nib3) as u8 } else { scrb[ob] = (87 + nib3) as u8 } ob = ob + 1; hn = hn + 1 }
420 ob = gk_cat(scrb, ob, "\ncontinue\ninfo registers pc\nquit\n" as *u8)
421 scrb[ob] = 0 as u8
422 gk_write("/tmp/nx_gdbstub_gate/b.gdb" as *u8, scrb)
423 outlen[0] = 0
424 let grcb: i64 = gk_run_capture_ms(gdb, "-q" as *u8, "-batch" as *u8, "-x" as *u8, "/tmp/nx_gdbstub_gate/b.gdb" as *u8, GG_GDB_MS, out, GG_OUT_CAP, outlen)
425 let onb: i64 = outlen[0]
426 let srcb: i64 = gg_reap(pidB, GG_GDB_MS)
427 let slb: i64 = gk_read(logB, slog, GG_OUT_CAP - 1)
428 gg_p(" gdb rc=" as *u8); gg_n(grcb); gg_p(" output_bytes=" as *u8); gg_n(onb); gg_p(" stub rc=" as *u8); gg_n(srcb); gg_p(" log_bytes=" as *u8); gg_n(slb); gg_p("\n" as *u8)
429 var exited: i64 = 0
430 if gg_find(out, onb, 0, "exited normally" as *u8) >= 0 { exited = 1 }
431 var bphit: i64 = 0
432 if gg_find(out, onb, 0, "Breakpoint 1, " as *u8) >= 0 { bphit = 1 }
433 var guest_done: i64 = 0
434 if gg_find(slog, slb, 0, "halted=1 code=0" as *u8) >= 0 { guest_done = 1 }
435 gg_p(" gdb saw exit=" as *u8); gg_n(exited); gg_p(" breakpoint-stop=" as *u8); gg_n(bphit); gg_p(" guest halted clean=" as *u8); gg_n(guest_done); gg_p("\n" as *u8)
436 if exited == 1 { if bphit == 0 { if guest_done == 1 { t8 = 1 } } }
437 }
438 gv_check("T8 neg-control-never-executed-breakpoint-does-NOT-stop: guest runs to exit under gdb, no breakpoint stop" as *u8, t8, ctr)
439 }
440
441 // =========================== SESSION C: protocol neg-controls, sovereign client, no gdb needed ===========================
442 let readyC: *u8 = "/tmp/nx_gdbstub_gate/readyC" as *u8
443 let logC: *u8 = "/tmp/nx_gdbstub_gate/stubC.log" as *u8
444 gk_rm(readyC)
445 let pidC: i64 = gg_spawn(elf, pathp[0] as *u8, "0" as *u8, readyC, logC)
446 let portC: i64 = gg_wait_port(readyC)
447 gg_p(" session C: stub pid=" as *u8); gg_n(pidC); gg_p(" port=" as *u8); gg_n(portC); gg_p("\n" as *u8)
448 var t9: i64 = 0
449 var t10: i64 = 0
450 var t11: i64 = 0
451 var t12: i64 = 0
452 if portC > 0 {
453 let fd: i64 = gg_rsp_connect(portC)
454 if fd >= 0 {
455 let rb: *u8 = sys_mmap(256)
456 // bad checksum: "$g#00" -> expect '-'
457 sys_write(fd, "$g#00" as *u8, 5)
458 var n1: i64 = gg_rsp_read(fd, rb, 255, 1)
459 gg_p(" after bad-checksum frame, peer sent bytes=" as *u8); gg_n(n1); gg_p(" first=" as *u8); if n1 > 0 { gg_n(rb[0] as i64) } gg_p("\n" as *u8)
460 if n1 >= 1 { if (rb[0] as i64) == 45 { t9 = 1 } }
461 // good frame "$?#3f" -> expect "+$S05#b8" (8 bytes)
462 sys_write(fd, "$?#3f" as *u8, 5)
463 var n2: i64 = gg_rsp_read(fd, rb, 255, 8)
464 var got2: i64 = 0
465 if n2 >= 8 { if gg_find(rb, n2, 0, "+$S05#b8" as *u8) >= 0 { got2 = 1 } }
466 gg_p(" after good '?' frame, bytes=" as *u8); gg_n(n2); gg_p(" ack+S05=" as *u8); gg_n(got2); gg_p("\n" as *u8)
467 t10 = got2
468 // ACK the reply: in ack mode the stub (like gdbserver) holds the line until it sees '+' for its
469 // own frame, so a client that skips the ack is the one breaking the protocol, not the stub
470 sys_write(fd, "+" as *u8, 1)
471 // detach "$D#44" -> expect "+$OK#9a"
472 sys_write(fd, "$D#44" as *u8, 5)
473 var n3: i64 = gg_rsp_read(fd, rb, 255, 7)
474 if n3 >= 7 { if gg_find(rb, n3, 0, "+$OK#9a" as *u8) >= 0 { t11 = 1 } }
475 gg_p(" after D frame, bytes=" as *u8); gg_n(n3); gg_p(" ack+OK=" as *u8); gg_n(t11); gg_p("\n" as *u8)
476 sys_write(fd, "+" as *u8, 1)
477 sys_close(fd)
478 }
479 let srcc: i64 = gg_reap(pidC, GG_GDB_MS)
480 let slc: i64 = gk_read(logC, slog, GG_OUT_CAP - 1)
481 if gg_find(slog, slc, 0, "bad_checksum_frames=1" as *u8) >= 0 { if gg_find(slog, slc, 0, "after-detach halted=1 code=0" as *u8) >= 0 { t12 = 1 } }
482 gg_p(" stub rc=" as *u8); gg_n(srcc); gg_p(" counted-bad-frame+guest-finished=" as *u8); gg_n(t12); gg_p("\n" as *u8)
483 }
484 gv_check("T9 neg-control-bad-checksum-frame-is-NAKed ('-')" as *u8, t9, ctr)
485 gv_check("T10 good '?' frame is ACKed and answered S05 (trap stop reason)" as *u8, t10, ctr)
486 gv_check("T11 D (detach) is answered OK" as *u8, t11, ctr)
487 gv_check("T12 the stub COUNTED the bad frame (bad_checksum_frames=1) and the guest finished after detach" as *u8, t12, ctr)
488
489 return gv_verdict("GDBSTUB-GATE" as *u8, ctr, "oracle=stock gdb when present (named above); reference=stub-free sim+decoder+image file; fixtures /tmp/nx_gdbstub_gate" as *u8)
490}