nx_dbg_step_gate.nx
buildroot/runtime/nx_dbg_step_gate.nx
about
nx_dbg_step_gate.nx -- the VERDICT gate for LN12 (nx_dbg_step, watch symbol dbg_step_over).
THE ACCEPT RULE (/compare/lang LN12): break and step on our own binaries over the line table we
emit. So this gate does not mock anything:
* it ASSEMBLES a real RV64 program containing a real linking CALL,
* it EMITS that program's .debug_line with nx_dwarf_line.nx -- OUR OWN DWARF v5 emitter, the one
LB5 shipped and GNU readelf validates -- so the table under test is the table we produce,
* it runs the deployed nx_dbg_step.elf against the pair and compares against expectations that
the debugger had no part in computing.
THE SUBJECT IS NEVER ITS OWN ORACLE. Every expected value comes from somewhere else:
breakpoint address = the gate's OWN fixture layout (it placed line 12 at offset 8; the debugger
has to rediscover that through the line table, and the two must agree).
callee / caller line = likewise the gate's layout.
exit code and halt = a SECOND, DEBUGGER-FREE sim (bootcap_machine) run in this process.
never-executed addr = not assumed -- the reference sim records every pc it fetches and the gate
PROVES the neg-control address is absent from that set before using it.
the call is a call = decoded arithmetically with rv64im_min_decoder (kind == JAL, rd == ra)
BEFORE any stepping outcome is asserted, so a fixture that could not
exercise step-over cannot score a pass.
WHY THE DISCRIMINATING TOOTH IS THE ONE THAT MATTERS. A debugger that single-steps INSTRUCTIONS
passes "the line changed" trivially. The only thing that separates a stepping debugger from an
instruction stepper is that STEP-OVER MUST NOT REPORT THE CALLEE'S LINE while STEP-INTO MUST. Both
are asserted here, on the same fixture, from the same starting pc -- two tests that each isolate one
signal would not prove discrimination between them.
NEVER-BRICK IS TESTED BEHAVIOURALLY, NOT BY GREPPING FOR A WORD. A pid-shaped argument must be
REFUSED (a process id can never become a subject), and the source must contain no sys_ptrace call
site. The behavioural half cannot be fooled by prose, which a source scan can be -- this file and
the organ both discuss ptrace at length, and a detector that finds its own explanation of the bug
it hunts is the trap this estate has already paid for twice.
Fixtures live in /tmp/nx_dbg_step_gate/ (created at setup, so a crashed run cannot leave a fixture
beside a production beat) and are ASSEMBLED AT RUNTIME, never checked in.
license_tier: ORIGINAL
dependencies 6 imports · 0 importers
imports: nx_syscalls.nxnx_gate_verdict.nxnx_gatekit_lib.nxnx_dwarf_line.nxnx_bootcap.nxrv64im_min_decoder.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 44 | const DG_DIR: *u8 = "/tmp/nx_dbg_step_gate" |
| 45 | const DG_IMG: *u8 = "/tmp/nx_dbg_step_gate/fixture.bin" |
| 46 | const DG_LT: *u8 = "/tmp/nx_dbg_step_gate/fixture.debug_line" |
| 47 | const DG_SRCNAME: *u8 = "dbgfix.nx" |
| 48 | const DG_SRC: *u8 = "buildroot/runtime/nx_dbg_step.nx" |
| 52 | const DG_RUN_MS: i64 = 60000 |
| 53 | const DG_POLL_MS: i64 = 25 |
| 54 | const DG_OUT_CAP: i64 = 1048576 // the gatekit capture bound; asserted not reached, never silent |
| 55 | const DG_ARGV_MAX: i64 = 16 |
| 56 | const DG_MODE_644: i64 = 420 |
| 57 | const DG_ASCII_ZERO: i64 = 48 |
| 58 | const DG_ASCII_NINE: i64 = 57 |
| 59 | const DG_ASCII_a: i64 = 97 |
| 60 | const DG_ASCII_f: i64 = 102 |
| 61 | const DG_DECIMAL: i64 = 10 |
| 62 | const DG_HEXBASE: i64 = 16 |
| 63 | const DG_PTR: i64 = 8 |
| 64 | const DG_SCRATCH: i64 = 64 |
| 65 | const DG_BYTE_MASK: i64 = 255 |
| 70 | const DG_N_INSTR: i64 = 10 |
| 71 | const DG_OFF_L10: i64 = 0 // addi a0, x0, 7 |
| 72 | const DG_OFF_L11: i64 = 4 // addi a1, x0, 2 |
| 73 | const DG_OFF_CALL: i64 = 8 // jal ra, +24 <- THE CALL |
| 74 | const DG_OFF_AFTER: i64 = 12 // addi a2, x0, 3 <- where a step-OVER must land |
| 75 | const DG_OFF_SETRC: i64 = 16 // addi a0, x0, 5 |
| 76 | const DG_OFF_HALT: i64 = 20 // ebreak <- halt_code = a0 |
| 77 | const DG_OFF_NEVER: i64 = 24 // addi a3, x0, 9 <- NEVER executed (neg-control) |
| 78 | const DG_OFF_NEVER2: i64 = 28 // ebreak <- NEVER executed |
| 79 | const DG_OFF_CALLEE: i64 = 32 // addi a4, x0, 4 <- where a step-INTO must land |
| 80 | const DG_OFF_CALLRET: i64 = 36 // jalr x0, 0(ra) |
| 81 | const DG_IMG_BYTES: i64 = 40 |
| 83 | const DG_LINE_L10: i64 = 10 |
| 84 | const DG_LINE_L11: i64 = 11 |
| 85 | const DG_LINE_CALL: i64 = 12 |
| 86 | const DG_LINE_AFTER: i64 = 13 |
| 87 | const DG_LINE_SETRC: i64 = 14 |
| 88 | const DG_LINE_HALT: i64 = 15 |
| 89 | const DG_LINE_NEVER: i64 = 20 |
| 90 | const DG_LINE_NEVER2: i64 = 21 |
| 91 | const DG_LINE_CALLEE: i64 = 100 |
| 92 | const DG_LINE_CALLRET: i64 = 101 |
| 93 | const DG_LINE_BOGUS: i64 = 999 // present in no row: the resolver must refuse it BY NAME |
| 94 | const DG_END_OFF: i64 = 40 // end_sequence address = one past the last instruction |
| 99 | const DG_OPC_OP_IMM: i64 = 0x13 |
| 100 | const DG_OPC_JAL: i64 = 0x6F |
| 101 | const DG_OPC_JALR: i64 = 0x67 |
| 102 | const DG_EBREAK: i64 = 0x00100073 |
| 103 | const DG_REG_RA: i64 = 1 |
| 104 | const DG_REG_A0: i64 = 10 |
| 105 | const DG_REG_A1: i64 = 11 |
| 106 | const DG_REG_A2: i64 = 12 |
| 107 | const DG_REG_A3: i64 = 13 |
| 108 | const DG_REG_A4: i64 = 14 |
| 109 | const DG_IMM12_MASK: i64 = 4095 |
| 110 | const DG_EXIT_RC: i64 = 5 // what the fixture puts in a0 before ebreak |
| 111 | const DG_A0_INITIAL: i64 = 7 // deliberately different, so a wrong exit code cannot look right |
functions
| 113 | func dg_addi(rd: i64, rs1: i64, imm: i64) -> i64 called by 1: main |
| 116 | func dg_jal(rd: i64, off: i64) -> i64 called by 1: main |
| 123 | func dg_jalr(rd: i64, rs1: i64, imm: i64) -> i64 called by 1: main |
| 126 | func dg_put32(b: *u8, off: i64, w: i64) -> i64 called by 1: main |
| 131 | func dg_get32(b: *u8, off: i64) -> i64 called by 1: main |
| 139 | func dg_p(s: *u8) -> i64 { return gv_puts(s) } |
| 140 | func dg_n(v: i64) -> i64 { return gv_num(v) } |
| 142 | func dg_write_bin(path: *u8, buf: *u8, n: i64) -> i64 |
| 152 | func dg_spawn(elf: *u8, words: *i64, nwords: i64, logpath: *u8) -> i64 |
| 171 | func dg_reap(pid: i64, budget_ms: i64) -> i64 |
| 188 | func dg_session(elf: *u8, words: *i64, nwords: i64, log: *u8, outbuf: *u8, outlen: *i64) -> i64 |
| 202 | func dg_find(buf: *u8, n: i64, from: i64, needle: *u8) -> i64 |
| 216 | func dg_dec_after(buf: *u8, n: i64, anchor: *u8, field: *u8, found: *i64) -> i64 |
| 243 | func dg_hex_after(buf: *u8, n: i64, anchor: *u8, field: *u8, found: *i64) -> i64 |
| 272 | func dg_has(buf: *u8, n: i64, needle: *u8) -> i64 |
| 278 | func dg_word(v: i64) -> *u8 |
| 291 | func main() -> i64 |