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}