code wiki / (root) / nx_dwline_e2e_gate.nx

nx_dwline_e2e_gate.nx source

↩ module page · 351 lines · 16721 B

1// nx_dwline_e2e_gate.nx -- COMPILER -> ASSEMBLER -> ELF -> DECODER, on the real toolchain. 2// 3// nx_dwline_roundtrip_gate proves our DWARF encoder and decoder agree ON A BUFFER. That is a real 4// two-system check but it does NOT prove the shipped pipeline produces such a buffer. This gate 5// drives the actual sovereign toolchain -- nx_cc_sovereign.elf -g, then nxasm_x86_main.elf -- reads 6// the ELF that falls out, and resolves an address through nx_addr2line_lib. 7// 8// GROUND TRUTH IS THE SOURCE FILE, NOT OUR OWN OUTPUT. runtime/nx_probe_dbgtiny.nx defines 9// dbgtiny_a on line 5 and main on line 9. GNU readelf independently resolved 0x400096 -> line 5 and 10// 0x4000AD -> line 9 on this exact probe, so asserting "e_entry resolves to line 9" is checked 11// against an external oracle and against the source, not against a number we made up. 12// 13// THE LOAD-BEARING TOOTH IS THE NEGATIVE CONTROL. Compiling the SAME probe WITHOUT -g must produce 14// an ELF with NO .debug_line at all. Without it, a pipeline that emitted debug info unconditionally 15// would pass every positive tooth here -- and it would silently regress the published "168 B minimal 16// static binary" measurement, which is a MEASURED exceed on /compare/lang. DDR-002 named that 17// tradeoff as its load-bearing risk; this is the check that holds it. 18// 19// license_tier: ORIGINAL expect_exit: 0 20import "syscalls.nx" 21import "nx_elf_read.nx" 22import "nx_addr2line_lib.nx" 23import "nx_deploy_lib.nx" 24import "nx_gate_verdict.nx" 25 26const E2E_CAP: i64 = 4194304 27const E2E_CC: *u8 = "_offc/nx_cc_sovereign.elf" as *u8 28const E2E_ASM: *u8 = "_offc/nxasm_x86_main.elf" as *u8 29const E2E_SRC: *u8 = "runtime/nx_probe_dbgtiny.nx" as *u8 30const E2E_SG: *u8 = "/tmp/nx_dwe2e_g.s" as *u8 31const E2E_EG: *u8 = "/tmp/nx_dwe2e_g.elf" as *u8 32const E2E_SN: *u8 = "/tmp/nx_dwe2e_nog.s" as *u8 33const E2E_EN: *u8 = "/tmp/nx_dwe2e_nog.elf" as *u8 34 35func e2e_streq(a: *u8, b: *u8) -> i64 { 36 var i: i64 = 0 37 while a[i] != (0 as u8) { 38 if a[i] != b[i] { return 0 } 39 i = i + 1 40 } 41 if b[i] != (0 as u8) { return 0 } 42 return 1 43} 44 45// STDOUT-ONLY capture. dep_run_capture dup3s the output fd over BOTH 1 and 2, which is right for 46// showing a tool"s output to a human and WRONG for capturing a compiler"s primary artifact: the 47// compiler correctly writes its "fn=NAME" trace to fd 2 (nx_x86_64_ctx.nx:3166), and merging the 48// streams prepended that trace to the .s, so nxasm rejected it with rc=5. The compiler was never 49// at fault; my capture helper was. A CAPTURE THAT MERGES STDERR CANNOT CAPTURE AN ARTIFACT. 50// Implemented HERE rather than added to nx_deploy_lib on purpose: that lib is one of the 17 51// measured SHADOWED names whose two copies differ, so widening it would edit a file whose other 52// copy is what actually compiles for someone else. 53func e2e_run_out(elf: *u8, args: *i64, nargs: i64, outfile: *u8) -> i64 { 54 let pid: i64 = sys_fork() 55 if pid == 0 { 56 let fd: i64 = sys_openat_wr(outfile, 0x1a4) 57 if fd >= 0 { sys_dup3(fd, 1, 0) } 58 let argv: *i64 = sys_mmap(8 * (nargs + 2)) as *i64 59 let envp: *i64 = sys_mmap(16) as *i64 60 envp[0] = 0 61 argv[0] = elf as i64 62 var i: i64 = 0 63 while i < nargs { argv[i+1] = args[i]; i = i + 1 } 64 argv[nargs+1] = 0 65 sys_execve(elf, argv, envp) 66 sys_exit(127) 67 } 68 let st: *i64 = sys_mmap(16) as *i64 69 sys_wait4(pid, st, 0) 70 if (st[0] % 128) != 0 { return 0 - 1 } 71 return (st[0] >> 8) & 0xff 72} 73 74// substring search, so the tooth asserts the CLI PRINTED the answer rather than merely exiting 0. 75func e2e_contains(hay: *u8, n: i64, needle: *u8) -> i64 { 76 var m: i64 = 0 77 while needle[m] != (0 as u8) { m = m + 1 } 78 if m == 0 { return 1 } 79 var i: i64 = 0 80 while i + m <= n { 81 var j: i64 = 0 82 var ok: i64 = 1 83 while j < m { if hay[i+j] != needle[j] { ok = 0; j = m } else { j = j + 1 } } 84 if ok == 1 { return 1 } 85 i = i + 1 86 } 87 return 0 88} 89// render v as 0x-prefixed lowercase hex into dst, NUL-terminated 90func e2e_hex(dst: *u8, v: i64) -> i64 { 91 dst[0] = 48 as u8 92 dst[1] = 120 as u8 93 var nyb: i64 = 15 94 var started: i64 = 0 95 var p: i64 = 2 96 while nyb >= 0 { 97 let d: i64 = (v >> (nyb * 4)) & 15 98 if d != 0 { started = 1 } 99 if started == 1 { 100 if d < 10 { dst[p] = (48 + d) as u8 } else { dst[p] = (87 + d) as u8 } 101 p = p + 1 102 } 103 nyb = nyb - 1 104 } 105 if started == 0 { dst[p] = 48 as u8; p = p + 1 } 106 dst[p] = 0 as u8 107 return p 108} 109 110func e2e_read(path: *u8, buf: *u8, cap: i64) -> i64 { 111 let fd: i64 = sys_openat_rd(path) 112 if fd < 0 { return 0 - 1 } 113 var tot: i64 = 0 114 while tot < cap { 115 let n: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot) 116 if n <= 0 { break } 117 tot = tot + n 118 } 119 sys_close(fd) 120 return tot 121} 122 123// Returns .debug_line SIZE (0 if absent) and writes its file offset to box[0]. 124func e2e_debug_line(buf: *u8, len: i64, box: *i64) -> i64 { 125 let h: *NxElfHeader = nx_elf_parse_header(buf, len) 126 if h.valid != 1 { return 0 } 127 if h.e_shnum <= 0 { return 0 } 128 let shstr: *NxElfShdr = nx_elf_read_shdr(buf, h, h.e_shstrndx) 129 var i: i64 = 0 130 while i < h.e_shnum { 131 let sh: *NxElfShdr = nx_elf_read_shdr(buf, h, i) 132 let nm: *u8 = nx_elf_strtab_at(buf, shstr.sh_offset, sh.sh_name) 133 if e2e_streq(".debug_line" as *u8, nm) == 1 { 134 box[0] = sh.sh_offset 135 return sh.sh_size 136 } 137 i = i + 1 138 } 139 return 0 140} 141 142func main() -> i64 { 143 let ctr: *i64 = gv_ctr() 144 gv_head("=== NX-DWLINE-E2E GATE -- sovereign compiler + assembler produce readable DWARF ===" as *u8) 145 146 // /api/gate_run executes from the nishihost root, but every toolchain path here is relative to 147 // buildroot. Without this chdir the gate would find nothing and report a VACUOUS pass. 148 sys_chdir("buildroot" as *u8) 149 150 let args: *i64 = sys_mmap(64) as *i64 151 152 // ---- with -g ---- 153 // The compiler emits assembly on STDOUT and takes exactly ONE non-flag path (nx_compile_x86.nx 154 // L104-L129: first arg not starting with - is the source, the rest are ignored). My first 155 // version passed an output path as argv[3]; it was silently DISCARDED and the .s never existed, 156 // so every downstream tooth failed for a reason that had nothing to do with DWARF. 157 // AN IGNORED ARGUMENT IS NOT AN ERROR MESSAGE -- READ THE CLI, DO NOT INFER IT FROM ITS NAME. 158 args[0] = "-g" as i64 159 args[1] = E2E_SRC as i64 160 let rc_cc_g: i64 = e2e_run_out(E2E_CC, args, 2, E2E_SG) 161 gv_check("compile-with-g-exits-0" as *u8, (rc_cc_g == 0) as i64, ctr) 162 // The capture must contain the directives, else the .s is not what we think it is. 163 let sbuf: *u8 = sys_mmap(E2E_CAP) 164 let sn: i64 = e2e_read(E2E_SG, sbuf, E2E_CAP) 165 gv_check("captured-s-is-nonempty" as *u8, (sn > 256) as i64, ctr) 166 // The .s must be PURE assembly: byte 0 is the generator comment, not a stderr trace. 167 gv_check("captured-s-is-not-polluted-by-stderr" as *u8, (sbuf[0] == (35 as u8)) as i64, ctr) 168 169 args[0] = E2E_SG as i64 170 args[1] = E2E_EG as i64 171 let rc_as_g: i64 = dep_run(E2E_ASM, args, 2) 172 gv_check("assemble-with-g-exits-0" as *u8, (rc_as_g == 0) as i64, ctr) 173 174 let gbuf: *u8 = sys_mmap(E2E_CAP) 175 let gn: i64 = e2e_read(E2E_EG, gbuf, E2E_CAP) 176 gv_check("debug-elf-is-readable" as *u8, (gn > 64) as i64, ctr) 177 178 let gbox: *i64 = sys_mmap(32) as *i64 179 let gsz: i64 = e2e_debug_line(gbuf, gn, gbox) 180 gv_check("debug-elf-carries-debug-line" as *u8, (gsz > 0) as i64, ctr) 181 182 // ---- without -g: the OPT-IN control ---- 183 args[0] = E2E_SRC as i64 184 let rc_cc_n: i64 = e2e_run_out(E2E_CC, args, 1, E2E_SN) 185 gv_check("compile-without-g-exits-0" as *u8, (rc_cc_n == 0) as i64, ctr) 186 187 args[0] = E2E_SN as i64 188 args[1] = E2E_EN as i64 189 let rc_as_n: i64 = dep_run(E2E_ASM, args, 2) 190 gv_check("assemble-without-g-exits-0" as *u8, (rc_as_n == 0) as i64, ctr) 191 192 let nbuf: *u8 = sys_mmap(E2E_CAP) 193 let nn: i64 = e2e_read(E2E_EN, nbuf, E2E_CAP) 194 let nbox: *i64 = sys_mmap(32) as *i64 195 let nsz: i64 = e2e_debug_line(nbuf, nn, nbox) 196 197 // LOAD-BEARING: debug info must be OPT-IN. A toolchain emitting it unconditionally passes every 198 // positive tooth above and regresses the published minimal-binary number. 199 gv_check("neg-control-no-g-means-no-debug-line" as *u8, (nsz == 0) as i64, ctr) 200 201 // And the -g build must actually be LARGER, which is the cost side of that tradeoff being real. 202 gv_check("g-build-is-larger-than-plain" as *u8, (gn > nn) as i64, ctr) 203 204 // ---- resolve through the section ---- 205 let h: *NxElfHeader = nx_elf_parse_header(gbuf, gn) 206 let sect: *u8 = ((gbuf as i64) + gbox[0]) as *u8 207 let re: *NxA2LResult = nx_a2l_run_section(sect, gsz, h.e_entry) 208 209 // CORRECTED EXPECTATION, 2026-08-07. I first asserted e_entry resolves to line 9, assuming the 210 // entry point was main(). IT IS NOT: e_entry is _start, the compiler-emitted stub that sets up 211 // argc/argv and calls main, and it carries NO .loc because it corresponds to no source line. 212 // found=0 there is the HONEST answer and the decoder was right; the tooth was wrong. Asserting 213 // it deliberately, because "the entry stub has no source location" is a real property worth 214 // locking in -- inventing a line for compiler-generated code is exactly the failure mode that 215 // sends a debugger to a line the programmer never wrote. 216 gv_check("entry-stub-_start-has-no-source-line" as *u8, (re.found == 0) as i64, ctr) 217 218 // Both real source lines must be REACHABLE from real addresses. Scanning the text range and 219 // collecting the distinct lines proves the table covers the program, without hardcoding an 220 // address whose value is a codegen detail that may legitimately move. 221 var saw5: i64 = 0 222 var saw9: i64 = 0 223 var sa: i64 = h.e_entry 224 let send: i64 = h.e_entry + 512 225 while sa < send { 226 let rs: *NxA2LResult = nx_a2l_run_section(sect, gsz, sa) 227 if rs.found == 1 { 228 if rs.line == 5 { saw5 = 1 } 229 if rs.line == 9 { saw9 = 1 } 230 } 231 sa = sa + 1 232 } 233 gv_check("scan-finds-source-line-5-dbgtiny_a" as *u8, (saw5 == 1) as i64, ctr) 234 gv_check("scan-finds-source-line-9-main" as *u8, (saw9 == 1) as i64, ctr) 235 236 // NEGATIVE CONTROL: address 0 precedes every row and must not resolve. 237 let rz: *NxA2LResult = nx_a2l_run_section(sect, gsz, 0) 238 gv_check("neg-control-addr-zero-unresolved" as *u8, (rz.found == 0) as i64, ctr) 239 240 // ---- THE SHIPPED CLI, not just the library underneath it ---------------------------------- 241 // nx_addr2line was a self-test wearing a tool name. Proving the LIB resolves addresses says 242 // nothing about whether the BINARY someone runs does: it has its own arg parsing, its own file 243 // read, and its OWN section finder duplicated from this gate. A FIX ONLY THE GATE EXERCISES IS 244 // NOT A SHIPPED FIX -- so drive the promoted artifact end to end. 245 // The address is COMPUTED, never hardcoded: scan for the first row reporting line 5, so a 246 // legitimate codegen shift moves the tooth with it instead of falsely failing. 247 var a5: i64 = 0 248 var sb: i64 = h.e_entry 249 while sb < send { 250 if a5 == 0 { 251 let rr: *NxA2LResult = nx_a2l_run_section(sect, gsz, sb) 252 if rr.found == 1 { if rr.line == 5 { a5 = rr.addr } } 253 } 254 sb = sb + 1 255 } 256 gv_check("scan-recovered-a-line-5-address" as *u8, (a5 > 0) as i64, ctr) 257 258 let hexs: *u8 = sys_mmap(64) 259 e2e_hex(hexs, a5) 260 let cargs: *i64 = sys_mmap(64) as *i64 261 cargs[0] = E2E_EG as i64 262 cargs[1] = hexs as i64 263 let rc_cli: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_cli.txt" as *u8) 264 gv_check("cli-nx_addr2line-exits-0" as *u8, (rc_cli == 0) as i64, ctr) 265 266 let cbuf: *u8 = sys_mmap(65536) 267 let cn: i64 = e2e_read("/tmp/nx_dwe2e_cli.txt" as *u8, cbuf, 65536) 268 gv_check("cli-reports-line-5" as *u8, ((cn > 0) & (e2e_contains(cbuf, cn, "line=5" as *u8) == 1)) as i64, ctr) 269 270 // NEGATIVE CONTROL on the CLI: the no-g ELF has no .debug_line, so it must REFUSE (rc=4) rather 271 // than print a location. A tool that invents a source line for an unstripped-but-undebugged 272 // binary is worse than one that has no debug support at all. 273 cargs[0] = E2E_EN as i64 274 cargs[1] = hexs as i64 275 let rc_cli_n: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_cli_n.txt" as *u8) 276 gv_check("neg-control-cli-refuses-elf-without-debug-line" as *u8, (rc_cli_n == 4) as i64, ctr) 277 278 // ---- DDR-009 step 3: does the CLI NAME THE FUNCTION, and the RIGHT one? --------------- 279 // A tool that always answered "dbgtiny_a" would pass a single-address check. The load-bearing 280 // part is that two DIFFERENT addresses yield two DIFFERENT, CORRECT names -- discrimination, 281 // not recall. And the no-debug ELF must still REFUSE rather than invent a name. 282 var a9: i64 = 0 283 var sc2: i64 = h.e_entry 284 while sc2 < send { 285 if a9 == 0 { 286 let r9: *NxA2LResult = nx_a2l_run_section(sect, gsz, sc2) 287 if r9.found == 1 { if r9.line == 9 { a9 = r9.addr } } 288 } 289 sc2 = sc2 + 1 290 } 291 // SUBJECT CORRECTION. Everything above deliberately exercises the BLESSED toolchain 292 // (_offc/nxasm_x86_main.elf), which predates .debug_info -- so the CLI correctly printed "??" 293 // there: an honest "no function info", not a failure. Function NAMING is a property of the NEW 294 // assembler, so it needs an ELF built BY that assembler. Testing it against the blessed output 295 // would have been the instrument-subject mismatch this estate keeps paying for. 296 let nargs2: *i64 = sys_mmap(64) as *i64 297 nargs2[0] = E2E_SG as i64 298 nargs2[1] = "/tmp/nx_dwe2e_g_new.elf" as i64 299 let rcn2: i64 = e2e_run_out("../nxasm_x86_main.elf" as *u8, nargs2, 2, "/tmp/nx_dwe2e_asm2.log" as *u8) 300 gv_check("new-assembler-produced-a-debug_info-elf" as *u8, (rcn2 == 0) as i64, ctr) 301 302 let hex9: *u8 = sys_mmap(64) 303 e2e_hex(hex9, a9) 304 cargs[0] = "/tmp/nx_dwe2e_g_new.elf" as i64 305 cargs[1] = hex9 as i64 306 let rc9: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_fn9.txt" as *u8) 307 let fb: *u8 = sys_mmap(65536) 308 let fn9: i64 = e2e_read("/tmp/nx_dwe2e_fn9.txt" as *u8, fb, 65536) 309 gv_check("cli-names-main-at-the-line-9-address" as *u8, 310 ((rc9 == 0) & (e2e_contains(fb, fn9, "main" as *u8) == 1)) as i64, ctr) 311 312 // the line-5 address must name the OTHER function -- this is the discrimination tooth. 313 // Re-run against the SAME new-assembler ELF so both names come from one subject. 314 cargs[1] = hexs as i64 315 let rc5b: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_fn5.txt" as *u8) 316 let gb: *u8 = sys_mmap(65536) 317 let fn5: i64 = e2e_read("/tmp/nx_dwe2e_fn5.txt" as *u8, gb, 65536) 318 gv_check("cli-names-dbgtiny_a-at-the-line-5-address" as *u8, 319 (e2e_contains(gb, fn5, "dbgtiny_a" as *u8) == 1) as i64, ctr) 320 321 gv_puts("\n --- diagnostics ---\n" as *u8) 322 gv_puts(" line5_addr=" as *u8); gv_num(a5) 323 gv_puts(" cli_rc=" as *u8); gv_num(rc_cli) 324 gv_puts(" cli_rc_nog=" as *u8); gv_num(rc_cli_n) 325 gv_puts(" cli_out=" as *u8) 326 var ci: i64 = 0 327 while ci < cn { if cbuf[ci] != (10 as u8) { sys_write(1, ((cbuf as i64) + ci) as *u8, 1) } ci = ci + 1 } 328 gv_puts("\n" as *u8) 329 330 gv_puts(" rc_cc_g=" as *u8); gv_num(rc_cc_g) 331 gv_puts(" rc_as_g=" as *u8); gv_num(rc_as_g) 332 gv_puts(" rc_cc_n=" as *u8); gv_num(rc_cc_n) 333 gv_puts(" rc_as_n=" as *u8); gv_num(rc_as_n) 334 gv_puts(" s_bytes=" as *u8); gv_num(sn); gv_puts("\n" as *u8) 335 gv_puts(" first 120 bytes of the captured .s ->|" as *u8) 336 var di: i64 = 0 337 while di < 120 { if di < sn { sys_write(1, ((sbuf as i64) + di) as *u8, 1) } di = di + 1 } 338 gv_puts("|<-\n" as *u8) 339 340 gv_puts("\n --- measured ---\n" as *u8) 341 gv_puts(" g_elf_bytes=" as *u8); gv_num(gn) 342 gv_puts(" nog_elf_bytes=" as *u8); gv_num(nn) 343 gv_puts(" debug_line_bytes=" as *u8); gv_num(gsz); gv_puts("\n" as *u8) 344 gv_puts(" e_entry=" as *u8); gv_num(h.e_entry) 345 gv_puts(" -> found=" as *u8); gv_num(re.found) 346 gv_puts(" line=" as *u8); gv_num(re.line) 347 gv_puts(" row_addr=" as *u8); gv_num(re.addr); gv_puts("\n" as *u8) 348 349 return gv_verdict("nx_dwline_e2e_gate" as *u8, ctr, 350 "real nx_cc_sovereign + nxasm_x86_main output, decoded by nx_addr2line_lib; line 9 corroborated by source and by GNU readelf" as *u8) 351}