code wiki / (root) / nx_addr2line_lib.nx

nx_addr2line_lib.nx source

↩ module page · 392 lines · 15798 B

1// nx_addr2line_lib.nx -- the sovereign DWARF v5 .debug_line DECODER, as a SHARED library. 2// 3// WHY THIS FILE EXISTS (2026-08-07). `nx_a2l_run` was proven and useful but lived inside 4// nx_addr2line.nx, which owns `main()` -- so no gate and no other organ could ever call it. 5// A PROVEN PREDICATE TRAPPED INSIDE AN ORGAN THAT OWNS main() IS NOT A SHARED CAPABILITY, 6// IT IS A COPY WAITING TO BE MADE, AND EVERY COPY IS FREE TO DISAGREE WITH THE ONE THAT WAS 7// PROVEN. Lifted VERBATIM (no semantic edit) so the extraction is mechanically checkable; 8// nx_addr2line.nx now imports this and keeps only its CLI main. 9// 10// The decoder is deliberately INDEPENDENT of nx_dwarf_line.nx (the encoder): it repeats the 11// DWARF opcode constants rather than sharing them. That independence is the whole point -- 12// it is what makes encoder-vs-decoder a genuine TWO-SYSTEM check instead of one system 13// agreeing with itself. Do not "clean this up" by unifying the constants. 14// 15// Needs only sys_mmap. Importing nx_elf_read here would be wrong: locating the section in an 16// ELF is the CALLER's job, and nx_elf_read owns a main() of its own. 17// 18// license_tier: ORIGINAL expect_exit: 0 19import "syscalls.nx" 20const NX_MAGIC_65536: i64 = 65536 21const NX_MAGIC_16777216: i64 = 16777216 22// Same constants as nx_dwarf_line, repeated here so this module is 23// independently usable. Constant unification is a future cleanup 24// (all DWARF spec constants in one nx_dwarf_const.nx). 25const NX_A2L_DW_LNS_copy: i64 = 0x01 26const NX_A2L_DW_LNS_advance_pc: i64 = 0x02 27const NX_A2L_DW_LNS_advance_line: i64 = 0x03 28const NX_A2L_DW_LNS_set_file: i64 = 0x04 29const NX_A2L_DW_LNE_end_sequence: i64 = 0x01 30const NX_A2L_DW_LNE_set_address: i64 = 0x02 31 32struct NxA2LResult { 33 found: i64, // 1 if a row covered the address 34 file_idx: i64, 35 line: i64, 36 addr: i64, // the row's address (which is <= target_addr) 37} 38 39const NX_A2L_RESULT_BYTES: i64 = 32 40 41// uleb128 decoder. Reads from buf[off..]; updates *consumed. 42func nx_a2l_uleb(buf: *u8, off: i64, consumed: *i64) -> i64 { 43 var v: i64 = 0 44 var shift: i64 = 0 45 var i: i64 = 0 46 var go: i64 = 1 47 while go == 1 { 48 let b: i64 = buf[off + i] 49 v = v | ((b & 0x7F) << shift) 50 i = i + 1 51 if (b & 0x80) == 0 { go = 0 } 52 shift = shift + 7 53 } 54 *consumed = i 55 return v 56} 57 58// sleb128 decoder. 59func nx_a2l_sleb(buf: *u8, off: i64, consumed: *i64) -> i64 { 60 var v: i64 = 0 61 var shift: i64 = 0 62 var i: i64 = 0 63 var b: i64 = 0 64 var go: i64 = 1 65 while go == 1 { 66 b = buf[off + i] 67 v = v | ((b & 0x7F) << shift) 68 shift = shift + 7 69 i = i + 1 70 if (b & 0x80) == 0 { go = 0 } 71 } 72 // Sign-extend if the last byte's bit 6 is set. 73 if shift < 64 { 74 if (b & 0x40) != 0 { 75 v = v | ((~0) << shift) 76 } 77 } 78 *consumed = i 79 return v 80} 81 82// Decode the line program at section bytes `buf[0..len)` and find 83// the row matching `target_addr`. This implements the minimum VM 84// our nx_dwarf_line emitter uses; the full DWARF line program also 85// allows special opcodes which we'll add when the emitter does. 86// 87// Note: we treat the section as a SEQUENCE (no header decode for 88// v0.0.1). Callers should pass the raw line-program bytes after 89// any header. When elf-side header decoding lands we'll handle 90// the offset bookkeeping. 91// ---- SECTION-LEVEL ENTRY POINT (added 2026-08-07) -------------------------------- 92// 93// nx_a2l_run below runs a BARE line PROGRAM starting at buf[0]. A real .debug_line section is a 94// UNIT: a v5 header (file + directory tables) followed by that program. The two were never 95// reconciled -- nx_dwline_* emits a complete unit, this decoder consumed only a program -- so every 96// lookup against a real section returned found=0 while the file header comment claimed 97// "2. Decode the line-program header". A CAPABILITY DESCRIBED IN A COMMENT IS NOT A CAPABILITY IN 98// THE CODE. Caught by nx_dwline_roundtrip_gate on its first run. 99// 100// nx_a2l_run KEEPS its original contract (bare program) because its existing caller -- the 101// nx_addr2line self-test -- hand-builds a program with no header. Adding a field would have been a 102// silent behaviour change to a shipped entry point; this is additive instead (rule 19). 103 104func nx_a2l_u32(buf: *u8, off: i64) -> i64 { 105 var v: i64 = 0 106 v = v + (buf[off] as i64) 107 v = v + ((buf[off+1] as i64) * 256) 108 v = v + ((buf[off+2] as i64) * NX_MAGIC_65536) 109 v = v + ((buf[off+3] as i64) * NX_MAGIC_16777216) 110 return v 111} 112 113// Byte offset of the first opcode in a complete DWARF v5 unit, or -1 if this is not one. 114// FAILS CLOSED: an unrecognised version or a self-inconsistent length returns -1 rather than a 115// plausible offset, because a wrong offset makes the VM decode rubble and report it confidently. 116func nx_a2l_prog_off(buf: *u8, len: i64) -> i64 { 117 if len < 12 { return 0 - 1 } 118 let unit_len: i64 = nx_a2l_u32(buf, 0) 119 if unit_len < 4 { return 0 - 1 } 120 if unit_len + 4 > len { return 0 - 1 } 121 let ver: i64 = (buf[4] as i64) + ((buf[5] as i64) * 256) 122 if ver != 5 { return 0 - 1 } 123 // v5 order: unit_length(4) version(2) address_size(1) segment_selector_size(1) header_length(4). 124 // header_length is measured from JUST AFTER its own field, i.e. from offset 12. 125 let hlen: i64 = nx_a2l_u32(buf, 8) 126 let off: i64 = 12 + hlen 127 if off >= len { return 0 - 1 } 128 return off 129} 130 131// ---- .debug_info SUBPROGRAM LOOKUP (DDR-009 step 3, 2026-08-07) --------------------------- 132// .debug_line answers "which source line is this address". .debug_info answers "which FUNCTION am 133// I in" -- and that is what makes a backtrace name its frames. GNU addr2line -f prints exactly 134// these two facts, in that order, which is the contract being matched here. 135// 136// FORMS ARE DECODED, NOT ASSUMED. Only the two forms our emitter uses are understood 137// (DW_FORM_addr, DW_FORM_string); ANY other form makes this REFUSE (-1) rather than skip a field 138// of unknown width. Guessing a width desynchronises the whole DIE stream and would then report a 139// CONFIDENT WRONG FUNCTION NAME -- worse than no answer, and the exact class .debug_line already 140// taught us with the header offset. 141const NX_A2L_TAG_SUBPROGRAM: i64 = 0x2E 142const NX_A2L_AT_NAME: i64 = 0x03 143const NX_A2L_AT_LOW_PC: i64 = 0x11 144const NX_A2L_AT_HIGH_PC: i64 = 0x12 145const NX_A2L_FORM_ADDR: i64 = 0x01 146const NX_A2L_FORM_STRING: i64 = 0x08 147const NX_A2L_MAX_ABBR: i64 = 256 148const NX_A2L_MAX_ATTR: i64 = 2048 149 150func nx_a2l_u64le(b: *u8, off: i64) -> i64 { 151 var v: i64 = 0 152 var i: i64 = 0 153 while i < 8 { v = v | ((b[off+i] as i64) << (i * 8)); i = i + 1 } 154 return v 155} 156 157// Resolve `target` to a subprogram name. Returns 1 and fills `out` (NUL-terminated) on a hit, 158// 0 when no subprogram covers the address, -1 when the DIE stream cannot be decoded exactly. 159func nx_a2l_func_at(info: *u8, ilen: i64, abbr: *u8, alen: i64, 160 target: i64, out: *u8, ocap: i64) -> i64 { 161 if ilen < 12 { return 0 - 1 } 162 if alen <= 0 { return 0 - 1 } 163 164 // ---- abbrev table: code -> (tag, attr/form list) ---- 165 let a_code: *i64 = sys_mmap(8 * NX_A2L_MAX_ABBR) as *i64 166 let a_tag: *i64 = sys_mmap(8 * NX_A2L_MAX_ABBR) as *i64 167 let a_beg: *i64 = sys_mmap(8 * NX_A2L_MAX_ABBR) as *i64 168 let a_cnt: *i64 = sys_mmap(8 * NX_A2L_MAX_ABBR) as *i64 169 let at_a: *i64 = sys_mmap(8 * NX_A2L_MAX_ATTR) as *i64 170 let at_f: *i64 = sys_mmap(8 * NX_A2L_MAX_ATTR) as *i64 171 var n_ab: i64 = 0 172 var n_at: i64 = 0 173 let cons: *i64 = sys_mmap(16) as *i64 174 var p: i64 = 0 175 var go: i64 = 1 176 while go == 1 { 177 if p >= alen { go = 0 } else { 178 *cons = 0 179 let code: i64 = nx_a2l_uleb(abbr, p, cons) 180 p = p + *cons 181 if code == 0 { go = 0 } else { 182 if n_ab >= NX_A2L_MAX_ABBR { return 0 - 1 } 183 *cons = 0 184 let tag: i64 = nx_a2l_uleb(abbr, p, cons) 185 p = p + *cons 186 p = p + 1 // has_children 187 a_code[n_ab] = code 188 a_tag[n_ab] = tag 189 a_beg[n_ab] = n_at 190 var na: i64 = 0 191 var inner: i64 = 1 192 while inner == 1 { 193 *cons = 0 194 let att: i64 = nx_a2l_uleb(abbr, p, cons) 195 p = p + *cons 196 *cons = 0 197 let frm: i64 = nx_a2l_uleb(abbr, p, cons) 198 p = p + *cons 199 if att == 0 { inner = 0 } else { 200 if n_at >= NX_A2L_MAX_ATTR { return 0 - 1 } 201 at_a[n_at] = att 202 at_f[n_at] = frm 203 n_at = n_at + 1 204 na = na + 1 205 } 206 } 207 a_cnt[n_ab] = na 208 n_ab = n_ab + 1 209 } 210 } 211 } 212 213 // ---- walk the DIEs ---- 214 var q: i64 = 12 // past the v5 CU header 215 var walking: i64 = 1 216 while walking == 1 { 217 if q >= ilen { walking = 0 } else { 218 *cons = 0 219 let code: i64 = nx_a2l_uleb(info, q, cons) 220 q = q + *cons 221 if code == 0 { 222 // null DIE: end of a sibling chain. Keep going -- there may be more. 223 } else { 224 var ai: i64 = 0 - 1 225 var k: i64 = 0 226 while k < n_ab { if a_code[k] == code { ai = k; k = n_ab } else { k = k + 1 } } 227 if ai < 0 { return 0 - 1 } 228 var lo: i64 = 0 229 var hi: i64 = 0 230 var nm: i64 = 0 - 1 231 var j: i64 = 0 232 while j < a_cnt[ai] { 233 let att: i64 = at_a[a_beg[ai] + j] 234 let frm: i64 = at_f[a_beg[ai] + j] 235 if frm == NX_A2L_FORM_ADDR { 236 let v: i64 = nx_a2l_u64le(info, q) 237 if att == NX_A2L_AT_LOW_PC { lo = v } 238 if att == NX_A2L_AT_HIGH_PC { hi = v } 239 q = q + 8 240 } else { 241 if frm == NX_A2L_FORM_STRING { 242 if att == NX_A2L_AT_NAME { nm = q } 243 while q < ilen { if info[q] == (0 as u8) { break } q = q + 1 } 244 q = q + 1 245 } else { 246 return 0 - 1 // unknown width: REFUSE, never skip blind 247 } 248 } 249 j = j + 1 250 } 251 if a_tag[ai] == NX_A2L_TAG_SUBPROGRAM { 252 if nm >= 0 { 253 if target >= lo { 254 if target < hi { 255 var c: i64 = 0 256 while c + 1 < ocap { 257 if info[nm + c] == (0 as u8) { break } 258 out[c] = info[nm + c] 259 c = c + 1 260 } 261 out[c] = 0 as u8 262 return 1 263 } 264 } 265 } 266 } 267 } 268 } 269 } 270 return 0 271} 272 273func nx_a2l_run_section(buf: *u8, len: i64, target_addr: i64) -> *NxA2LResult { 274 let off: i64 = nx_a2l_prog_off(buf, len) 275 if off < 0 { 276 let raw: *u8 = sys_mmap(NX_A2L_RESULT_BYTES) 277 let r: *NxA2LResult = raw as *NxA2LResult 278 r.found = 0 279 r.file_idx = 1 280 r.line = 1 281 r.addr = 0 282 return r 283 } 284 return nx_a2l_run(((buf as i64) + off) as *u8, len - off, target_addr) 285} 286 287func nx_a2l_run(buf: *u8, len: i64, target_addr: i64) -> *NxA2LResult { 288 let raw: *u8 = sys_mmap(NX_A2L_RESULT_BYTES) 289 let r: *NxA2LResult = raw as *NxA2LResult 290 r.found = 0 291 r.file_idx = 1 292 r.line = 1 293 r.addr = 0 294 295 // VM state. 296 var addr: i64 = 0 297 var line: i64 = 1 298 var file_idx: i64 = 1 299 300 let consumed_raw: *u8 = sys_mmap(16) 301 let consumed: *i64 = consumed_raw as *i64 302 303 var pc: i64 = 0 304 var go: i64 = 1 305 while go == 1 { 306 if pc >= len { go = 0 } 307 if go == 1 { 308 let op: i64 = buf[pc] 309 pc = pc + 1 310 311 if op == 0 { 312 // Extended opcode: 0 + uleb len + ext_op + payload. 313 *consumed = 0 314 let ext_len: i64 = nx_a2l_uleb(buf, pc, consumed) 315 pc = pc + *consumed 316 let ext_op: i64 = buf[pc] 317 pc = pc + 1 318 if ext_op == NX_A2L_DW_LNE_end_sequence { 319 // end_sequence: VM reset for next program. We 320 // don't iterate further sequences here. 321 go = 0 322 } 323 if ext_op == NX_A2L_DW_LNE_set_address { 324 // 8-byte LE address. 325 var v: i64 = 0 326 var k: i64 = 0 327 while k < 8 { 328 v = v | (buf[pc + k] << (k * 8)) 329 k = k + 1 330 } 331 addr = v 332 pc = pc + 8 333 } 334 if ext_op != NX_A2L_DW_LNE_end_sequence { 335 if ext_op != NX_A2L_DW_LNE_set_address { 336 // Skip unknown extended op's payload. 337 pc = pc + (ext_len - 1) 338 } 339 } 340 } else { 341 if op == NX_A2L_DW_LNS_copy { 342 // Commit row. If this row is the best so far 343 // (addr <= target, but greater than last best), 344 // record it. 345 if addr <= target_addr { 346 if addr > r.addr { 347 r.found = 1 348 r.addr = addr 349 r.line = line 350 r.file_idx = file_idx 351 } 352 if r.found == 0 { 353 // First-ever row covering the target. 354 r.found = 1 355 r.addr = addr 356 r.line = line 357 r.file_idx = file_idx 358 } 359 } 360 if addr > target_addr { go = 0 } 361 } else { 362 if op == NX_A2L_DW_LNS_advance_pc { 363 *consumed = 0 364 let delta: i64 = nx_a2l_uleb(buf, pc, consumed) 365 pc = pc + *consumed 366 addr = addr + delta 367 } else { 368 if op == NX_A2L_DW_LNS_advance_line { 369 *consumed = 0 370 let delta: i64 = nx_a2l_sleb(buf, pc, consumed) 371 pc = pc + *consumed 372 line = line + delta 373 } else { 374 if op == NX_A2L_DW_LNS_set_file { 375 *consumed = 0 376 let f: i64 = nx_a2l_uleb(buf, pc, consumed) 377 pc = pc + *consumed 378 file_idx = f 379 } else { 380 // Unknown standard op: cannot safely 381 // skip without abbrev-table knowledge. 382 go = 0 383 } 384 } 385 } 386 } 387 } 388 } 389 } 390 391 return r 392}