code wiki / (root) / nx_dwarf_line.nx

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}