nx_dwarf_line.nx source
↩ module page · 416 lines · 16189 B
1// nx_dwarf_line.nx -- DWARF v5 .debug_line section builder.
2//
3// Maps machine-instruction addresses back to source-file line + col.
4// Enables `gdb` (and our future sovereign debugger) to show the .nx
5// source line when stopped at any RV64 PC.
6//
7// DWARF v5 line program (DWARF 5 spec §6.2):
8//
9// .debug_line consists of one "line number program" per compilation
10// unit. Header (~30 bytes), then a sequence of standard + extended
11// opcodes that build a "line table" via a small VM:
12//
13// state: address (u64), op_index, file, line, column,
14// is_stmt, basic_block, end_sequence, prologue_end,
15// epilogue_begin, isa, discriminator
16//
17// Standard opcodes drive the VM:
18// DW_LNS_copy commit current row
19// DW_LNS_advance_pc += operand uleb128
20// DW_LNS_advance_line += signed leb
21// DW_LNS_set_file := operand
22// DW_LNS_set_column := operand
23// ... etc
24//
25// Why now (R2 capability lever):
26//
27// * Managed platforms (V8/WASM) can't give us proper PC-to-source
28// debugging at the silicon level. We can.
29// * Pairs with nx_dbg.nx (future) for full sovereign debugging:
30// read .debug_line, decode VM, print source location at faults.
31// * F6-deterministic: same source -> same .debug_line bytes.
32//
33// What we ship here (v0.0.1):
34//
35// * Builder for the .debug_line bytes (header + line program).
36// * uleb128 / sleb128 encoders (used widely in DWARF).
37// * Helper to register (address, file, line) tuples and finalize.
38//
39// What we DEFER (separate commits):
40//
41// * .debug_info builder (DIE forest -- compilation units, types,
42// functions, variables). ~500 LoC; lands when nxc2 grows the
43// -g flag.
44// * .debug_str + .debug_str_offsets builders. Tied to nx_strtab.
45// * .debug_abbrev builder (abbreviation table for DIEs).
46// * Reader/decoder for sovereign debugger.
47
48// nx_safety_envelope:
49// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
50// sil_target: SIL1
51// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
52// verdict: NOT_YET_EVALUATED
53
54import "syscalls.nx"
55
56// ---- VM standard opcodes (DWARF 5 §6.2.5.2) ----------------------
57
58const DW_LNS_copy: i64 = 0x01
59const DW_LNS_advance_pc: i64 = 0x02
60const DW_LNS_advance_line: i64 = 0x03
61const DW_LNS_set_file: i64 = 0x04
62const DW_LNS_set_column: i64 = 0x05
63const DW_LNS_negate_stmt: i64 = 0x06
64const DW_LNS_set_basic_block: i64 = 0x07
65const DW_LNS_const_add_pc: i64 = 0x08
66const DW_LNS_fixed_advance_pc: i64 = 0x09
67const DW_LNS_set_prologue_end: i64 = 0x0A
68const DW_LNS_set_epilogue_begin: i64 = 0x0B
69const DW_LNS_set_isa: i64 = 0x0C
70
71// Extended opcodes (preceded by 0 + uleb-length).
72const DW_LNE_end_sequence: i64 = 0x01
73const DW_LNE_set_address: i64 = 0x02
74const DW_LNE_set_discriminator: i64 = 0x04
75
76// Special-opcode tuning constants. Keep simple defaults; line_base
77// = -5 / line_range = 14 / opcode_base = 13 are common across LLVM
78// + GCC.
79const NX_DW_LINE_BASE: i64 = -5
80const NX_DW_LINE_RANGE: i64 = 14
81const NX_DW_OPCODE_BASE: i64 = 13
82const NX_DW_MIN_INST_LEN: i64 = 1
83const NX_DW_MAX_OPS_PER_INST: i64 = 1
84const NX_DW_DEFAULT_IS_STMT: i64 = 1
85
86// ---- builder context ---------------------------------------------
87
88struct NxDwLine {
89 buf: *u8,
90 cap: i64,
91 used: i64,
92 cur_addr: i64, // VM state: current address
93 cur_line: i64, // VM state: current line
94 cur_file: i64, // VM state: current file index (1-based)
95}
96
97const NX_DWLINE_BYTES: i64 = 48
98
99func nx_dwline_new(cap: i64) -> *NxDwLine {
100 let raw: *u8 = sys_mmap(NX_DWLINE_BYTES)
101 let d: *NxDwLine = raw as *NxDwLine
102 d.buf = sys_mmap(cap)
103 d.cap = cap
104 d.used = 0
105 d.cur_addr = 0
106 d.cur_line = 1 // DWARF lines are 1-based
107 d.cur_file = 1 // first file
108 return d
109}
110
111// ---- byte append ---------------------------------------------------
112
113func nx_dwline_byte(d: *NxDwLine, v: i64) -> i64 {
114 if d.used >= d.cap { return -1 }
115 d.buf[d.used] = v & 0xFF
116 d.used = d.used + 1
117 return 0
118}
119
120// uleb128 encoder.
121func nx_dwline_uleb(d: *NxDwLine, v: i64) -> i64 {
122 var x: i64 = v
123 var go: i64 = 1
124 while go == 1 {
125 var b: i64 = x & 0x7F
126 x = x >> 7
127 if x != 0 { b = b | 0x80 }
128 if nx_dwline_byte(d, b) < 0 { return -1 }
129 if x == 0 { go = 0 }
130 }
131 return 0
132}
133
134// sleb128 encoder. Two's-complement with sign-extension stop rule.
135func nx_dwline_sleb(d: *NxDwLine, v: i64) -> i64 {
136 var x: i64 = v
137 var go: i64 = 1
138 while go == 1 {
139 var b: i64 = x & 0x7F
140 // Arithmetic shift: preserves sign for negatives.
141 x = x >> 7
142 let sign_bit: i64 = b & 0x40
143 let done: i64 = ((x == 0) & (sign_bit == 0)) | ((x == -1) & (sign_bit != 0))
144 if done == 0 { b = b | 0x80 }
145 if nx_dwline_byte(d, b) < 0 { return -1 }
146 if done != 0 { go = 0 }
147 }
148 return 0
149}
150
151// ---- VM ops ------------------------------------------------------
152
153// DW_LNS_advance_pc: opcode + uleb operand
154func nx_dwline_advance_pc(d: *NxDwLine, delta: i64) -> i64 {
155 if delta < 0 { return -1 }
156 nx_dwline_byte(d, DW_LNS_advance_pc)
157 nx_dwline_uleb(d, delta / NX_DW_MIN_INST_LEN)
158 d.cur_addr = d.cur_addr + delta
159 return 0
160}
161
162// DW_LNS_advance_line: opcode + sleb operand
163func nx_dwline_advance_line(d: *NxDwLine, delta: i64) -> i64 {
164 nx_dwline_byte(d, DW_LNS_advance_line)
165 nx_dwline_sleb(d, delta)
166 d.cur_line = d.cur_line + delta
167 return 0
168}
169
170// DW_LNS_set_file: opcode + uleb file index
171func nx_dwline_set_file(d: *NxDwLine, file_idx: i64) -> i64 {
172 nx_dwline_byte(d, DW_LNS_set_file)
173 nx_dwline_uleb(d, file_idx)
174 d.cur_file = file_idx
175 return 0
176}
177
178// DW_LNS_copy: commit current row
179func nx_dwline_copy(d: *NxDwLine) -> i64 {
180 return nx_dwline_byte(d, DW_LNS_copy)
181}
182
183// Extended op: end of sequence (closes the program).
184func nx_dwline_end_sequence(d: *NxDwLine) -> i64 {
185 // 0x00 (extended marker), uleb128(1) (length), DW_LNE_end_sequence
186 nx_dwline_byte(d, 0)
187 nx_dwline_byte(d, 1)
188 nx_dwline_byte(d, DW_LNE_end_sequence)
189 return 0
190}
191
192// Extended op: set address (8 bytes for 64-bit).
193func nx_dwline_set_address(d: *NxDwLine, addr: i64) -> i64 {
194 nx_dwline_byte(d, 0)
195 nx_dwline_byte(d, 9) // length: opcode + 8 bytes
196 nx_dwline_byte(d, DW_LNE_set_address)
197 nx_dwline_byte(d, (addr ) & 0xFF)
198 nx_dwline_byte(d, (addr >> 8) & 0xFF)
199 nx_dwline_byte(d, (addr >> 16) & 0xFF)
200 nx_dwline_byte(d, (addr >> 24) & 0xFF)
201 nx_dwline_byte(d, (addr >> 32) & 0xFF)
202 nx_dwline_byte(d, (addr >> 40) & 0xFF)
203 nx_dwline_byte(d, (addr >> 48) & 0xFF)
204 nx_dwline_byte(d, (addr >> 56) & 0xFF)
205 d.cur_addr = addr
206 return 0
207}
208
209// ---- high-level: register one (addr, file, line) row -------------
210//
211// Idiomatic usage: walk emitted instructions and call this for each
212// statement boundary. The builder advances PC + line via the VM,
213// then DW_LNS_copy commits.
214
215func nx_dwline_record(d: *NxDwLine, addr: i64, file_idx: i64, line: i64) -> i64 {
216 if file_idx != d.cur_file {
217 nx_dwline_set_file(d, file_idx)
218 }
219 let pc_delta: i64 = addr - d.cur_addr
220 if pc_delta < 0 {
221 // Backward jump (e.g. inlining): emit DW_LNE_set_address.
222 nx_dwline_set_address(d, addr)
223 } else {
224 if pc_delta > 0 { nx_dwline_advance_pc(d, pc_delta) }
225 }
226 let line_delta: i64 = line - d.cur_line
227 if line_delta != 0 { nx_dwline_advance_line(d, line_delta) }
228 return nx_dwline_copy(d)
229}
230
231// ---- DWARF v5 HEADER (2026-08-07) -----------------------------------
232//
233// This file's own "what we ship" note claimed "header + line program" and shipped only the
234// PROGRAM -- for months, invisibly, because our only other DWARF reader (nx_addr2line) has no
235// header decoder either, so the pair agreed perfectly on a subset and neither met the format
236// (debt 1786111455). A ROUND-TRIP BETWEEN A PRODUCER AND A CONSUMER THAT SHARE AN AUTHOR TESTS
237// THEIR AGREEMENT, NOT THEIR CORRECTNESS.
238//
239// So this header was written against an EXTERNAL ruler: GNU readelf 2.42 parses what it emits,
240// including per-row file attribution. Byte-for-byte fixture + field decode:
241// knowledge/dwarf_debugline_fixture_2026_08_07.md
242//
243// FORM CHOICE -- DW_FORM_string (inline paths), NOT gcc's DW_FORM_line_strp:
244// gcc stores paths as 4-byte offsets into a separate .debug_line_str and resolves them with
245// RELOCATIONS (measured: those fields read zero in a raw .o dump). nxasm emits a FLAT,
246// fully-resolved image with no linker -- there is nothing in our model that could apply a
247// relocation, so line_strp would have forced BOTH a second section and a reloc pass.
248// Cost accepted: no string sharing between entries. Reversible; the form is a per-table choice.
249
250const NX_DW_LNCT_path: i64 = 0x01
251const NX_DW_LNCT_directory_index: i64 = 0x02
252const NX_DW_FORM_string: i64 = 0x08
253const NX_DW_FORM_udata: i64 = 0x0f
254
255func nx_dwline_u16(d: *NxDwLine, v: i64) -> i64 {
256 nx_dwline_byte(d, v & 0xFF)
257 nx_dwline_byte(d, (v >> 8) & 0xFF)
258 return 0
259}
260
261func nx_dwline_u32(d: *NxDwLine, v: i64) -> i64 {
262 nx_dwline_byte(d, v & 0xFF)
263 nx_dwline_byte(d, (v >> 8) & 0xFF)
264 nx_dwline_byte(d, (v >> 16) & 0xFF)
265 nx_dwline_byte(d, (v >> 24) & 0xFF)
266 return 0
267}
268
269// Overwrite a little-endian u32 ALREADY emitted at `off`. Both length fields are only knowable
270// after the bytes they measure exist, so they are written as zero and patched.
271func nx_dwline_patch_u32(d: *NxDwLine, off: i64, v: i64) -> i64 {
272 if off + 4 > d.used { return 0 - 1 }
273 d.buf[off] = (v ) & 0xFF
274 d.buf[off + 1] = (v >> 8) & 0xFF
275 d.buf[off + 2] = (v >> 16) & 0xFF
276 d.buf[off + 3] = (v >> 24) & 0xFF
277 return 0
278}
279
280// Emit `n` bytes then a NUL. The assembler holds file names as BOUNDS into the .s buffer rather
281// than as C strings, so length-delimited is the shape that avoids a copy.
282func nx_dwline_strn(d: *NxDwLine, s: *u8, n: i64) -> i64 {
283 var i: i64 = 0
284 while i < n { nx_dwline_byte(d, s[i]); i = i + 1 }
285 nx_dwline_byte(d, 0)
286 return 0
287}
288
289// Emit the complete v5 header, including the directory and file tables.
290// Returns the offset at which the line PROGRAM begins; pass nothing back -- nx_dwline_finish
291// recomputes what it needs -- but the caller may use it to assert.
292//
293// FILE NUMBERING, THE PART THAT IS EASY TO GET SILENTLY WRONG: DWARF v5 file indices are 0-based
294// and the line program's `file` register DEFAULTS TO 1. Our .s writes 1-based `.file N`. So entry
295// 0 is emitted as a DUPLICATE of the first file, making DWARF index N == our N. gcc does exactly
296// this (its one-source-file object carries TWO identical file entries) and the reason only becomes
297// visible when you check per-row attribution rather than whether the header merely parses.
298func nx_dwline_header(d: *NxDwLine, src: *u8, foff: *i64, flen: *i64, nfiles: i64) -> i64 {
299 if nfiles <= 0 { return 0 - 1 }
300 nx_dwline_u32(d, 0) // unit_length -- patched by nx_dwline_finish
301 nx_dwline_u16(d, 5) // version
302 nx_dwline_byte(d, 8) // address_size
303 nx_dwline_byte(d, 0) // segment_selector_size
304 nx_dwline_u32(d, 0) // header_length -- patched at the end of this function
305 nx_dwline_byte(d, 1) // minimum_instruction_length
306 nx_dwline_byte(d, 1) // maximum_operations_per_instruction
307 nx_dwline_byte(d, 1) // default_is_stmt
308 nx_dwline_byte(d, 0xFB) // line_base = -5
309 nx_dwline_byte(d, 14) // line_range
310 nx_dwline_byte(d, 13) // opcode_base
311 // standard_opcode_lengths[12]: operand counts for DW_LNS_copy .. DW_LNS_set_isa.
312 nx_dwline_byte(d, 0)
313 nx_dwline_byte(d, 1)
314 nx_dwline_byte(d, 1)
315 nx_dwline_byte(d, 1)
316 nx_dwline_byte(d, 1)
317 nx_dwline_byte(d, 0)
318 nx_dwline_byte(d, 0)
319 nx_dwline_byte(d, 0)
320 nx_dwline_byte(d, 1)
321 nx_dwline_byte(d, 0)
322 nx_dwline_byte(d, 0)
323 nx_dwline_byte(d, 1)
324 // directory table -- one entry, "."
325 nx_dwline_byte(d, 1) // directory_entry_format_count
326 nx_dwline_uleb(d, NX_DW_LNCT_path)
327 nx_dwline_uleb(d, NX_DW_FORM_string)
328 nx_dwline_uleb(d, 1) // directories_count
329 nx_dwline_byte(d, 46) // '.'
330 nx_dwline_byte(d, 0)
331 // file table
332 nx_dwline_byte(d, 2) // file_name_entry_format_count
333 nx_dwline_uleb(d, NX_DW_LNCT_path)
334 nx_dwline_uleb(d, NX_DW_FORM_string)
335 nx_dwline_uleb(d, NX_DW_LNCT_directory_index)
336 nx_dwline_uleb(d, NX_DW_FORM_udata)
337 nx_dwline_uleb(d, nfiles + 1) // +1 for the index-0 duplicate; see the note above
338 nx_dwline_strn(d, ((src as i64) + foff[0]) as *u8, flen[0])
339 nx_dwline_uleb(d, 0) // directory_index
340 var i: i64 = 0
341 while i < nfiles {
342 nx_dwline_strn(d, ((src as i64) + foff[i]) as *u8, flen[i])
343 nx_dwline_uleb(d, 0)
344 i = i + 1
345 }
346 // header_length is measured from just AFTER its own field (offset 12) to here.
347 nx_dwline_patch_u32(d, 8, d.used - 12)
348 return d.used
349}
350
351// Close the unit. unit_length EXCLUDES its own four bytes -- the classic off-by-four that makes a
352// reader walk into the next unit and report garbage rather than fail.
353func nx_dwline_finish(d: *NxDwLine) -> i64 {
354 nx_dwline_patch_u32(d, 0, d.used - 4)
355 return d.used
356}
357
358// ---- query --------------------------------------------------------
359
360func nx_dwline_size(d: *NxDwLine) -> i64 { return d.used }
361func nx_dwline_bytes(d: *NxDwLine) -> *u8 { return d.buf }
362
363// ---- self-test ----------------------------------------------------
364
365func main() -> i64 {
366 let d: *NxDwLine = nx_dwline_new(256)
367
368 // Empty: 0 bytes.
369 if nx_dwline_size(d) != 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
370
371 // Encode a few typical operations.
372
373 // Set initial address to 0x10000.
374 nx_dwline_set_address(d, 0x10000)
375 // Bytes: 0x00, 0x09, 0x02, then 8 LE bytes for 0x10000.
376 if d.buf[0] != 0x00 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
377 if d.buf[1] != 0x09 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
378 if d.buf[2] != 0x02 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
379 if d.buf[3] != 0x00 { return __syscall(93, 23, 0, 0, 0, 0, 0) } // addr LE byte 0
380 if d.buf[4] != 0x00 { return __syscall(93, 24, 0, 0, 0, 0, 0) }
381 if d.buf[5] != 0x01 { return __syscall(93, 25, 0, 0, 0, 0, 0) } // 0x10000 byte 2
382 if d.cur_addr != 0x10000 { return __syscall(93, 26, 0, 0, 0, 0, 0) }
383
384 // Advance PC by 4 (one RV64 instr).
385 let s_before: i64 = d.used
386 nx_dwline_advance_pc(d, 4)
387 // Expect: 0x02 + uleb128(4) = 2 bytes total.
388 if (d.used - s_before) != 2 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
389 if d.buf[s_before + 0] != DW_LNS_advance_pc { return __syscall(93, 31, 0, 0, 0, 0, 0) }
390 if d.buf[s_before + 1] != 4 { return __syscall(93, 32, 0, 0, 0, 0, 0) }
391 if d.cur_addr != 0x10004 { return __syscall(93, 33, 0, 0, 0, 0, 0) }
392
393 // Advance line by +3.
394 let s_line: i64 = d.used
395 nx_dwline_advance_line(d, 3)
396 if (d.used - s_line) != 2 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
397 if d.buf[s_line + 0] != DW_LNS_advance_line { return __syscall(93, 41, 0, 0, 0, 0, 0) }
398 if d.buf[s_line + 1] != 3 { return __syscall(93, 42, 0, 0, 0, 0, 0) }
399 if d.cur_line != 4 { return __syscall(93, 43, 0, 0, 0, 0, 0) }
400
401 // Test high-level record helper.
402 let d2: *NxDwLine = nx_dwline_new(256)
403 nx_dwline_set_address(d2, 0x10000)
404 nx_dwline_record(d2, 0x10010, 1, 5)
405 if d2.cur_addr != 0x10010 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
406 if d2.cur_line != 5 { return __syscall(93, 51, 0, 0, 0, 0, 0) }
407
408 nx_dwline_record(d2, 0x10014, 1, 6)
409 if d2.cur_addr != 0x10014 { return __syscall(93, 52, 0, 0, 0, 0, 0) }
410 if d2.cur_line != 6 { return __syscall(93, 53, 0, 0, 0, 0, 0) }
411
412 // End sequence cleanly.
413 nx_dwline_end_sequence(d2)
414
415 return 0
416}