nx_dbg.nx source
↩ module page · 234 lines · 7631 B
1// nx_dbg.nx -- sovereign single-step debugger primitive.
2//
3// Wraps nx_rv64_sim with debugger superpowers:
4//
5// * step run one instruction, return control
6// * cont run until breakpoint / halt / illegal
7// * break_at set a code-address breakpoint
8// * break_clr clear a code-address breakpoint
9// * dump_regs print all 32 GPR + pc to fd
10// * dump_stack print N words above sp
11// * locate PC -> source (file_idx, line) via nx_addr2line
12//
13// All operations work without GDB / qemu / docker. When wired with
14// nxc_native_wrap, this is the sovereign equivalent of:
15//
16// gdb --batch -ex "break main" -ex "run" -ex "info reg" my.elf
17//
18// Pairs with our existing primitives:
19// nx_rv64_sim -- VM
20// nx_dis -- disassemble PC at current state
21// nx_addr2line -- map PC -> source line
22// nx_strfmt -- formatted output
23//
24// v0.0.1 scope: simple breakpoint set (linear scan, up to 64
25// breakpoints). Future commits add: watchpoints, conditional
26// breakpoints, scripted recipes, GDB-stub remote-protocol.
27
28// nx_safety_envelope:
29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
30// sil_target: SIL1
31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
32// verdict: NOT_YET_EVALUATED
33
34import "syscalls.nx"
35import "nx_rv64_sim.nx"
36import "nx_dis.nx"
37import "nx_addr2line.nx"
38import "nx_strfmt.nx"
39const NX_MAGIC_9999: i64 = 9999
40
41const NX_DBG_MAX_BREAKPOINTS: i64 = 64
42
43struct NxDbg {
44 cpu: *NxRv64Cpu,
45 bp_addrs: *i64, // up to NX_DBG_MAX_BREAKPOINTS i64s
46 n_bp: i64,
47}
48
49const NX_DBG_BYTES: i64 = 24
50
51func nx_dbg_new(cpu: *NxRv64Cpu) -> *NxDbg {
52 let raw: *u8 = sys_mmap(NX_DBG_BYTES)
53 let d: *NxDbg = raw as *NxDbg
54 d.cpu = cpu
55 d.bp_addrs = sys_mmap(NX_DBG_MAX_BREAKPOINTS * 8) as *i64
56 d.n_bp = 0
57 return d
58}
59
60// ---- breakpoint table -------------------------------------------
61
62// Set a breakpoint at code address `addr`. Returns 0 / -1 on full.
63// No-op (returns 0) if already present.
64func nx_dbg_break_at(d: *NxDbg, addr: i64) -> i64 {
65 var i: i64 = 0
66 while i < d.n_bp {
67 if d.bp_addrs[i] == addr { return 0 }
68 i = i + 1
69 }
70 if d.n_bp >= NX_DBG_MAX_BREAKPOINTS { return -1 }
71 d.bp_addrs[d.n_bp] = addr
72 d.n_bp = d.n_bp + 1
73 return 0
74}
75
76// Clear a breakpoint. Returns 1 if found+removed, 0 if absent.
77func nx_dbg_break_clr(d: *NxDbg, addr: i64) -> i64 {
78 var i: i64 = 0
79 while i < d.n_bp {
80 if d.bp_addrs[i] == addr {
81 // Swap-remove.
82 d.bp_addrs[i] = d.bp_addrs[d.n_bp - 1]
83 d.n_bp = d.n_bp - 1
84 return 1
85 }
86 i = i + 1
87 }
88 return 0
89}
90
91// True (1) if the current PC is a breakpoint.
92func nx_dbg_at_bp(d: *NxDbg) -> i64 {
93 var i: i64 = 0
94 while i < d.n_bp {
95 if d.bp_addrs[i] == d.cpu.pc { return 1 }
96 i = i + 1
97 }
98 return 0
99}
100
101// ---- run controls -----------------------------------------------
102
103// Single-step: execute one instruction. Returns the underlying
104// nx_rv64_step result (1 cont / 0 halted / -1 illegal).
105func nx_dbg_step(d: *NxDbg) -> i64 {
106 return nx_rv64_step(d.cpu)
107}
108
109// Continue: run until breakpoint hit, halt, or illegal. Returns:
110// 1 = stopped at breakpoint
111// 0 = halted (sys_exit)
112// -1 = illegal instruction
113func nx_dbg_cont(d: *NxDbg, max_insns: i64) -> i64 {
114 var i: i64 = 0
115 while i < max_insns {
116 let r: i64 = nx_rv64_step(d.cpu)
117 if r == 0 { return 0 }
118 if r < 0 { return -1 }
119 if nx_dbg_at_bp(d) == 1 { return 1 }
120 i = i + 1
121 }
122 return 1 // budget expired (treated as breakpoint-like stop)
123}
124
125// ---- inspection -------------------------------------------------
126
127// Print all 32 GPRs + pc to fd in two columns. Format:
128// x0 (zero) = 0x... x1 (ra) = 0x...
129// ...
130// pc = 0x...
131func nx_dbg_dump_regs(d: *NxDbg, fd: i64) -> i64 {
132 let argv_raw: *u8 = sys_mmap(64)
133 let argv: *i64 = argv_raw as *i64
134 var i: i64 = 0
135 while i < 32 {
136 argv[0] = i
137 argv[1] = nx_dis_reg_name(i) as i64
138 argv[2] = d.cpu.regs[i]
139 nx_fmt_to_fd(fd, " x%d (%s) = 0x%X\n" as *u8, argv, 3)
140 i = i + 1
141 }
142 argv[0] = d.cpu.pc
143 nx_fmt_to_fd(fd, " pc = 0x%X\n" as *u8, argv, 1)
144 return 0
145}
146
147// Disassemble + print the next `n` instructions starting at the
148// current pc (does not execute them).
149func nx_dbg_dump_disasm(d: *NxDbg, n: i64, fd: i64) -> i64 {
150 var p: i64 = d.cpu.pc
151 var i: i64 = 0
152 while i < n {
153 let argv_raw: *u8 = sys_mmap(32)
154 let argv: *i64 = argv_raw as *i64
155 argv[0] = p
156 nx_fmt_to_fd(fd, " 0x%X: " as *u8, argv, 1)
157 let consumed: i64 = nx_dis_one(d.cpu.mem_base, p, fd)
158 sys_write(fd, "\n" as *u8, 1)
159 if consumed <= 0 { return 0 }
160 p = p + consumed
161 i = i + 1
162 }
163 return 0
164}
165
166// Dump `n` words starting at sim address `addr` to fd.
167func nx_dbg_dump_mem(d: *NxDbg, addr: i64, n: i64, fd: i64) -> i64 {
168 var i: i64 = 0
169 while i < n {
170 let v: i64 = nx_rv64_load_u64(d.cpu, addr + i * 8)
171 let argv_raw: *u8 = sys_mmap(32)
172 let argv: *i64 = argv_raw as *i64
173 argv[0] = addr + i * 8
174 argv[1] = v
175 nx_fmt_to_fd(fd, " [0x%X] = 0x%X\n" as *u8, argv, 2)
176 i = i + 1
177 }
178 return 0
179}
180
181// ---- self-test ---------------------------------------------------
182
183func main() -> i64 {
184 // Set up a tiny program at offset 0:
185 // 0x00: addi a0, zero, 1
186 // 0x04: addi a0, a0, 1 -> a0 = 2
187 // 0x08: addi a0, a0, 1 -> a0 = 3
188 // 0x0C: addi a7, zero, 93
189 // 0x10: ecall
190 let mem: *u8 = sys_mmap(64)
191 mem[0] = 0x13; mem[1] = 0x05; mem[2] = 0x10; mem[3] = 0x00 // addi a0,zero,1
192 mem[4] = 0x13; mem[5] = 0x05; mem[6] = 0x15; mem[7] = 0x00 // addi a0,a0,1
193 mem[8] = 0x13; mem[9] = 0x05; mem[10] = 0x15; mem[11] = 0x00 // addi a0,a0,1
194 mem[12] = 0x93; mem[13] = 0x08; mem[14] = 0xD0; mem[15] = 0x05 // addi a7,zero,93
195 mem[16] = 0x73; mem[17] = 0x00; mem[18] = 0x00; mem[19] = 0x00 // ecall
196
197 let cpu: *NxRv64Cpu = nx_rv64_cpu_new(mem, 64, 0)
198 let d: *NxDbg = nx_dbg_new(cpu)
199
200 // Set a breakpoint at 0x08 + run until hit.
201 nx_dbg_break_at(d, 0x08)
202 if d.n_bp != 1 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
203
204 let r1: i64 = nx_dbg_cont(d, 100)
205 if r1 != 1 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
206 if cpu.pc != 0x08 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
207 // After 2 instructions, a0 = 2.
208 if cpu.regs[10] != 2 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
209
210 // Step once: executes the third addi -> a0 = 3.
211 nx_dbg_step(d)
212 if cpu.pc != 0x0C { return __syscall(93, 30, 0, 0, 0, 0, 0) }
213 if cpu.regs[10] != 3 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
214
215 // Clear breakpoint, then run to halt.
216 nx_dbg_break_clr(d, 0x08)
217 if d.n_bp != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
218 let r2: i64 = nx_dbg_cont(d, 100)
219 if r2 != 0 { return __syscall(93, 41, 0, 0, 0, 0, 0) } // halted
220 if cpu.exit_code != 3 { return __syscall(93, 42, 0, 0, 0, 0, 0) }
221
222 // bp_clr on absent is a no-op.
223 if nx_dbg_break_clr(d, 0xDEAD) != 0 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
224
225 // Set max+1 breakpoints; the 65th rejects.
226 var k: i64 = 0
227 while k < NX_DBG_MAX_BREAKPOINTS {
228 if nx_dbg_break_at(d, k) != 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
229 k = k + 1
230 }
231 if nx_dbg_break_at(d, NX_MAGIC_9999) != -1 { return __syscall(93, 61, 0, 0, 0, 0, 0) }
232
233 return 0
234}