nx_dbg_step_gate.nx source
↩ module page · 648 lines · 34255 B
1// nx_dbg_step_gate.nx -- the VERDICT gate for LN12 (nx_dbg_step, watch symbol dbg_step_over).
2//
3// THE ACCEPT RULE (/compare/lang LN12): break and step on our own binaries over the line table we
4// emit. So this gate does not mock anything:
5// * it ASSEMBLES a real RV64 program containing a real linking CALL,
6// * it EMITS that program's .debug_line with nx_dwarf_line.nx -- OUR OWN DWARF v5 emitter, the one
7// LB5 shipped and GNU readelf validates -- so the table under test is the table we produce,
8// * it runs the deployed nx_dbg_step.elf against the pair and compares against expectations that
9// the debugger had no part in computing.
10//
11// THE SUBJECT IS NEVER ITS OWN ORACLE. Every expected value comes from somewhere else:
12// breakpoint address = the gate's OWN fixture layout (it placed line 12 at offset 8; the debugger
13// has to rediscover that through the line table, and the two must agree).
14// callee / caller line = likewise the gate's layout.
15// exit code and halt = a SECOND, DEBUGGER-FREE sim (bootcap_machine) run in this process.
16// never-executed addr = not assumed -- the reference sim records every pc it fetches and the gate
17// PROVES the neg-control address is absent from that set before using it.
18// the call is a call = decoded arithmetically with rv64im_min_decoder (kind == JAL, rd == ra)
19// BEFORE any stepping outcome is asserted, so a fixture that could not
20// exercise step-over cannot score a pass.
21//
22// WHY THE DISCRIMINATING TOOTH IS THE ONE THAT MATTERS. A debugger that single-steps INSTRUCTIONS
23// passes "the line changed" trivially. The only thing that separates a stepping debugger from an
24// instruction stepper is that STEP-OVER MUST NOT REPORT THE CALLEE'S LINE while STEP-INTO MUST. Both
25// are asserted here, on the same fixture, from the same starting pc -- two tests that each isolate one
26// signal would not prove discrimination between them.
27//
28// NEVER-BRICK IS TESTED BEHAVIOURALLY, NOT BY GREPPING FOR A WORD. A pid-shaped argument must be
29// REFUSED (a process id can never become a subject), and the source must contain no sys_ptrace call
30// site. The behavioural half cannot be fooled by prose, which a source scan can be -- this file and
31// the organ both discuss ptrace at length, and a detector that finds its own explanation of the bug
32// it hunts is the trap this estate has already paid for twice.
33//
34// Fixtures live in /tmp/nx_dbg_step_gate/ (created at setup, so a crashed run cannot leave a fixture
35// beside a production beat) and are ASSEMBLED AT RUNTIME, never checked in.
36// license_tier: ORIGINAL
37import "nx_syscalls.nx"
38import "nx_gate_verdict.nx"
39import "nx_gatekit_lib.nx"
40import "nx_dwarf_line.nx"
41import "nx_bootcap.nx"
42import "rv64im_min_decoder.nx"
43
44const DG_DIR: *u8 = "/tmp/nx_dbg_step_gate"
45const DG_IMG: *u8 = "/tmp/nx_dbg_step_gate/fixture.bin"
46const DG_LT: *u8 = "/tmp/nx_dbg_step_gate/fixture.debug_line"
47const DG_SRCNAME: *u8 = "dbgfix.nx"
48const DG_SRC: *u8 = "buildroot/runtime/nx_dbg_step.nx"
49
50// A session over a 10-instruction fixture finishes in milliseconds; a minute is a hang, and a hung
51// subject must become a FAILED TOOTH, not a hung gate.
52const DG_RUN_MS: i64 = 60000
53const DG_POLL_MS: i64 = 25
54const DG_OUT_CAP: i64 = 1048576 // the gatekit capture bound; asserted not reached, never silent
55const DG_ARGV_MAX: i64 = 16
56const DG_MODE_644: i64 = 420
57const DG_ASCII_ZERO: i64 = 48
58const DG_ASCII_NINE: i64 = 57
59const DG_ASCII_a: i64 = 97
60const DG_ASCII_f: i64 = 102
61const DG_DECIMAL: i64 = 10
62const DG_HEXBASE: i64 = 16
63const DG_PTR: i64 = 8
64const DG_SCRATCH: i64 = 64
65const DG_BYTE_MASK: i64 = 255
66
67// ---- the fixture, as DATA. offsets are 4-byte instruction slots; lines are the source lines the
68// emitted table will carry. Every expectation below is derived from THIS table, never from a literal
69// repeated at the assertion site.
70const DG_N_INSTR: i64 = 10
71const DG_OFF_L10: i64 = 0 // addi a0, x0, 7
72const DG_OFF_L11: i64 = 4 // addi a1, x0, 2
73const DG_OFF_CALL: i64 = 8 // jal ra, +24 <- THE CALL
74const DG_OFF_AFTER: i64 = 12 // addi a2, x0, 3 <- where a step-OVER must land
75const DG_OFF_SETRC: i64 = 16 // addi a0, x0, 5
76const DG_OFF_HALT: i64 = 20 // ebreak <- halt_code = a0
77const DG_OFF_NEVER: i64 = 24 // addi a3, x0, 9 <- NEVER executed (neg-control)
78const DG_OFF_NEVER2: i64 = 28 // ebreak <- NEVER executed
79const DG_OFF_CALLEE: i64 = 32 // addi a4, x0, 4 <- where a step-INTO must land
80const DG_OFF_CALLRET: i64 = 36 // jalr x0, 0(ra)
81const DG_IMG_BYTES: i64 = 40
82
83const DG_LINE_L10: i64 = 10
84const DG_LINE_L11: i64 = 11
85const DG_LINE_CALL: i64 = 12
86const DG_LINE_AFTER: i64 = 13
87const DG_LINE_SETRC: i64 = 14
88const DG_LINE_HALT: i64 = 15
89const DG_LINE_NEVER: i64 = 20
90const DG_LINE_NEVER2: i64 = 21
91const DG_LINE_CALLEE: i64 = 100
92const DG_LINE_CALLRET: i64 = 101
93const DG_LINE_BOGUS: i64 = 999 // present in no row: the resolver must refuse it BY NAME
94const DG_END_OFF: i64 = 40 // end_sequence address = one past the last instruction
95
96// ---- RV64 encodings. Hand-built HERE on purpose: the gate must derive its fixture without help
97// from the subject, and an encoder shared with the debugger would make the fixture agree with it by
98// construction rather than by correctness.
99const DG_OPC_OP_IMM: i64 = 0x13
100const DG_OPC_JAL: i64 = 0x6F
101const DG_OPC_JALR: i64 = 0x67
102const DG_EBREAK: i64 = 0x00100073
103const DG_REG_RA: i64 = 1
104const DG_REG_A0: i64 = 10
105const DG_REG_A1: i64 = 11
106const DG_REG_A2: i64 = 12
107const DG_REG_A3: i64 = 13
108const DG_REG_A4: i64 = 14
109const DG_IMM12_MASK: i64 = 4095
110const DG_EXIT_RC: i64 = 5 // what the fixture puts in a0 before ebreak
111const DG_A0_INITIAL: i64 = 7 // deliberately different, so a wrong exit code cannot look right
112
113func dg_addi(rd: i64, rs1: i64, imm: i64) -> i64 {
114 return ((imm & DG_IMM12_MASK) << 20) | (rs1 << 15) | (rd << 7) | DG_OPC_OP_IMM
115}
116func dg_jal(rd: i64, off: i64) -> i64 {
117 let b20: i64 = (off >> 20) & 1
118 let b10_1: i64 = (off >> 1) & 1023
119 let b11: i64 = (off >> 11) & 1
120 let b19_12: i64 = (off >> 12) & DG_BYTE_MASK
121 return (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12) | (rd << 7) | DG_OPC_JAL
122}
123func dg_jalr(rd: i64, rs1: i64, imm: i64) -> i64 {
124 return ((imm & DG_IMM12_MASK) << 20) | (rs1 << 15) | (rd << 7) | DG_OPC_JALR
125}
126func dg_put32(b: *u8, off: i64, w: i64) -> i64 {
127 var i: i64 = 0
128 while i < 4 { b[off + i] = ((w >> (i * 8)) & DG_BYTE_MASK) as u8; i = i + 1 }
129 return 0
130}
131func dg_get32(b: *u8, off: i64) -> i64 {
132 var v: i64 = 0
133 var i: i64 = 0
134 while i < 4 { v = v | ((b[off + i] as i64) << (i * 8)); i = i + 1 }
135 return v
136}
137
138// ---- output plumbing
139func dg_p(s: *u8) -> i64 { return gv_puts(s) }
140func dg_n(v: i64) -> i64 { return gv_num(v) }
141
142func dg_write_bin(path: *u8, buf: *u8, n: i64) -> i64 {
143 let fd: i64 = sys_openat_wr(path, DG_MODE_644)
144 if fd < 0 { return 0 - 1 }
145 gk_write_all(fd, buf, n)
146 sys_close(fd)
147 return n
148}
149
150// spawn the subject with an arbitrary-arity argv (gk_run_capture tops out at four, and a debugger
151// script is longer than that), stdout+stderr to a log. Returns the pid.
152func dg_spawn(elf: *u8, words: *i64, nwords: i64, logpath: *u8) -> i64 {
153 let argv: *i64 = sys_mmap(DG_PTR * (DG_ARGV_MAX + 1)) as *i64
154 argv[0] = elf as i64
155 var i: i64 = 0
156 while i < nwords { argv[1 + i] = words[i]; i = i + 1 }
157 argv[1 + nwords] = 0
158 let envp: *i64 = sys_mmap(DG_PTR * 2) as *i64
159 envp[0] = 0
160 let pid: i64 = sys_fork()
161 if pid < 0 { return 0 - 1 }
162 if pid == 0 {
163 let lf: i64 = sys_openat_wr(logpath, DG_MODE_644)
164 if lf >= 0 { sys_dup3(lf, 1, 0); sys_dup3(lf, 2, 0) }
165 sys_execve(elf, argv, envp)
166 sys_exit(127)
167 }
168 return pid
169}
170// bounded reap: the exit code, or -1 if it had to be killed (announced, and a failed tooth)
171func dg_reap(pid: i64, budget_ms: i64) -> i64 {
172 let st: *i64 = sys_mmap(DG_SCRATCH) as *i64
173 var waited: i64 = 0
174 while waited < budget_ms {
175 st[0] = 0
176 let r: i64 = sys_wait4(pid, st, WNOHANG)
177 if r == pid { return gk_wait_code(st[0]) }
178 sys_sleep_ms(DG_POLL_MS)
179 waited = waited + DG_POLL_MS
180 }
181 dg_p(" WATCHDOG: subject did not exit within budget -- killed (a failed tooth, never a pass)\n" as *u8)
182 nx_kill(pid, 9)
183 sys_wait4(pid, st, 0)
184 return 0 - 1
185}
186
187// run one debugger session; returns the exit code and fills outbuf/outlen
188func dg_session(elf: *u8, words: *i64, nwords: i64, log: *u8, outbuf: *u8, outlen: *i64) -> i64 {
189 let pid: i64 = dg_spawn(elf, words, nwords, log)
190 if pid < 0 { outlen[0] = 0; return 0 - 1 }
191 let rc: i64 = dg_reap(pid, DG_RUN_MS)
192 let n: i64 = gk_read(log, outbuf, DG_OUT_CAP - 1)
193 var m: i64 = n
194 if m < 0 { m = 0 }
195 outbuf[m] = 0 as u8
196 outlen[0] = m
197 return rc
198}
199
200// ---- output readers. Anchored: find the ANCHOR first, then the field after it, so a field name
201// occurring earlier in the transcript can never be read as this line's answer.
202func dg_find(buf: *u8, n: i64, from: i64, needle: *u8) -> i64 {
203 let nl: i64 = gk_len(needle)
204 if nl == 0 { return 0 - 1 }
205 var i: i64 = from
206 while i + nl <= n {
207 var j: i64 = 0
208 var ok: i64 = 1
209 while j < nl { if buf[i + j] != needle[j] { ok = 0; j = nl } else { j = j + 1 } }
210 if ok == 1 { return i }
211 i = i + 1
212 }
213 return 0 - 1
214}
215// decimal value of `field` that appears AFTER `anchor`; found[0]=0 when either is missing
216func dg_dec_after(buf: *u8, n: i64, anchor: *u8, field: *u8, found: *i64) -> i64 {
217 found[0] = 0
218 var at: i64 = 0
219 if gk_len(anchor) > 0 {
220 at = dg_find(buf, n, 0, anchor)
221 if at < 0 { return 0 }
222 at = at + gk_len(anchor)
223 }
224 let f: i64 = dg_find(buf, n, at, field)
225 if f < 0 { return 0 }
226 var i: i64 = f + gk_len(field)
227 var v: i64 = 0
228 var d: i64 = 0
229 var go: i64 = 1
230 while go == 1 {
231 if i >= n { go = 0; continue }
232 let c: i64 = buf[i] as i64
233 if c < DG_ASCII_ZERO { go = 0; continue }
234 if c > DG_ASCII_NINE { go = 0; continue }
235 v = v * DG_DECIMAL + (c - DG_ASCII_ZERO)
236 d = d + 1
237 i = i + 1
238 }
239 if d == 0 { return 0 }
240 found[0] = 1
241 return v
242}
243func dg_hex_after(buf: *u8, n: i64, anchor: *u8, field: *u8, found: *i64) -> i64 {
244 found[0] = 0
245 var at: i64 = 0
246 if gk_len(anchor) > 0 {
247 at = dg_find(buf, n, 0, anchor)
248 if at < 0 { return 0 }
249 at = at + gk_len(anchor)
250 }
251 let f: i64 = dg_find(buf, n, at, field)
252 if f < 0 { return 0 }
253 var i: i64 = f + gk_len(field)
254 var v: i64 = 0
255 var d: i64 = 0
256 var go: i64 = 1
257 while go == 1 {
258 if i >= n { go = 0; continue }
259 let c: i64 = buf[i] as i64
260 var h: i64 = 0 - 1
261 if c >= DG_ASCII_ZERO { if c <= DG_ASCII_NINE { h = c - DG_ASCII_ZERO } }
262 if c >= DG_ASCII_a { if c <= DG_ASCII_f { h = c - (DG_ASCII_a - DG_DECIMAL) } }
263 if h < 0 { go = 0; continue }
264 v = v * DG_HEXBASE + h
265 d = d + 1
266 i = i + 1
267 }
268 if d == 0 { return 0 }
269 found[0] = 1
270 return v
271}
272func dg_has(buf: *u8, n: i64, needle: *u8) -> i64 {
273 if dg_find(buf, n, 0, needle) >= 0 { return 1 }
274 return 0
275}
276
277// ---- decimal word for argv (breakpoint line numbers are passed as text)
278func dg_word(v: i64) -> *u8 {
279 let b: *u8 = sys_mmap(DG_SCRATCH)
280 let t: *u8 = sys_mmap(DG_SCRATCH)
281 var m: i64 = v
282 var k: i64 = 0
283 if m == 0 { t[0] = DG_ASCII_ZERO as u8; k = 1 }
284 while m > 0 { t[k] = (DG_ASCII_ZERO + (m % DG_DECIMAL)) as u8; m = m / DG_DECIMAL; k = k + 1 }
285 var i: i64 = 0
286 while i < k { b[i] = t[k - 1 - i]; i = i + 1 }
287 b[k] = 0 as u8
288 return b
289}
290
291func main() -> i64 {
292 gv_head("nx_dbg_step gate -- statement stepping on a real RV64 fixture over a line table emitted by our own nx_dwarf_line; expectations from the gate's fixture layout and a debugger-free reference sim; neg-controls for a never-executed breakpoint, a bogus file:line, and a pid-shaped subject" as *u8)
293 let ctr: *i64 = gv_ctr()
294 gk_mkdir(DG_DIR)
295
296 // ---- resolve the subject
297 let elf: *u8 = sys_mmap(1024)
298 var have: i64 = 0
299 if gk_exists("_offc/nx_dbg_step.elf" as *u8) == 1 { gk_cat(elf, 0, "_offc/nx_dbg_step.elf" as *u8); elf[gk_len("_offc/nx_dbg_step.elf" as *u8)] = 0 as u8; have = 1 }
300 if have == 0 { if gk_exists("./nx_dbg_step.elf" as *u8) == 1 { gk_cat(elf, 0, "./nx_dbg_step.elf" as *u8); elf[gk_len("./nx_dbg_step.elf" as *u8)] = 0 as u8; have = 1 } }
301 if have == 0 { if gk_exists("_build/nx_dbg_step.sov.elf" as *u8) == 1 { gk_cat(elf, 0, "_build/nx_dbg_step.sov.elf" as *u8); elf[gk_len("_build/nx_dbg_step.sov.elf" as *u8)] = 0 as u8; have = 1 } }
302 dg_p(" subject: " as *u8)
303 if have == 1 { dg_p(elf) } else { dg_p("ABSENT (looked in _offc/, ./, _build/)" as *u8) }
304 dg_p("\n" as *u8)
305 gv_need("deployed nx_dbg_step.elf (the subject)" as *u8, have, ctr)
306 if have == 0 { return gv_verdict("DBG-STEP-GATE" as *u8, ctr, "subject absent" as *u8) }
307
308 // =====================================================================================
309 // ASSEMBLE THE FIXTURE
310 // =====================================================================================
311 let img: *u8 = sys_mmap(DG_IMG_BYTES)
312 dg_put32(img, DG_OFF_L10, dg_addi(DG_REG_A0, 0, DG_A0_INITIAL))
313 dg_put32(img, DG_OFF_L11, dg_addi(DG_REG_A1, 0, 2))
314 dg_put32(img, DG_OFF_CALL, dg_jal(DG_REG_RA, DG_OFF_CALLEE - DG_OFF_CALL))
315 dg_put32(img, DG_OFF_AFTER, dg_addi(DG_REG_A2, 0, 3))
316 dg_put32(img, DG_OFF_SETRC, dg_addi(DG_REG_A0, 0, DG_EXIT_RC))
317 dg_put32(img, DG_OFF_HALT, DG_EBREAK)
318 dg_put32(img, DG_OFF_NEVER, dg_addi(DG_REG_A3, 0, 9))
319 dg_put32(img, DG_OFF_NEVER2, DG_EBREAK)
320 dg_put32(img, DG_OFF_CALLEE, dg_addi(DG_REG_A4, 0, 4))
321 dg_put32(img, DG_OFF_CALLRET, dg_jalr(0, DG_REG_RA, 0))
322 dg_write_bin(DG_IMG, img, DG_IMG_BYTES)
323
324 let base: i64 = BOOTCAP_MEM_BASE
325 let addr_call: i64 = base + DG_OFF_CALL
326 let addr_after: i64 = base + DG_OFF_AFTER
327 let addr_callee: i64 = base + DG_OFF_CALLEE
328 let addr_never: i64 = base + DG_OFF_NEVER
329
330 // ---- FIXTURE PRECONDITIONS, ASSERTED ARITHMETICALLY AND FIRST. A fixture whose call site is
331 // not a linking call cannot exercise step-over at all, and every outcome tooth below would then
332 // pass for the wrong reason.
333 let w_call: i64 = dg_get32(img, DG_OFF_CALL)
334 let k_call: i64 = nx_rv64im_decode_kind(w_call)
335 let rd_call: i64 = nx_rv64im_rd(w_call)
336 dg_p(" fixture call word=" as *u8); dg_n(w_call)
337 dg_p(" decoded_kind=" as *u8); dg_n(k_call); dg_p(" (JAL=" as *u8); dg_n(NX_RV64IM_OP_JAL); dg_p(")" as *u8)
338 dg_p(" rd=" as *u8); dg_n(rd_call); dg_p(" (ra=" as *u8); dg_n(DG_REG_RA); dg_p(")\n" as *u8)
339 var call_ok: i64 = 0
340 if k_call == NX_RV64IM_OP_JAL { if rd_call == DG_REG_RA { call_ok = 1 } }
341 gv_check("fixture-call-site-decodes-as-a-LINKING-jal-rd-is-ra-so-step-over-has-something-to-step-over" as *u8, call_ok, ctr)
342
343 let w_ret: i64 = dg_get32(img, DG_OFF_CALLRET)
344 let k_ret: i64 = nx_rv64im_decode_kind(w_ret)
345 let rd_ret: i64 = nx_rv64im_rd(w_ret)
346 let rs1_ret: i64 = nx_rv64im_rs1(w_ret)
347 dg_p(" fixture ret word=" as *u8); dg_n(w_ret)
348 dg_p(" kind=" as *u8); dg_n(k_ret); dg_p(" (JALR=" as *u8); dg_n(NX_RV64IM_OP_JALR); dg_p(")" as *u8)
349 dg_p(" rd=" as *u8); dg_n(rd_ret); dg_p(" rs1=" as *u8); dg_n(rs1_ret); dg_p("\n" as *u8)
350 var ret_ok: i64 = 0
351 if k_ret == NX_RV64IM_OP_JALR { if rd_ret == 0 { if rs1_ret == DG_REG_RA { ret_ok = 1 } } }
352 gv_check("fixture-callee-returns-through-jalr-x0-ra-so-the-call-depth-really-comes-back-down" as *u8, ret_ok, ctr)
353
354 // =====================================================================================
355 // THE REFERENCE RUN -- a SECOND sim, no debugger. Supplies the exit code and PROVES the
356 // neg-control address is genuinely never fetched.
357 // =====================================================================================
358 let reftx: *u8 = sys_mmap(BOOTCAP_MEM_SIZE)
359 let ref: *NxRv64imSim = bootcap_machine(img, DG_IMG_BYTES, reftx, BOOTCAP_MEM_SIZE)
360 var never_seen: i64 = 0
361 var call_seen: i64 = 0
362 var callee_seen: i64 = 0
363 var refsteps: i64 = 0
364 var rgo: i64 = 1
365 while rgo == 1 {
366 if ref.halted == 1 { rgo = 0; continue }
367 if refsteps >= BOOTCAP_MAX_STEPS { rgo = 0; continue }
368 if ref.pc == addr_never { never_seen = never_seen + 1 }
369 if ref.pc == addr_call { call_seen = call_seen + 1 }
370 if ref.pc == addr_callee { callee_seen = callee_seen + 1 }
371 nx_rv64im_sim_step(ref)
372 refsteps = refsteps + 1
373 }
374 dg_p(" reference run (no debugger): halted=" as *u8); dg_n(ref.halted)
375 dg_p(" exit_code=" as *u8); dg_n(ref.halt_code)
376 dg_p(" steps=" as *u8); dg_n(refsteps)
377 dg_p(" call_pc_fetched=" as *u8); dg_n(call_seen)
378 dg_p(" callee_pc_fetched=" as *u8); dg_n(callee_seen)
379 dg_p(" never_addr_fetched=" as *u8); dg_n(never_seen); dg_p("\n" as *u8)
380 var refclean: i64 = 0
381 if ref.halted == 1 { if ref.halt_code == DG_EXIT_RC { refclean = 1 } }
382 gv_check("reference-sim-halts-clean-and-its-exit-code-is-the-one-the-fixture-stores-not-a-hardcoded-expectation" as *u8, refclean, ctr)
383 var reached: i64 = 0
384 if call_seen > 0 { if callee_seen > 0 { reached = 1 } }
385 gv_check("fixture-reached-the-condition-both-the-call-site-and-the-callee-really-execute" as *u8, reached, ctr)
386 var neverproven: i64 = 0
387 if never_seen == 0 { neverproven = 1 }
388 gv_check("neg-control-precondition-the-never-executed-address-is-PROVEN-unfetched-over-the-whole-reference-run" as *u8, neverproven, ctr)
389
390 // =====================================================================================
391 // EMIT THE LINE TABLE WITH OUR OWN nx_dwarf_line (LB5's emitter) -- this is the "over the line
392 // table we emit" half of the contract, and it is why this is not a mock.
393 // =====================================================================================
394 let srcbuf: *u8 = sys_mmap(DG_SCRATCH)
395 let nmlen: i64 = gk_len(DG_SRCNAME)
396 gk_cat(srcbuf, 0, DG_SRCNAME)
397 let foff: *i64 = sys_mmap(DG_PTR * 2) as *i64
398 let flen: *i64 = sys_mmap(DG_PTR * 2) as *i64
399 foff[0] = 0
400 flen[0] = nmlen
401 let d: *NxDwLine = nx_dwline_new(GK_MAGIC_4096)
402 nx_dwline_header(d, srcbuf, foff, flen, 1)
403 nx_dwline_set_address(d, base + DG_OFF_L10)
404 nx_dwline_record(d, base + DG_OFF_L10, 1, DG_LINE_L10)
405 nx_dwline_record(d, base + DG_OFF_L11, 1, DG_LINE_L11)
406 nx_dwline_record(d, base + DG_OFF_CALL, 1, DG_LINE_CALL)
407 nx_dwline_record(d, base + DG_OFF_AFTER, 1, DG_LINE_AFTER)
408 nx_dwline_record(d, base + DG_OFF_SETRC, 1, DG_LINE_SETRC)
409 nx_dwline_record(d, base + DG_OFF_HALT, 1, DG_LINE_HALT)
410 nx_dwline_record(d, base + DG_OFF_NEVER, 1, DG_LINE_NEVER)
411 nx_dwline_record(d, base + DG_OFF_NEVER2, 1, DG_LINE_NEVER2)
412 nx_dwline_record(d, base + DG_OFF_CALLEE, 1, DG_LINE_CALLEE)
413 nx_dwline_record(d, base + DG_OFF_CALLRET, 1, DG_LINE_CALLRET)
414 nx_dwline_advance_pc(d, DG_END_OFF - DG_OFF_CALLRET)
415 nx_dwline_end_sequence(d)
416 nx_dwline_finish(d)
417 let ltbytes: i64 = nx_dwline_size(d)
418 dg_write_bin(DG_LT, nx_dwline_bytes(d), ltbytes)
419 dg_p(" line table emitted by nx_dwarf_line: bytes=" as *u8); dg_n(ltbytes)
420 dg_p(" rows_recorded=" as *u8); dg_n(DG_N_INSTR)
421 dg_p(" file=" as *u8); dg_p(DG_SRCNAME); dg_p("\n" as *u8)
422
423 let out: *u8 = sys_mmap(DG_OUT_CAP)
424 let olen: *i64 = sys_mmap(DG_SCRATCH) as *i64
425 let fnd: *i64 = sys_mmap(DG_SCRATCH) as *i64
426 let words: *i64 = sys_mmap(DG_PTR * (DG_ARGV_MAX + 1)) as *i64
427
428 // ---- SESSION 0: dump the table. The debugger must rediscover every row we recorded, plus the
429 // end_sequence marker, by decoding bytes it did not write.
430 words[0] = "lines" as i64
431 words[1] = DG_LT as i64
432 var rc: i64 = dg_session(elf, words, 2, "/tmp/nx_dbg_step_gate/s0.log" as *u8, out, olen)
433 var n: i64 = olen[0]
434 dg_p(" S0 lines: rc=" as *u8); dg_n(rc); dg_p(" bytes=" as *u8); dg_n(n); dg_p("\n" as *u8)
435 var capfree: i64 = 0
436 if n < DG_OUT_CAP - 1 { capfree = 1 }
437 gv_check("capture-did-not-reach-its-bound-so-no-tooth-below-is-reading-a-truncated-transcript" as *u8, capfree, ctr)
438 let got_rows: i64 = dg_dec_after(out, n, "DBG linetable ok=" as *u8, "rows=" as *u8, fnd)
439 let got_files: i64 = dg_dec_after(out, n, "DBG linetable ok=" as *u8, "files=" as *u8, fnd)
440 let got_dc: i64 = dg_dec_after(out, n, "DBG linetable ok=" as *u8, "decode_complete=" as *u8, fnd)
441 dg_p(" S0 decoded rows=" as *u8); dg_n(got_rows)
442 dg_p(" expected=" as *u8); dg_n(DG_N_INSTR + 1)
443 dg_p(" files=" as *u8); dg_n(got_files)
444 dg_p(" decode_complete=" as *u8); dg_n(got_dc); dg_p("\n" as *u8)
445 var rows_ok: i64 = 0
446 if got_rows == DG_N_INSTR + 1 { rows_ok = 1 } // 10 statement rows + end_sequence
447 gv_check("every-row-our-emitter-wrote-is-recovered-by-the-debuggers-independent-decode-plus-the-end-sequence" as *u8, rows_ok, ctr)
448 var dc_ok: i64 = 0
449 if got_dc == 1 { dc_ok = 1 }
450 gv_check("line-table-decode-is-COMPLETE-so-a-refusal-below-means-absent-and-not-merely-unread" as *u8, dc_ok, ctr)
451 var name_ok: i64 = 0
452 if dg_has(out, n, DG_SRCNAME) == 1 { name_ok = 1 }
453 gv_check("the-v5-file-name-table-is-decoded-so-a-breakpoint-can-name-a-FILE-and-not-only-a-file-index" as *u8, name_ok, ctr)
454
455 // ---- SESSION 1: STEP INTO. break at the call line, continue to it, then step -- which MUST
456 // enter the callee and report the callee's source line.
457 words[0] = "run" as i64
458 words[1] = DG_IMG as i64
459 words[2] = DG_LT as i64
460 words[3] = "break" as i64
461 words[4] = DG_SRCNAME as i64
462 words[5] = dg_word(DG_LINE_CALL) as i64
463 words[6] = "continue" as i64
464 words[7] = "step" as i64
465 words[8] = "detach" as i64
466 rc = dg_session(elf, words, 9, "/tmp/nx_dbg_step_gate/s1.log" as *u8, out, olen)
467 n = olen[0]
468 let bp_addr: i64 = dg_hex_after(out, n, "BREAK ok=1" as *u8, "addr=0x" as *u8, fnd)
469 let bp_found: i64 = fnd[0]
470 let cont_pc: i64 = dg_hex_after(out, n, "STOP cmd=continue" as *u8, "pc=0x" as *u8, fnd)
471 let step_line: i64 = dg_dec_after(out, n, "STOP cmd=step " as *u8, "line=" as *u8, fnd)
472 let step_found: i64 = fnd[0]
473 let s1_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd)
474 let s1_halted: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "halted=" as *u8, fnd)
475 let s1_unch: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "code_bytes_unchanged=" as *u8, fnd)
476 let s1_auto: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "auto=" as *u8, fnd)
477 dg_p(" S1 step-into: rc=" as *u8); dg_n(rc)
478 dg_p(" bp_addr=" as *u8); dg_n(bp_addr); dg_p(" expected=" as *u8); dg_n(addr_call)
479 dg_p(" continue_pc=" as *u8); dg_n(cont_pc)
480 dg_p(" step_line=" as *u8); dg_n(step_line); dg_p(" expected=" as *u8); dg_n(DG_LINE_CALLEE)
481 dg_p(" exit_code=" as *u8); dg_n(s1_exit); dg_p(" halted=" as *u8); dg_n(s1_halted)
482 dg_p(" code_unchanged=" as *u8); dg_n(s1_unch); dg_p(" auto_detach=" as *u8); dg_n(s1_auto)
483 dg_p("\n" as *u8)
484
485 var bpok: i64 = 0
486 if bp_found == 1 { if bp_addr == addr_call { bpok = 1 } }
487 gv_check("breakpoint-set-by-FILE-and-LINE-resolves-through-our-line-table-to-the-address-the-fixture-layout-fixes" as *u8, bpok, ctr)
488 var hitok: i64 = 0
489 if cont_pc == addr_call { hitok = 1 }
490 gv_check("that-breakpoint-actually-HITS-and-execution-stops-with-pc-exactly-on-it" as *u8, hitok, ctr)
491 var intook: i64 = 0
492 if step_found == 1 { if step_line == DG_LINE_CALLEE { intook = 1 } }
493 gv_check("step-INTO-from-the-call-statement-enters-the-callee-and-reports-the-callee-source-line" as *u8, intook, ctr)
494 var resumed1: i64 = 0
495 if s1_halted == 1 { if s1_exit == ref.halt_code { resumed1 = 1 } }
496 gv_check("after-detach-the-guest-RESUMES-and-exits-with-the-reference-sims-exit-code-the-safety-tooth" as *u8, resumed1, ctr)
497 var unch1: i64 = 0
498 if s1_unch == 1 { unch1 = 1 }
499 gv_check("guest-code-bytes-are-byte-identical-after-the-session-breakpoints-never-patched-the-guest" as *u8, unch1, ctr)
500
501 // ---- SESSION 2: STEP OVER, from the SAME pc as session 1. This is the discriminating pair:
502 // same fixture, same starting statement, opposite required answer.
503 words[7] = "stepover" as i64
504 rc = dg_session(elf, words, 9, "/tmp/nx_dbg_step_gate/s2.log" as *u8, out, olen)
505 n = olen[0]
506 let over_line: i64 = dg_dec_after(out, n, "STOP cmd=stepover" as *u8, "line=" as *u8, fnd)
507 let over_found: i64 = fnd[0]
508 let over_pc: i64 = dg_hex_after(out, n, "STOP cmd=stepover" as *u8, "pc=0x" as *u8, fnd)
509 let over_depth: i64 = dg_dec_after(out, n, "STOP cmd=stepover" as *u8, "depth=" as *u8, fnd)
510 let s2_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd)
511 let s2_halted: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "halted=" as *u8, fnd)
512 dg_p(" S2 step-over: rc=" as *u8); dg_n(rc)
513 dg_p(" line=" as *u8); dg_n(over_line); dg_p(" expected=" as *u8); dg_n(DG_LINE_AFTER)
514 dg_p(" (callee line " as *u8); dg_n(DG_LINE_CALLEE); dg_p(" must NOT appear here)" as *u8)
515 dg_p(" pc=" as *u8); dg_n(over_pc); dg_p(" expected=" as *u8); dg_n(addr_after)
516 dg_p(" depth=" as *u8); dg_n(over_depth)
517 dg_p(" exit_code=" as *u8); dg_n(s2_exit); dg_p(" halted=" as *u8); dg_n(s2_halted)
518 dg_p("\n" as *u8)
519
520 var overok: i64 = 0
521 if over_found == 1 { if over_line == DG_LINE_AFTER { overok = 1 } }
522 gv_check("step-OVER-from-the-call-statement-lands-on-the-next-statement-IN-THE-CALLER" as *u8, overok, ctr)
523 var overpcok: i64 = 0
524 if over_pc == addr_after { overpcok = 1 }
525 gv_check("step-OVER-stops-at-the-return-address-so-the-whole-callee-really-ran-rather-than-being-skipped" as *u8, overpcok, ctr)
526 // THE DISCRIMINATING TOOTH. A debugger that merely single-steps instructions reports the callee
527 // line here; only one that tracks call depth does not.
528 var notcallee: i64 = 1
529 if over_line == DG_LINE_CALLEE { notcallee = 0 }
530 gv_check("neg-control-step-OVER-never-reports-the-CALLEE-line-the-one-behaviour-that-separates-a-statement-stepper-from-an-instruction-stepper" as *u8, notcallee, ctr)
531 // exactly ONE statement, measured as a line delta against the line we started on
532 var onestmt: i64 = 0
533 if over_line - DG_LINE_CALL == 1 { onestmt = 1 }
534 gv_check("step-OVER-advanced-exactly-ONE-source-statement-line-delta-is-one-not-a-jump-to-wherever" as *u8, onestmt, ctr)
535 var depthback: i64 = 0
536 if over_depth == 0 { depthback = 1 }
537 gv_check("call-depth-returned-to-zero-so-the-callees-return-was-observed-and-not-merely-outrun" as *u8, depthback, ctr)
538 var resumed2: i64 = 0
539 if s2_halted == 1 { if s2_exit == ref.halt_code { resumed2 = 1 } }
540 gv_check("after-a-step-over-session-the-guest-also-resumes-and-exits-normally" as *u8, resumed2, ctr)
541
542 // ---- SESSION 3: NEG-CONTROL, a breakpoint on a line the reference run PROVED is never reached.
543 words[5] = dg_word(DG_LINE_NEVER) as i64
544 words[6] = "continue" as i64
545 words[7] = "detach" as i64
546 rc = dg_session(elf, words, 8, "/tmp/nx_dbg_step_gate/s3.log" as *u8, out, olen)
547 n = olen[0]
548 let n3_addr: i64 = dg_hex_after(out, n, "BREAK ok=1" as *u8, "addr=0x" as *u8, fnd)
549 let n3_set: i64 = fnd[0]
550 let n3_halt: i64 = dg_has(out, n, "STOP cmd=continue reason=halt" as *u8)
551 let n3_bp: i64 = dg_has(out, n, "STOP cmd=continue reason=breakpoint" as *u8)
552 let n3_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd)
553 dg_p(" S3 neg-control never-executed line: bp_set=" as *u8); dg_n(n3_set)
554 dg_p(" addr=" as *u8); dg_n(n3_addr); dg_p(" expected=" as *u8); dg_n(addr_never)
555 dg_p(" stopped_on_halt=" as *u8); dg_n(n3_halt)
556 dg_p(" stopped_on_breakpoint=" as *u8); dg_n(n3_bp)
557 dg_p(" exit_code=" as *u8); dg_n(n3_exit); dg_p("\n" as *u8)
558 // the breakpoint must be REAL (resolved and armed) or the neg-control proves nothing
559 var n3_real: i64 = 0
560 if n3_set == 1 { if n3_addr == addr_never { n3_real = 1 } }
561 gv_check("neg-control-is-armed-the-never-executed-line-DID-resolve-and-a-breakpoint-was-really-set-there" as *u8, n3_real, ctr)
562 var n3ok: i64 = 0
563 if n3_bp == 0 { if n3_halt == 1 { n3ok = 1 } }
564 gv_check("neg-control-a-breakpoint-on-a-never-executed-line-does-NOT-stop-and-the-guest-runs-to-its-normal-halt" as *u8, n3ok, ctr)
565
566 // ---- SESSION 4: NEG-CONTROL, a bogus file:line. It must be refused BY NAME. A breakpoint that
567 // quietly did not exist reads exactly like code that never ran, which is the failure this whole
568 // rung exists to make impossible.
569 words[5] = dg_word(DG_LINE_BOGUS) as i64
570 rc = dg_session(elf, words, 8, "/tmp/nx_dbg_step_gate/s4.log" as *u8, out, olen)
571 n = olen[0]
572 let n4_named: i64 = dg_has(out, n, "BREAK REFUSED-NO-SUCH-LINE" as *u8)
573 let n4_silent: i64 = dg_has(out, n, "BREAK ok=1" as *u8)
574 dg_p(" S4 neg-control bogus line " as *u8); dg_n(DG_LINE_BOGUS)
575 dg_p(": refused_by_name=" as *u8); dg_n(n4_named)
576 dg_p(" silently_accepted=" as *u8); dg_n(n4_silent)
577 dg_p(" exit_rc=" as *u8); dg_n(rc); dg_p("\n" as *u8)
578 var n4ok: i64 = 0
579 if n4_named == 1 { if n4_silent == 0 { n4ok = 1 } }
580 gv_check("neg-control-a-bogus-file-line-is-REFUSED-BY-NAME-never-silently-ignored" as *u8, n4ok, ctr)
581 var n4rc: i64 = 0
582 if rc != 0 { n4rc = 1 }
583 gv_check("neg-control-that-refusal-also-carries-a-NONZERO-exit-code-so-a-script-cannot-miss-it" as *u8, n4rc, ctr)
584
585 // ---- SESSION 5: DETACH RUNS EVEN WHEN THE SCRIPT NEVER ASKED FOR IT. This is the always-detach
586 // guarantee: a caller that forgets, or a script that ends early, must still leave the subject
587 // resumed and finished -- never stopped.
588 words[0] = "run" as i64
589 words[1] = DG_IMG as i64
590 words[2] = DG_LT as i64
591 words[3] = "step" as i64
592 rc = dg_session(elf, words, 4, "/tmp/nx_dbg_step_gate/s5.log" as *u8, out, olen)
593 n = olen[0]
594 let n5_auto: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "auto=" as *u8, fnd)
595 let n5_found: i64 = fnd[0]
596 let n5_halted: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "halted=" as *u8, fnd)
597 let n5_exit: i64 = dg_dec_after(out, n, "DETACH resumed=1" as *u8, "exit_code=" as *u8, fnd)
598 dg_p(" S5 no-detach-in-script: auto=" as *u8); dg_n(n5_auto)
599 dg_p(" halted=" as *u8); dg_n(n5_halted)
600 dg_p(" exit_code=" as *u8); dg_n(n5_exit); dg_p(" expected=" as *u8); dg_n(ref.halt_code); dg_p("\n" as *u8)
601 var n5ok: i64 = 0
602 if n5_found == 1 { if n5_auto == 1 { if n5_halted == 1 { if n5_exit == ref.halt_code { n5ok = 1 } } } }
603 gv_check("the-subject-is-resumed-and-runs-to-completion-even-when-the-script-NEVER-issued-detach" as *u8, n5ok, ctr)
604
605 // ---- SESSION 6: NEG-CONTROL, NEVER-BRICK, tested by BEHAVIOUR. A pid-shaped argument must be
606 // refused: there is no path by which a running process becomes this debugger's subject. Asserted
607 // this way rather than by grepping for a word, because both this file and the organ DISCUSS
608 // ptrace, and a detector that matches its own explanation of a bug is the trap already paid for.
609 words[0] = "run" as i64
610 words[1] = "12345" as i64
611 words[2] = DG_LT as i64
612 words[3] = "continue" as i64
613 rc = dg_session(elf, words, 4, "/tmp/nx_dbg_step_gate/s6.log" as *u8, out, olen)
614 n = olen[0]
615 let n6_ref: i64 = dg_has(out, n, "DBG REFUSED reason=image-absent-or-empty" as *u8)
616 dg_p(" S6 neg-control pid-shaped subject: refused=" as *u8); dg_n(n6_ref)
617 dg_p(" rc=" as *u8); dg_n(rc); dg_p("\n" as *u8)
618 var n6ok: i64 = 0
619 if n6_ref == 1 { if rc != 0 { n6ok = 1 } }
620 gv_check("neg-control-never-brick-a-pid-shaped-argument-is-REFUSED-a-live-process-can-never-become-the-subject" as *u8, n6ok, ctr)
621
622 // ---- the source-level half of the same claim: no ptrace CALL SITE. Counted and PRINTED, so a
623 // future edit that adds one fires this rather than sliding past a boolean.
624 let ptrace_sites: i64 = gk_count(DG_SRC, "sys_ptrace(" as *u8)
625 dg_p(" source scan " as *u8); dg_p(DG_SRC)
626 dg_p(": sys_ptrace( call sites=" as *u8); dg_n(ptrace_sites); dg_p("\n" as *u8)
627 var noptrace: i64 = 0
628 if ptrace_sites == 0 { noptrace = 1 }
629 gv_check("neg-control-never-brick-the-shipped-source-contains-ZERO-ptrace-call-sites-so-no-host-process-is-reachable" as *u8, noptrace, ctr)
630
631 // ---- the budget must announce its provenance on every run, or nobody can tell a derived bound
632 // from a stale pinned one without reading the source.
633 // READ A SESSION THAT ACTUALLY ATTACHED. The first draft of this tooth asserted against session
634 // 6's buffer -- the pid-shaped refusal, which exits BEFORE any machine is built and therefore
635 // could never print a budget line. It failed for the RIGHT reason and caught itself: a tooth
636 // whose subject never reached the condition it is testing proves nothing about the condition.
637 n = gk_read("/tmp/nx_dbg_step_gate/s1.log" as *u8, out, DG_OUT_CAP - 1)
638 if n < 0 { n = 0 }
639 out[n] = 0 as u8
640 var attached: i64 = 0
641 if dg_has(out, n, "DBG image=" as *u8) == 1 { attached = 1 }
642 gv_check("provenance-tooth-precondition-the-session-being-read-really-attached-to-a-machine" as *u8, attached, ctr)
643 var prov: i64 = 0
644 if dg_has(out, n, "budget_provenance=" as *u8) == 1 { prov = 1 }
645 gv_check("every-run-announces-which-step-budget-is-in-force-and-where-it-came-from" as *u8, prov, ctr)
646
647 return gv_verdict("DBG-STEP-GATE" as *u8, ctr, "statement stepping proven on a runtime-assembled RV64 fixture over a line table emitted by nx_dwarf_line" as *u8)
648}