code wiki / (root) / nx_dbg.nx

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}