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}