nx_dwarf_aranges.nx source
↩ module page · 184 lines · 6410 B
1// nx_dwarf_aranges.nx -- DWARF .debug_aranges builder.
2//
3// .debug_aranges is a fast PC-to-CU (Compilation Unit) lookup table.
4// addr2line / gdb consult it FIRST when given a program counter:
5// each entry maps a [start_addr, length) range to the file offset of
6// a CU's DIE in .debug_info. Without aranges, the debugger has to
7// scan every CU's DIE forest to find the one covering the PC --
8// O(n_CUs) per address lookup vs O(log n) here.
9//
10// Per-CU header layout (DWARF v5, section 6.1.2):
11// length (u32 / u64 -- excludes itself)
12// version (u16, =2 for DWARF v5)
13// debug_info_off (u32 / u64 -- file offset of CU header in .debug_info)
14// addr_size (u8, 8 for RV64)
15// seg_size (u8, 0 -- we don't use segments)
16// PADDING to 2*addr_size boundary
17// tuples: (start_addr, length) pairs of `addr_size` bytes each
18// final tuple: (0, 0) terminator
19//
20// We always use 32-bit DWARF (single .o files don't exceed 4 GB
21// of debug info; if they do we'll switch to 64-bit DWARF).
22//
23// Pairs with nx_dwarf_info (the consumer of debug_info_off) +
24// nx_dwarf_line / nx_dwarf_str / nx_dwarf_abbrev (cousins).
25
26// nx_safety_envelope:
27// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
28// sil_target: SIL1
29// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
30// verdict: NOT_YET_EVALUATED
31
32import "syscalls.nx"
33
34const NX_DWAR_VERSION: i64 = 2
35const NX_DWAR_ADDR_SIZE: i64 = 8
36const NX_DWAR_SEG_SIZE: i64 = 0
37
38struct NxDwAranges {
39 buf: *u8,
40 len: i64,
41 cap: i64,
42 cu_start: i64, // index where current CU header begins
43 cu_open: i64, // 1 if a CU is in progress
44}
45
46const NX_DWARANGES_BYTES: i64 = 40
47
48func nx_dwar_new(cap: i64) -> *NxDwAranges {
49 let raw: *u8 = sys_mmap(NX_DWARANGES_BYTES)
50 let a: *NxDwAranges = raw as *NxDwAranges
51 a.cap = cap
52 a.buf = sys_mmap(cap)
53 a.len = 0
54 a.cu_start = 0
55 a.cu_open = 0
56 return a
57}
58
59func nx_dwar_byte(a: *NxDwAranges, b: i64) -> i64 {
60 if a.len >= a.cap { return -1 }
61 a.buf[a.len] = b & 0xFF
62 a.len = a.len + 1
63 return 0
64}
65
66func nx_dwar_u16(a: *NxDwAranges, v: i64) -> i64 {
67 nx_dwar_byte(a, v & 0xFF)
68 return nx_dwar_byte(a, (v >> 8) & 0xFF)
69}
70
71func nx_dwar_u32(a: *NxDwAranges, v: i64) -> i64 {
72 nx_dwar_byte(a, v & 0xFF)
73 nx_dwar_byte(a, (v >> 8) & 0xFF)
74 nx_dwar_byte(a, (v >> 16) & 0xFF)
75 return nx_dwar_byte(a, (v >> 24) & 0xFF)
76}
77
78func nx_dwar_u64(a: *NxDwAranges, v: i64) -> i64 {
79 var i: i64 = 0
80 while i < 8 {
81 nx_dwar_byte(a, (v >> (i * 8)) & 0xFF)
82 i = i + 1
83 }
84 return 0
85}
86
87// Patch a u32 in-place at offset `off`.
88func nx_dwar_patch_u32(a: *NxDwAranges, off: i64, v: i64) -> i64 {
89 a.buf[off] = v & 0xFF
90 a.buf[off + 1] = (v >> 8) & 0xFF
91 a.buf[off + 2] = (v >> 16) & 0xFF
92 a.buf[off + 3] = (v >> 24) & 0xFF
93 return 0
94}
95
96// Begin a new CU's range list. Caller passes the file offset of
97// the CU header in .debug_info (the same offset they recorded in
98// the .debug_info CU header's debug_abbrev_offset).
99func nx_dwar_cu_begin(a: *NxDwAranges, debug_info_off: i64) -> i64 {
100 if a.cu_open == 1 { return -1 }
101 a.cu_start = a.len
102 a.cu_open = 1
103 nx_dwar_u32(a, 0) // length placeholder
104 nx_dwar_u16(a, NX_DWAR_VERSION)
105 nx_dwar_u32(a, debug_info_off)
106 nx_dwar_byte(a, NX_DWAR_ADDR_SIZE)
107 nx_dwar_byte(a, NX_DWAR_SEG_SIZE)
108 // Pad to 2 * addr_size = 16 bytes from the start of the unit
109 // (DWARF v5 requires the first tuple to be 2*addr_size aligned).
110 var pad_done: i64 = 0
111 while pad_done == 0 {
112 let off_in_cu: i64 = a.len - a.cu_start
113 if (off_in_cu & 15) == 0 { pad_done = 1 }
114 else { nx_dwar_byte(a, 0) }
115 }
116 return 0
117}
118
119// Add one (start_addr, length) tuple. Caller calls this once per
120// contiguous PC range owned by the CU (typically just one per CU
121// for our codegen, since our functions land in one .text block).
122func nx_dwar_add(a: *NxDwAranges, start: i64, len: i64) -> i64 {
123 if a.cu_open == 0 { return -1 }
124 nx_dwar_u64(a, start)
125 return nx_dwar_u64(a, len)
126}
127
128// Close the current CU's range list. Writes the (0, 0) terminator
129// then patches the length field at the start.
130func nx_dwar_cu_end(a: *NxDwAranges) -> i64 {
131 if a.cu_open == 0 { return -1 }
132 nx_dwar_u64(a, 0)
133 nx_dwar_u64(a, 0)
134 let end_off: i64 = a.len
135 let unit_len: i64 = end_off - a.cu_start - 4 // exclude the length field itself
136 nx_dwar_patch_u32(a, a.cu_start, unit_len)
137 a.cu_open = 0
138 return 0
139}
140
141// ---- self-test ---------------------------------------------------
142
143func main() -> i64 {
144 let a: *NxDwAranges = nx_dwar_new(256)
145
146 nx_dwar_cu_begin(a, 0x1234)
147 nx_dwar_add(a, 0x10000, 0x800) // function 1
148 nx_dwar_add(a, 0x10800, 0x400) // function 2
149 nx_dwar_cu_end(a)
150
151 // Length field at offset 0 should be (total - 4).
152 if a.len < 16 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
153 let stored_len: i64 = a.buf[0]
154 | (a.buf[1] << 8)
155 | (a.buf[2] << 16)
156 | (a.buf[3] << 24)
157 if stored_len != a.len - 4 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
158
159 // version u16 at offset 4 should be 2.
160 if a.buf[4] != 2 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
161 if a.buf[5] != 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
162
163 // debug_info_off u32 at offset 6 should be 0x1234.
164 if a.buf[6] != 0x34 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
165 if a.buf[7] != 0x12 { return __syscall(93, 6, 0, 0, 0, 0, 0) }
166
167 // addr_size byte at offset 10 should be 8.
168 if a.buf[10] != 8 { return __syscall(93, 7, 0, 0, 0, 0, 0) }
169 // seg_size byte at offset 11 should be 0.
170 if a.buf[11] != 0 { return __syscall(93, 8, 0, 0, 0, 0, 0) }
171
172 // First tuple should appear at offset 16 (after pad to 16 from CU start).
173 // start_addr = 0x10000 little-endian.
174 if a.buf[16] != 0x00 { return __syscall(93, 9, 0, 0, 0, 0, 0) }
175 if a.buf[17] != 0x00 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
176 if a.buf[18] != 0x01 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
177
178 // Reject double-begin without close.
179 if nx_dwar_cu_begin(a, 0) != 0 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
180 let r: i64 = nx_dwar_cu_begin(a, 0)
181 if r != -1 { return __syscall(93, 13, 0, 0, 0, 0, 0) }
182
183 return 0
184}