code wiki / (root) / nx_addr2line.nx

nx_addr2line.nx source

↩ module page · 283 lines · 10929 B

1// nx_addr2line.nx -- sovereign addr2line. 2// 3// Given an ELF + a code address, walk the DWARF .debug_line program 4// and return the source (file_idx, line) at that address. Foundation 5// for the future nx_dbg.nx debugger ("show me the source line at PC") 6// + sovereign nx_panic backtraces with file:line locations. 7// 8// What we do (DWARF v5 ยง6.2 line-program VM): 9// 10// 1. Locate .debug_line section via nx_elf_read. 11// 2. Decode the line-program header (file table + dir table). 12// 3. Run the line-program VM until address >= target_addr or end. 13// 4. Return the (file_idx, line) of the most recent committed row 14// whose address <= target. 15// 16// Pairs with nx_dwarf_line.nx which BUILDS the section; together, 17// produce + consume the same byte format with our own code only. 18// 19// v0.0.1 scope: 20// * Decode the standard opcodes our nx_dwarf_line emits: 21// DW_LNS_copy, DW_LNS_advance_pc, DW_LNS_advance_line, 22// DW_LNS_set_file 23// * Decode extended ops: 24// DW_LNE_end_sequence, DW_LNE_set_address 25// 26// Special opcodes (the compact bias-encoded form) NOT yet decoded; 27// our emitter doesn't use them today. Add when the emitter does. 28 29// nx_safety_envelope: 30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 31// sil_target: SIL1 32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 33// verdict: NOT_YET_EVALUATED 34 35import "syscalls.nx" 36import "nx_elf_read.nx" 37import "nx_dwarf_line.nx" 38 39// DECODER MOVED OUT 2026-08-07 -> nx_addr2line_lib.nx, so gates and other organs can call it. 40// Nothing here changed semantically; this file is now the CLI front end and its self-test. 41import "nx_addr2line_lib.nx" 42 43// ---- self-test --------------------------------------------------- 44 45// ---- CLI HELPERS (2026-08-07) ------------------------------------------------------------- 46// This organ was named nx_addr2line and shipped a SELF-TEST ONLY: its main hand-built a line 47// program at 0x10000 and never opened an ELF, while its own header comment described 48// "1. Locate .debug_line section via nx_elf_read" -- it imported nx_elf_read and never called it. 49// A TOOL THAT CANNOT DO THE THING ITS NAME PROMISES IS AN UNRUNNABLE CAPABILITY SCORING AS A 50// BUILT ONE. The self-test is PRESERVED VERBATIM as a_2l_selftest and still runs with no args, so 51// nothing is removed; resolving a real ELF is added alongside it. 52 53func a2l_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 54func a2l_num(v: i64) -> i64 { 55 var m: i64 = v 56 if m < 0 { a2l_puts("-" as *u8); m = 0 - m } 57 let t: *u8 = sys_mmap(32) 58 var k: i64 = 0 59 if m == 0 { t[0] = 48 as u8; k = 1 } 60 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 61 let o: *u8 = sys_mmap(32) 62 var i: i64 = 0 63 while i < k { o[i] = t[k-1-i]; i = i + 1 } 64 sys_write(1, o, k) 65 return 0 66} 67func a2l_streq(a: *u8, b: *u8) -> i64 { 68 var i: i64 = 0 69 while a[i] != (0 as u8) { 70 if a[i] != b[i] { return 0 } 71 i = i + 1 72 } 73 if b[i] != (0 as u8) { return 0 } 74 return 1 75} 76// Accepts 0x-prefixed or bare hex. Returns -1 on any non-hex byte rather than a partial value: 77// a silently truncated address would resolve to a confident WRONG line. 78func a2l_parse_hex(s: *u8) -> i64 { 79 var i: i64 = 0 80 if s[0] == (48 as u8) { if s[1] == (120 as u8) { i = 2 } } 81 var v: i64 = 0 82 var any: i64 = 0 83 while s[i] != (0 as u8) { 84 let c: i64 = s[i] as i64 85 var d: i64 = 0 - 1 86 if c >= 48 { if c <= 57 { d = c - 48 } } 87 if c >= 97 { if c <= 102 { d = c - 87 } } 88 if c >= 65 { if c <= 70 { d = c - 55 } } 89 if d < 0 { return 0 - 1 } 90 v = v * 16 + d 91 any = 1 92 i = i + 1 93 } 94 if any == 0 { return 0 - 1 } 95 return v 96} 97func a2l_read_file(path: *u8, buf: *u8, cap: i64) -> i64 { 98 let fd: i64 = sys_openat_rd(path) 99 if fd < 0 { return 0 - 1 } 100 var tot: i64 = 0 101 while tot < cap { 102 let n: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot) 103 if n <= 0 { break } 104 tot = tot + n 105 } 106 sys_close(fd) 107 return tot 108} 109// Locate .debug_line by walking the section header table. Kept HERE and not in nx_addr2line_lib 110// on purpose: the lib needs only sys_mmap, and dragging nx_elf_read into it would price it out of 111// the gates that exist to prove it. 112// Generic section lookup by name -- .debug_line, .debug_info, .debug_abbrev all need it, and 113// three copies of one walk is how they drift apart. 114func a2l_find_section(buf: *u8, len: i64, want: *u8, outbox: *i64) -> i64 { 115 let h: *NxElfHeader = nx_elf_parse_header(buf, len) 116 if h.valid != 1 { return 0 } 117 if h.e_shnum <= 0 { return 0 } 118 let shstr: *NxElfShdr = nx_elf_read_shdr(buf, h, h.e_shstrndx) 119 var i: i64 = 0 120 while i < h.e_shnum { 121 let sh: *NxElfShdr = nx_elf_read_shdr(buf, h, i) 122 let nm: *u8 = nx_elf_strtab_at(buf, shstr.sh_offset, sh.sh_name) 123 if a2l_streq(want, nm) == 1 { 124 outbox[0] = sh.sh_offset 125 outbox[1] = sh.sh_size 126 return 1 127 } 128 i = i + 1 129 } 130 return 0 131} 132 133func a2l_find_debug_line(buf: *u8, len: i64, outbox: *i64) -> i64 { 134 let h: *NxElfHeader = nx_elf_parse_header(buf, len) 135 if h.valid != 1 { return 0 } 136 if h.e_shnum <= 0 { return 0 } 137 let shstr: *NxElfShdr = nx_elf_read_shdr(buf, h, h.e_shstrndx) 138 var i: i64 = 0 139 while i < h.e_shnum { 140 let sh: *NxElfShdr = nx_elf_read_shdr(buf, h, i) 141 let nm: *u8 = nx_elf_strtab_at(buf, shstr.sh_offset, sh.sh_name) 142 if a2l_streq(".debug_line" as *u8, nm) == 1 { 143 outbox[0] = sh.sh_offset 144 outbox[1] = sh.sh_size 145 return 1 146 } 147 i = i + 1 148 } 149 return 0 150} 151 152func a2l_selftest() -> i64 { 153 // Build a tiny line-program by hand: 154 // set_address 0x10000 155 // copy -> row(addr=0x10000, line=1) 156 // advance_pc 4 157 // advance_line +2 -> line = 3 158 // copy -> row(addr=0x10004, line=3) 159 // advance_pc 8 160 // advance_line +1 -> line = 4 161 // copy -> row(addr=0x1000C, line=4) 162 // end_sequence 163 let buf: *u8 = sys_mmap(64) 164 var p: i64 = 0 165 // set_address 0x10000 166 buf[p] = 0; p = p + 1 167 buf[p] = 9; p = p + 1 168 buf[p] = NX_A2L_DW_LNE_set_address; p = p + 1 169 buf[p] = 0x00; p = p + 1 170 buf[p] = 0x00; p = p + 1 171 buf[p] = 0x01; p = p + 1 172 buf[p] = 0x00; p = p + 1 173 buf[p] = 0x00; p = p + 1 174 buf[p] = 0x00; p = p + 1 175 buf[p] = 0x00; p = p + 1 176 buf[p] = 0x00; p = p + 1 177 // copy 178 buf[p] = NX_A2L_DW_LNS_copy; p = p + 1 179 // advance_pc 4 (uleb128 = 0x04) 180 buf[p] = NX_A2L_DW_LNS_advance_pc; p = p + 1 181 buf[p] = 4; p = p + 1 182 // advance_line +2 (sleb128 = 0x02) 183 buf[p] = NX_A2L_DW_LNS_advance_line; p = p + 1 184 buf[p] = 2; p = p + 1 185 // copy 186 buf[p] = NX_A2L_DW_LNS_copy; p = p + 1 187 // advance_pc 8 188 buf[p] = NX_A2L_DW_LNS_advance_pc; p = p + 1 189 buf[p] = 8; p = p + 1 190 // advance_line +1 191 buf[p] = NX_A2L_DW_LNS_advance_line; p = p + 1 192 buf[p] = 1; p = p + 1 193 // copy 194 buf[p] = NX_A2L_DW_LNS_copy; p = p + 1 195 // end_sequence 196 buf[p] = 0; p = p + 1 197 buf[p] = 1; p = p + 1 198 buf[p] = NX_A2L_DW_LNE_end_sequence; p = p + 1 199 200 // Lookup at 0x10004 -- should hit row 2 (line 3). 201 let r1: *NxA2LResult = nx_a2l_run(buf, p, 0x10004) 202 if r1.found != 1 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 203 if r1.line != 3 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 204 if r1.addr != 0x10004 { return __syscall(93, 12, 0, 0, 0, 0, 0) } 205 206 // Lookup at 0x10006 (between rows) -- should still resolve to 207 // row 2 (the last committed addr <= target). 208 let r2: *NxA2LResult = nx_a2l_run(buf, p, 0x10006) 209 if r2.found != 1 { return __syscall(93, 20, 0, 0, 0, 0, 0) } 210 if r2.line != 3 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 211 212 // Lookup at 0x10000 (the very first row). 213 let r3: *NxA2LResult = nx_a2l_run(buf, p, 0x10000) 214 if r3.found != 1 { return __syscall(93, 30, 0, 0, 0, 0, 0) } 215 if r3.line != 1 { return __syscall(93, 31, 0, 0, 0, 0, 0) } 216 217 // Lookup before any row -- not found. 218 let r4: *NxA2LResult = nx_a2l_run(buf, p, 0x100) 219 if r4.found != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) } 220 221 a2l_puts("nx_addr2line selftest OK (bare line-program VM)\n" as *u8) 222 return 0 223} 224 225const A2L_CAP: i64 = 4194304 226 227// nx_addr2line <elf> <hex-addr> -- resolve a runtime PC to file-index:line 228// no args -- run the original self-test 229func main(argc: i64, argv: *i64) -> i64 { 230 if argc < 3 { return a2l_selftest() } 231 232 let path: *u8 = argv[1] as *u8 233 let addr: i64 = a2l_parse_hex(argv[2] as *u8) 234 if addr < 0 { 235 a2l_puts("nx_addr2line: second argument must be a hex address, e.g. 0x400096\n" as *u8) 236 return 2 237 } 238 239 let buf: *u8 = sys_mmap(A2L_CAP) 240 let n: i64 = a2l_read_file(path, buf, A2L_CAP) 241 if n <= 0 { 242 a2l_puts("nx_addr2line: cannot read " as *u8); a2l_puts(path); a2l_puts("\n" as *u8) 243 return 3 244 } 245 246 let box: *i64 = sys_mmap(32) as *i64 247 if a2l_find_debug_line(buf, n, box) != 1 { 248 // REFUSE, do not guess. An ELF with no .debug_line was compiled without -g; answering 249 // anything but "no information" would invent a source location. 250 a2l_puts("nx_addr2line: no .debug_line section (built without -g)\n" as *u8) 251 return 4 252 } 253 254 let sect: *u8 = ((buf as i64) + box[0]) as *u8 255 let res: *NxA2LResult = nx_a2l_run_section(sect, box[1], addr) 256 if res.found != 1 { 257 a2l_puts("nx_addr2line: no line-table row covers that address\n" as *u8) 258 return 5 259 } 260 261 // FUNCTION NAME FIRST, then the location -- the shape GNU addr2line -f uses, so a reader 262 // (human or script) that already knows that tool needs no new habits. Emitted only when 263 // .debug_info is present and actually covers the address: a missing name is REPORTED AS 264 // MISSING, never filled in with a neighbouring symbol. 265 let ibox: *i64 = sys_mmap(32) as *i64 266 let abox: *i64 = sys_mmap(32) as *i64 267 let fname: *u8 = sys_mmap(512) 268 var havefn: i64 = 0 269 if a2l_find_section(buf, n, ".debug_info" as *u8, ibox) == 1 { 270 if a2l_find_section(buf, n, ".debug_abbrev" as *u8, abox) == 1 { 271 let ip: *u8 = ((buf as i64) + ibox[0]) as *u8 272 let ap: *u8 = ((buf as i64) + abox[0]) as *u8 273 if nx_a2l_func_at(ip, ibox[1], ap, abox[1], addr, fname, 512) == 1 { havefn = 1 } 274 } 275 } 276 if havefn == 1 { a2l_puts(fname) } else { a2l_puts("??" as *u8) } 277 a2l_puts("\n" as *u8) 278 a2l_puts("file_index=" as *u8); a2l_num(res.file_idx) 279 a2l_puts(" line=" as *u8); a2l_num(res.line) 280 a2l_puts(" row_addr=" as *u8); a2l_num(res.addr) 281 a2l_puts("\n" as *u8) 282 return 0 283}