nx_dwarf_cfi.nx source
↩ module page · 277 lines · 9595 B
1// nx_dwarf_cfi.nx -- DWARF .debug_frame / .eh_frame builder.
2//
3// Encodes Call Frame Information used by stack unwinders (debuggers,
4// exception handlers, profilers, perf). Today our riscv.nx emits
5// .cfi_* DIRECTIVES for the gcc-as path; this module produces the
6// BINARY section directly so the off-C path doesn't need gcc-as.
7//
8// CFI section structure (DWARF v5 §6.4):
9//
10// Common Information Entry (CIE) -- shared header for many FDEs:
11// length u32 bytes after this field
12// CIE_id u32 = 0xFFFFFFFF for .debug_frame, 0 for .eh_frame
13// version u8 = 1
14// augstr NUL-term ASCII augmentation
15// code_align uleb128 multiplier for advance_loc
16// data_align sleb128 multiplier for cfa_offset
17// ra_reg uleb128 return-address register (typically 1=ra)
18// init_insns ... DW_CFA_* opcodes for default state
19//
20// Frame Description Entry (FDE) per function:
21// length u32
22// CIE_ptr u32 offset back to the CIE (.debug_frame) /
23// relative offset (.eh_frame)
24// init_loc u64 function start address (RV64)
25// range u64 function size
26// insns ... DW_CFA_* opcodes describing per-PC state
27//
28// What we ship today (v0.0.1):
29// * CIE writer (begin/end + opcode emit)
30// * FDE writer (begin/end + opcode emit)
31// * DW_CFA_* opcode constants
32// * advance_loc / def_cfa_offset / offset(reg, off) helpers
33//
34// Pairs with riscv.nx's .cfi directive emission: same logical
35// information, different encoding (binary vs assembler-text).
36// When the off-C path is the only path, riscv.nx will swap to
37// emitting straight to nx_dwarf_cfi instead of textual directives.
38
39// nx_safety_envelope:
40// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
41// sil_target: SIL1
42// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
43// verdict: NOT_YET_EVALUATED
44
45import "syscalls.nx"
46
47// ---- DW_CFA_* opcodes (DWARF v5 §6.4.2) --------------------------
48
49const NX_DW_CFA_advance_loc: i64 = 0x40 // | delta (low 6 bits)
50const NX_DW_CFA_offset: i64 = 0x80 // | reg (low 6 bits)
51const NX_DW_CFA_restore: i64 = 0xC0 // | reg
52const NX_DW_CFA_nop: i64 = 0x00
53const NX_DW_CFA_set_loc: i64 = 0x01
54const NX_DW_CFA_advance_loc1: i64 = 0x02
55const NX_DW_CFA_advance_loc2: i64 = 0x03
56const NX_DW_CFA_advance_loc4: i64 = 0x04
57const NX_DW_CFA_offset_extended: i64 = 0x05
58const NX_DW_CFA_restore_extended: i64 = 0x06
59const NX_DW_CFA_undefined: i64 = 0x07
60const NX_DW_CFA_same_value: i64 = 0x08
61const NX_DW_CFA_register: i64 = 0x09
62const NX_DW_CFA_remember_state: i64 = 0x0A
63const NX_DW_CFA_restore_state: i64 = 0x0B
64const NX_DW_CFA_def_cfa: i64 = 0x0C
65const NX_DW_CFA_def_cfa_register: i64 = 0x0D
66const NX_DW_CFA_def_cfa_offset: i64 = 0x0E
67
68// ---- builder struct ----------------------------------------------
69
70struct NxDwCfi {
71 buf: *u8,
72 cap: i64,
73 used: i64,
74 cie_off: i64, // offset of the start of the open CIE/FDE
75 open_kind: i64, // 0 none, 1 CIE, 2 FDE
76}
77
78const NX_DW_CFI_BYTES: i64 = 40
79
80func nx_dwcfi_new(cap: i64) -> *NxDwCfi {
81 let raw: *u8 = sys_mmap(NX_DW_CFI_BYTES)
82 let c: *NxDwCfi = raw as *NxDwCfi
83 c.buf = sys_mmap(cap)
84 c.cap = cap
85 c.used = 0
86 c.cie_off = 0
87 c.open_kind = 0
88 return c
89}
90
91// ---- low-level write helpers -------------------------------------
92
93func nx_dwcfi_byte(c: *NxDwCfi, v: i64) -> i64 {
94 if c.used >= c.cap { return -1 }
95 c.buf[c.used] = v & 0xFF
96 c.used = c.used + 1
97 return 0
98}
99
100func nx_dwcfi_u32(c: *NxDwCfi, v: i64) -> i64 {
101 nx_dwcfi_byte(c, v & 0xFF)
102 nx_dwcfi_byte(c, (v >> 8) & 0xFF)
103 nx_dwcfi_byte(c, (v >> 16) & 0xFF)
104 nx_dwcfi_byte(c, (v >> 24) & 0xFF)
105 return 0
106}
107
108func nx_dwcfi_u64(c: *NxDwCfi, v: i64) -> i64 {
109 var i: i64 = 0
110 while i < 8 {
111 nx_dwcfi_byte(c, (v >> (i * 8)) & 0xFF)
112 i = i + 1
113 }
114 return 0
115}
116
117func nx_dwcfi_uleb(c: *NxDwCfi, v: i64) -> i64 {
118 var x: i64 = v
119 var go: i64 = 1
120 while go == 1 {
121 var b: i64 = x & 0x7F
122 x = x >> 7
123 if x != 0 { b = b | 0x80 }
124 if nx_dwcfi_byte(c, b) < 0 { return -1 }
125 if x == 0 { go = 0 }
126 }
127 return 0
128}
129
130func nx_dwcfi_sleb(c: *NxDwCfi, v: i64) -> i64 {
131 var x: i64 = v
132 var go: i64 = 1
133 while go == 1 {
134 var b: i64 = x & 0x7F
135 x = x >> 7
136 let sign_bit: i64 = b & 0x40
137 let done: i64 = ((x == 0) & (sign_bit == 0)) | ((x == -1) & (sign_bit != 0))
138 if done == 0 { b = b | 0x80 }
139 if nx_dwcfi_byte(c, b) < 0 { return -1 }
140 if done != 0 { go = 0 }
141 }
142 return 0
143}
144
145// ---- CIE ---------------------------------------------------------
146
147// Begin a CIE. Caller writes initial DW_CFA_* opcodes via
148// nx_dwcfi_advance_loc / def_cfa_offset / offset(reg, off) etc.,
149// then calls nx_dwcfi_end to patch the length field.
150func nx_dwcfi_cie_begin(c: *NxDwCfi, code_align: i64, data_align: i64,
151 ra_reg: i64) -> i64 {
152 c.cie_off = c.used
153 c.open_kind = 1
154 nx_dwcfi_u32(c, 0) // length placeholder
155 nx_dwcfi_u32(c, 0xFFFFFFFF) // CIE_id (.debug_frame convention)
156 nx_dwcfi_byte(c, 1) // version
157 nx_dwcfi_byte(c, 0) // augstr "" (empty)
158 nx_dwcfi_uleb(c, code_align)
159 nx_dwcfi_sleb(c, data_align)
160 nx_dwcfi_uleb(c, ra_reg)
161 return 0
162}
163
164// ---- FDE ---------------------------------------------------------
165
166// Begin an FDE for a function. cie_offset = byte offset of the
167// CIE this FDE inherits from. init_loc = function start address.
168// range = function byte count.
169func nx_dwcfi_fde_begin(c: *NxDwCfi, cie_offset: i64,
170 init_loc: i64, range: i64) -> i64 {
171 c.cie_off = c.used
172 c.open_kind = 2
173 nx_dwcfi_u32(c, 0) // length placeholder
174 nx_dwcfi_u32(c, cie_offset)
175 nx_dwcfi_u64(c, init_loc)
176 nx_dwcfi_u64(c, range)
177 return 0
178}
179
180// End the open CIE / FDE: patch the length field to whatever
181// follows the 4-byte length itself.
182func nx_dwcfi_end(c: *NxDwCfi) -> i64 {
183 if c.open_kind == 0 { return -1 }
184 let length: i64 = c.used - (c.cie_off + 4)
185 c.buf[c.cie_off + 0] = length & 0xFF
186 c.buf[c.cie_off + 1] = (length >> 8) & 0xFF
187 c.buf[c.cie_off + 2] = (length >> 16) & 0xFF
188 c.buf[c.cie_off + 3] = (length >> 24) & 0xFF
189 c.open_kind = 0
190 return 0
191}
192
193// ---- per-op helpers ----------------------------------------------
194
195// DW_CFA_advance_loc with delta (in code units) <= 63 packed into
196// the opcode's low 6 bits. For larger deltas use advance_loc1/2/4.
197func nx_dwcfi_advance_loc(c: *NxDwCfi, delta: i64) -> i64 {
198 if delta <= 63 {
199 return nx_dwcfi_byte(c, NX_DW_CFA_advance_loc | (delta & 0x3F))
200 }
201 if delta <= 0xFF {
202 nx_dwcfi_byte(c, NX_DW_CFA_advance_loc1)
203 return nx_dwcfi_byte(c, delta)
204 }
205 if delta <= 0xFFFF {
206 nx_dwcfi_byte(c, NX_DW_CFA_advance_loc2)
207 nx_dwcfi_byte(c, delta & 0xFF)
208 return nx_dwcfi_byte(c, (delta >> 8) & 0xFF)
209 }
210 nx_dwcfi_byte(c, NX_DW_CFA_advance_loc4)
211 return nx_dwcfi_u32(c, delta)
212}
213
214// DW_CFA_def_cfa_offset: redefine CFA's offset (CFA stays at
215// previous register, just bumps offset).
216func nx_dwcfi_def_cfa_offset(c: *NxDwCfi, off: i64) -> i64 {
217 nx_dwcfi_byte(c, NX_DW_CFA_def_cfa_offset)
218 return nx_dwcfi_uleb(c, off)
219}
220
221// DW_CFA_def_cfa: define CFA = (reg, offset).
222func nx_dwcfi_def_cfa(c: *NxDwCfi, reg: i64, off: i64) -> i64 {
223 nx_dwcfi_byte(c, NX_DW_CFA_def_cfa)
224 nx_dwcfi_uleb(c, reg)
225 return nx_dwcfi_uleb(c, off)
226}
227
228// DW_CFA_offset(reg, factored_offset): "reg was saved at CFA + N".
229// `off` is the FACTORED offset (caller multiplies by abs(data_align)).
230func nx_dwcfi_offset(c: *NxDwCfi, reg: i64, factored_off: i64) -> i64 {
231 if reg <= 63 {
232 nx_dwcfi_byte(c, NX_DW_CFA_offset | (reg & 0x3F))
233 } else {
234 nx_dwcfi_byte(c, NX_DW_CFA_offset_extended)
235 nx_dwcfi_uleb(c, reg)
236 }
237 return nx_dwcfi_uleb(c, factored_off)
238}
239
240func nx_dwcfi_size(c: *NxDwCfi) -> i64 { return c.used }
241func nx_dwcfi_bytes(c: *NxDwCfi) -> *u8 { return c.buf }
242
243// ---- self-test ---------------------------------------------------
244
245func main() -> i64 {
246 let c: *NxDwCfi = nx_dwcfi_new(256)
247
248 // CIE: code_align=1, data_align=-4, ra_reg=1 (ra register).
249 let cie_off: i64 = c.used
250 nx_dwcfi_cie_begin(c, 1, -4, 1)
251 // Default state: CFA = sp (reg 2) + 0
252 nx_dwcfi_def_cfa(c, 2, 0)
253 nx_dwcfi_end(c)
254
255 // Verify length patched.
256 let cie_len: i64 = c.buf[cie_off] | (c.buf[cie_off+1] << 8) | (c.buf[cie_off+2] << 16) | (c.buf[cie_off+3] << 24)
257 if cie_len <= 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
258 if cie_len != c.used - cie_off - 4 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
259
260 // FDE: function at 0x10000, 64 bytes.
261 let fde_off: i64 = c.used
262 nx_dwcfi_fde_begin(c, cie_off, 0x10000, 64)
263 // After 16 bytes, CFA offset becomes 32.
264 nx_dwcfi_advance_loc(c, 16)
265 nx_dwcfi_def_cfa_offset(c, 32)
266 // ra (reg 1) saved at CFA - 8 -> factored = 8 / 4 = 2 (data_align=-4)
267 nx_dwcfi_offset(c, 1, 2)
268 nx_dwcfi_end(c)
269
270 let fde_len: i64 = c.buf[fde_off] | (c.buf[fde_off+1] << 8) | (c.buf[fde_off+2] << 16) | (c.buf[fde_off+3] << 24)
271 if fde_len <= 0 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
272
273 // Total size > 0.
274 if nx_dwcfi_size(c) <= 0 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
275
276 return 0
277}