code wiki / (root) / nx_dbg_step.nx

nx_dbg_step.nx source

↩ module page · 1272 lines · 58248 B

1// nx_dbg_step.nx -- THE STEPPING DEBUGGER (/compare/lang LN12, watch symbol dbg_step_over). 2// 3// THE RUNG (lang.plan): "Stepping debugger -- the doctor lane's DT1: break and step on our own 4// binaries over the line table we emit (one contract, two pages)". LB5 shipped the line table 5// (lm_stmt_stamp, statement granularity, readelf-validated). This is the debugger ON it. 6// 7// WHAT WAS REUSED, NOT REWRITTEN (nx_spendgate 2026-08-25 named all three): 8// nx_addr2line_lib.nx -- the sovereign DWARF v5 line-program primitives (uleb/sleb/u32/u64le, 9// nx_a2l_prog_off, and nx_a2l_func_at which already reads .debug_info 10// subprograms). Every ULEB/SLEB byte in this file is decoded by THAT 11// library's functions; there is still exactly one DWARF integer decoder. 12// nx_bootcap.nx -- bootcap_machine: the SAME device set the boot rulers measure. 13// rv64im_min_decoder.nx-- nx_rv64im_decode_kind + field extractors, for call/return classification. 14// nx_lineconf_lib.nx -- the one line-anchored conf reader. 15// nx_elf_read.nx -- ELF64 section walk. 16// 17// WHAT IS NEW HERE, AND WHY THE INCUMBENT COULD NOT DO IT: 18// nx_a2l_run answers ONE question -- "which line is at address X" -- and to do it cheaply it keeps 19// only the best row and STOPS AT THE FIRST end_sequence. A debugger needs the WHOLE table and it 20// needs it BOTH WAYS: 21// * file:line -> address (setting a breakpoint) <- the direction the incumbent cannot answer 22// * address -> row index (where am I) 23// * row i -> row i+1 (where does this statement END) <- the step primitive 24// So this file runs the line program once into a two-pass-sized ROW TABLE, across ALL sequences. 25// 26// It also decodes THREE opcode classes nx_a2l_run refuses: SPECIAL opcodes (the compact bias form), 27// DW_LNS_const_add_pc and DW_LNS_fixed_advance_pc, plus any other standard opcode via the header's 28// own standard_opcode_lengths table. That is not scope creep: it is the difference between a table 29// that works on our emitter's output and one that works on a line table from any DWARF v5 producer. 30// Anything still undecodable sets decode_complete=0 and then RESOLUTION REFUSES WITH "UNPROVEN" 31// RATHER THAN "NO SUCH LINE" -- a partial table that answers "absent" is a confident false negative, 32// which is the one failure mode a breakpoint resolver must never have. 33// 34// STEPPING SEMANTICS (the actual rung). A statement step is NOT an instruction step: 35// dbg_step_into -- single-step until the covering line-table ROW CHANGES. Entering a callee is a 36// row change, so this stops on the callee's first statement. 37// dbg_step_over -- the same, except while the call DEPTH is above the starting depth nothing is 38// tested: the whole callee runs and the stop lands on the statement AFTER the 39// call, in the caller. A `ret` past the starting frame is depth < 0, which is 40// tested (that is gdb's behaviour at the end of a function, deliberately kept). 41// Depth comes from the RISC-V unprivileged spec's OWN return-address-stack hint rules (ds_ras_delta) 42// -- rd/rs1 in {x1,x5} for JAL/JALR, plus C.JALR/C.JR for RVC -- not from a guess about which 43// register is "the" link register. x5 is a link register too and a debugger that forgets it 44// mis-tracks depth in exactly the code that uses the alternate link register. 45// 46// ===================== NEVER-BRICK, BY CONSTRUCTION, NOT BY PROMISE ===================== 47// Rule 26 binds a debugger harder than anything else in the estate, because the ordinary way to 48// build one is ptrace + patching INT3/EBREAK into a live process's text. This organ does none of it: 49// 50// 1. THERE IS NO ptrace, AND THERE IS NO WAY TO ADD ONE BY ACCIDENT. Measured 2026-08-25: 51// `nx_absent "func sys_ptrace" buildroot/runtime nx` -> matches=0, coverage_complete=1, 52// corpus_complete=1, verdict=ABSENT-PROVEN. The syscall WRAPPER IS NOT DEFINED anywhere in the 53// estate, so no call site can exist: "attach to a host process" is not a policy this organ 54// declines, it is a capability the platform does not have. 55// THE MEASUREMENT IS PHRASED ON THE DEFINITION ON PURPOSE. The first draft of this line quoted 56// a bare-word probe for "sys_ptrace" at matches=0 -- true when it was taken, and FALSE the moment 57// this comment existed, because the probe then matched THIS PARAGRAPH. Re-measured, the bare word 58// now returns 4 hits and every one is prose: this header, the gate's header, and the two lines of 59// the gate that carry the search literal itself. Prose is source bytes too, and a detector that 60// matches its own explanation of the defect it hunts is a false positive with an authoritative 61// name. The definition probe cannot be contaminated that way, and it is the stronger claim: with 62// no wrapper defined, the absence of call sites is a consequence rather than a second thing to 63// check. (The gate keeps the call-site count as well, printed, so a future edit that adds one 64// fires a tooth instead of sliding past.) 65// 2. THE SUBJECT IS A GUEST INSIDE OUR OWN INTERPRETER. The only execution surface is 66// nx_rv64im_sim_step over memory this process sys_mmap'd. No host process is ever stopped, so 67// the stopped-and-orphaned class this estate tracks CANNOT OCCUR -- there is nothing to orphan. 68// 3. THE GRAMMAR HAS NO PID. `run` takes an IMAGE PATH and nothing else executable. The absence of 69// a pid argument is the allow-list, and it is mechanical: a caller cannot name a victim. 70// 4. GUEST CODE IS NEVER PATCHED. Breakpoints are a BITMAP over RAM (one bit per halfword, sized 71// from mem_size -- no cap to guess), so the guest's bytes stay exactly what it loaded. Because 72// nothing is written, nothing has to be restored, and "the restore failed" is not a state that 73// exists. This is MEASURED, not asserted: ds_code_sum checksums the loaded image region at 74// attach and again at detach, and every run prints code_bytes_unchanged=<0|1>. 75// 5. DETACH ALWAYS RUNS, INCLUDING ON ABNORMAL EXIT. Detach is nx_rv64im_sim_run in THIS process -- 76// not a message to another process that might not be delivered. If this organ is SIGKILLed the 77// guest ceases to exist with it (it is heap), which is the only outcome that leaves nothing 78// stopped anywhere. There is no path in which a subject survives us in a stopped state. 79// 6. NO PERSISTENT HARDWARE STATE IS REACHABLE. The only writes this organ makes are to its own 80// sys_mmap heap and to fd 1. No firmware, UEFI, CMOS, NVRAM, vBIOS or EEPROM namespace is 81// opened, and the device models are the emulated bootcap set, which terminate in the sim's own 82// buffers. 83// ======================================================================================= 84// 85// usage: 86// nx_dbg_step lines <linetable.bin> dump every row of a raw .debug_line unit 87// nx_dbg_step resolve <linetable.bin> <file> <line> file:line -> address (the breakpoint lookup) 88// nx_dbg_step elflines <elf> the same, reading .debug_line out of an ELF 89// nx_dbg_step elfresolve <elf> <file> <line> 90// nx_dbg_step run <image.bin> <linetable.bin> <cmd>... attach, drive, detach 91// cmds: break <file> <line> | breakaddr <hex> | step | stepover | continue | bt | detach 92// nx_dbg_step selftest line-table KAT (a self-check, NOT a proof) 93// 94// exit: 0 ok | 2 usage | 3 image unreadable | 4 line table undecodable | 5 breakpoint refused 95// | 6 step budget exceeded (announced, never silent) 96// license_tier: ORIGINAL 97import "nx_syscalls.nx" 98import "nx_lineconf_lib.nx" 99import "nx_addr2line_lib.nx" 100import "nx_elf_read.nx" 101import "nx_bootcap.nx" 102import "rv64im_min_decoder.nx" 103 104// ---- DWARF v5 constants. Same values as nx_dwarf_line.nx, repeated here for the same reason 105// nx_addr2line_lib repeats them: so this module needs only the decoder, never the encoder. 106const DS_DW_VERSION_5: i64 = 5 107const DS_LNCT_path: i64 = 0x01 108const DS_LNCT_directory_index: i64 = 0x02 109const DS_FORM_string: i64 = 0x08 110const DS_FORM_udata: i64 = 0x0f 111const DS_LNS_copy: i64 = 0x01 112const DS_LNS_advance_pc: i64 = 0x02 113const DS_LNS_advance_line: i64 = 0x03 114const DS_LNS_set_file: i64 = 0x04 115const DS_LNS_const_add_pc: i64 = 0x08 116const DS_LNS_fixed_advance_pc: i64 = 0x09 117const DS_LNE_end_sequence: i64 = 0x01 118const DS_LNE_set_address: i64 = 0x02 119 120// header field offsets inside a v5 unit -- named, so no hand-counted literal drifts from the layout 121const DS_OFF_UNIT_LEN: i64 = 0 122const DS_OFF_VERSION: i64 = 4 123const DS_OFF_ADDR_SIZE: i64 = 6 124const DS_OFF_SEG_SEL: i64 = 7 125const DS_OFF_HEADER_LEN: i64 = 8 126const DS_OFF_PROLOGUE: i64 = 12 // header_length is measured from HERE 127const DS_U32_BYTES: i64 = 4 128const DS_UNIT_LEN_MIN: i64 = 4 129const DS_HDR_MIN_BYTES: i64 = 12 130const DS_BYTE_MOD: i64 = 256 131const DS_BYTE_SIGN: i64 = 128 132const DS_BYTE_MASK: i64 = 255 133const DS_SPECIAL_MAX: i64 = 255 134const DS_PTR_BYTES: i64 = 8 135const DS_SET_ADDR_BYTES: i64 = 8 136const DS_ASCII_ZERO: i64 = 48 137const DS_ASCII_NINE: i64 = 57 138const DS_ASCII_a: i64 = 97 139const DS_ASCII_f: i64 = 102 140const DS_ASCII_A: i64 = 65 141const DS_ASCII_F: i64 = 70 142const DS_ASCII_SLASH: i64 = 47 143const DS_DECIMAL: i64 = 10 144const DS_HEXBASE: i64 = 16 145 146// RV64 instruction shape -- for the call/return classifier only 147const DS_INST_BYTES: i64 = 4 148const DS_RVC_BYTES: i64 = 2 149const DS_ILEN32_MASK: i64 = 3 150const DS_ILEN32_VAL: i64 = 3 151const DS_REG_RA: i64 = 1 // x1, the primary link register 152const DS_REG_T0: i64 = 5 // x5, the ALTERNATE link register (RISC-V spec) 153const DS_RVC_OP_C2: i64 = 2 154const DS_RVC_F3_JALR: i64 = 4 155const DS_RVC_RS2_SHIFT: i64 = 2 156const DS_RVC_RD_SHIFT: i64 = 7 157const DS_RVC_F3_SHIFT: i64 = 13 158const DS_RVC_BIT12_SHIFT: i64 = 12 159const DS_REG_MASK: i64 = 31 160const DS_F3_MASK: i64 = 7 161 162// verdict / reason codes 163const DS_STOP_BREAKPOINT: i64 = 1 164const DS_STOP_STEP: i64 = 2 165const DS_STOP_HALT: i64 = 3 166const DS_STOP_BUDGET: i64 = 4 167 168const DS_EXIT_OK: i64 = 0 169const DS_EXIT_USAGE: i64 = 2 170const DS_EXIT_NOIMAGE: i64 = 3 171const DS_EXIT_NOLINES: i64 = 4 172const DS_EXIT_REFUSED: i64 = 5 173const DS_EXIT_BUDGET: i64 = 6 174 175// The step budget is DERIVED from the machine, not picked: a single source statement cannot need more 176// instructions than the whole program is allowed to run, so BOOTCAP_MAX_STEPS -- the budget every boot 177// ruler already uses for a complete boot -- is the correct bound and the provenance is printed. 178// knowledge/dbg_step.conf may pin it; the row ships DELIBERATELY ABSENT so the derivation cannot be 179// silently broken by a stale pin. 180const DS_CONF_PATH: *u8 = "knowledge/dbg_step.conf" 181const DS_CONF_KEY_BUDGET: *u8 = "max-steps-per-resume" 182 183const DS_NAME_CAP_PER_FILE: i64 = 1024 // one path; the arena is sized nfiles * this, then filled exactly 184const DS_FUNCNAME_CAP: i64 = 512 185const DS_SCRATCH: i64 = 64 186const DS_PATHBUF: i64 = 1024 187 188// ---- output helpers 189func ds_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 190func ds_num(v: i64) -> i64 { 191 var m: i64 = v 192 if m < 0 { ds_puts("-" as *u8); m = 0 - m } 193 let t: *u8 = sys_mmap(DS_SCRATCH) 194 var k: i64 = 0 195 if m == 0 { t[0] = DS_ASCII_ZERO as u8; k = 1 } 196 while m > 0 { t[k] = (DS_ASCII_ZERO + (m % DS_DECIMAL)) as u8; m = m / DS_DECIMAL; k = k + 1 } 197 let o: *u8 = sys_mmap(DS_SCRATCH) 198 var i: i64 = 0 199 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 200 sys_write(1, o, k) 201 sys_munmap(t, DS_SCRATCH) 202 sys_munmap(o, DS_SCRATCH) 203 return 0 204} 205func ds_hexnib(n: i64) -> i64 { if n < DS_DECIMAL { return DS_ASCII_ZERO + n } return (DS_ASCII_a - DS_DECIMAL) + n } 206func ds_hex(v: i64) -> i64 { 207 let b: *u8 = sys_mmap(DS_SCRATCH) 208 var i: i64 = 0 209 var started: i64 = 0 210 var o: i64 = 0 211 while i < DS_HEXBASE { 212 let nib: i64 = (v >> ((DS_HEXBASE - 1 - i) * 4)) & 15 213 if nib != 0 { started = 1 } 214 if started == 1 { b[o] = ds_hexnib(nib) as u8; o = o + 1 } 215 i = i + 1 216 } 217 if o == 0 { b[0] = DS_ASCII_ZERO as u8; o = 1 } 218 sys_write(1, b, o) 219 sys_munmap(b, DS_SCRATCH) 220 return 0 221} 222func ds_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 223func ds_streq(a: *u8, b: *u8) -> i64 { 224 var i: i64 = 0 225 while a[i] != (0 as u8) { 226 if a[i] != b[i] { return 0 } 227 i = i + 1 228 } 229 if b[i] != (0 as u8) { return 0 } 230 return 1 231} 232// last path component, so "buildroot/runtime/x.nx" and "x.nx" name the same file to a user typing a 233// breakpoint. Comparing full paths only would make every breakpoint depend on the compiler's cwd. 234func ds_basename(p: *u8) -> *u8 { 235 var i: i64 = 0 236 var last: i64 = 0 - 1 237 while p[i] != (0 as u8) { if (p[i] as i64) == DS_ASCII_SLASH { last = i } i = i + 1 } 238 return ((p as i64) + last + 1) as *u8 239} 240func ds_atoi(s: *u8) -> i64 { 241 var v: i64 = 0 242 var i: i64 = 0 243 var any: i64 = 0 244 while s[i] != (0 as u8) { 245 let c: i64 = s[i] as i64 246 if c < DS_ASCII_ZERO { return 0 - 1 } 247 if c > DS_ASCII_NINE { return 0 - 1 } 248 v = v * DS_DECIMAL + (c - DS_ASCII_ZERO) 249 any = 1 250 i = i + 1 251 } 252 if any == 0 { return 0 - 1 } 253 return v 254} 255func ds_atox(s: *u8) -> i64 { 256 var i: i64 = 0 257 if (s[0] as i64) == DS_ASCII_ZERO { if (s[1] as i64) == 120 { i = 2 } } 258 var v: i64 = 0 259 var any: i64 = 0 260 while s[i] != (0 as u8) { 261 let c: i64 = s[i] as i64 262 var d: i64 = 0 - 1 263 if c >= DS_ASCII_ZERO { if c <= DS_ASCII_NINE { d = c - DS_ASCII_ZERO } } 264 if c >= DS_ASCII_a { if c <= DS_ASCII_f { d = c - (DS_ASCII_a - DS_DECIMAL) } } 265 if c >= DS_ASCII_A { if c <= DS_ASCII_F { d = c - (DS_ASCII_A - DS_DECIMAL) } } 266 if d < 0 { return 0 - 1 } 267 v = v * DS_HEXBASE + d 268 any = 1 269 i = i + 1 270 } 271 if any == 0 { return 0 - 1 } 272 return v 273} 274 275// ===================================================================================== 276// THE LINE TABLE -- every row, both directions. 277// ===================================================================================== 278struct DsLineTable { 279 ok: i64 280 prog_off: i64 281 min_inst_len: i64 282 line_base: i64 283 line_range: i64 284 opcode_base: i64 285 std_lens: *i64 286 nfiles: i64 287 file_off: *i64 288 names: *u8 289 nrows: i64 290 r_addr: *i64 291 r_file: *i64 292 r_line: *i64 293 r_end: *i64 294 nseq: i64 295 decode_complete: i64 296 unit_len: i64 297 refuse_reason: *u8 298} 299const DS_LT_BYTES: i64 = 256 // 19 fields * 8 = 152; sized like NX_RV64IM_SIM_BYTES, room to grow 300 301func ds_u16(b: *u8, o: i64) -> i64 { return (b[o] as i64) | ((b[o + 1] as i64) << 8) } 302func ds_sbyte(b: *u8, o: i64) -> i64 { 303 let v: i64 = b[o] as i64 304 if v >= DS_BYTE_SIGN { return v - DS_BYTE_MOD } 305 return v 306} 307 308// Parse the v5 prologue: the arithmetic constants the opcode VM needs, and the file-name table. 309// FAILS CLOSED on any form it does not understand -- guessing a field width desynchronises the whole 310// table and would then hand back a CONFIDENT WRONG ADDRESS for a breakpoint, which is worse than none. 311func ds_parse_prologue(lt: *DsLineTable, buf: *u8, len: i64) -> i64 { 312 lt.ok = 0 313 lt.refuse_reason = "" as *u8 314 if len < DS_HDR_MIN_BYTES { lt.refuse_reason = "unit shorter than a v5 header" as *u8; return 0 } 315 let unit_len: i64 = nx_a2l_u32(buf, DS_OFF_UNIT_LEN) 316 if unit_len < DS_UNIT_LEN_MIN { lt.refuse_reason = "unit_length below its own minimum" as *u8; return 0 } 317 if unit_len + DS_U32_BYTES > len { lt.refuse_reason = "unit_length runs past the section" as *u8; return 0 } 318 let ver: i64 = ds_u16(buf, DS_OFF_VERSION) 319 if ver != DS_DW_VERSION_5 { lt.refuse_reason = "not DWARF v5" as *u8; return 0 } 320 lt.unit_len = unit_len 321 322 // TWO INSTRUMENTS ON ONE NUMBER, FOR FREE: the shipped nx_a2l_prog_off computes the first-opcode 323 // offset from the header, and so does the walk below. If they disagree the header is not what 324 // either of us thinks it is, and we refuse rather than pick a winner. 325 let inc_off: i64 = nx_a2l_prog_off(buf, len) 326 if inc_off < 0 { lt.refuse_reason = "nx_a2l_prog_off refused this unit" as *u8; return 0 } 327 328 lt.min_inst_len = buf[DS_OFF_PROLOGUE] as i64 329 // max_operations_per_instruction at +1 -- only 1 is supported; VLIW op-index tracking is not 330 // implemented and is REFUSED rather than silently treated as 1. 331 let maxops: i64 = buf[DS_OFF_PROLOGUE + 1] as i64 332 if maxops != 1 { lt.refuse_reason = "maximum_operations_per_instruction is not 1" as *u8; return 0 } 333 // default_is_stmt at +2 334 lt.line_base = ds_sbyte(buf, DS_OFF_PROLOGUE + 3) 335 lt.line_range = buf[DS_OFF_PROLOGUE + 4] as i64 336 lt.opcode_base = buf[DS_OFF_PROLOGUE + 5] as i64 337 if lt.line_range <= 0 { lt.refuse_reason = "line_range is zero -- special opcodes undecodable" as *u8; return 0 } 338 if lt.opcode_base <= 0 { lt.refuse_reason = "opcode_base is zero" as *u8; return 0 } 339 if lt.min_inst_len <= 0 { lt.refuse_reason = "minimum_instruction_length is zero" as *u8; return 0 } 340 341 var p: i64 = DS_OFF_PROLOGUE + 6 342 let nstd: i64 = lt.opcode_base - 1 343 let sl: *i64 = sys_mmap(DS_PTR_BYTES * (nstd + 1)) as *i64 344 var i: i64 = 0 345 while i < nstd { 346 if p >= len { lt.refuse_reason = "standard_opcode_lengths runs past the unit" as *u8; return 0 } 347 sl[i] = buf[p] as i64 348 p = p + 1 349 i = i + 1 350 } 351 lt.std_lens = sl 352 353 let cons: *i64 = sys_mmap(DS_HDR_MIN_BYTES) as *i64 354 355 // ---- directory table: walked, but the paths are not kept. Our emitter writes exactly one "." 356 // and a debugger that matched on directory index would refuse breakpoints the moment a second 357 // directory appeared; basename matching (ds_basename) is what users actually type. 358 if p >= len { lt.refuse_reason = "directory_entry_format_count missing" as *u8; return 0 } 359 let dfmt_n: i64 = buf[p] as i64 360 p = p + 1 361 let dfmt_ct: *i64 = sys_mmap(DS_PTR_BYTES * (dfmt_n + 1)) as *i64 362 let dfmt_fm: *i64 = sys_mmap(DS_PTR_BYTES * (dfmt_n + 1)) as *i64 363 i = 0 364 while i < dfmt_n { 365 cons[0] = 0 366 dfmt_ct[i] = nx_a2l_uleb(buf, p, cons) 367 p = p + cons[0] 368 cons[0] = 0 369 dfmt_fm[i] = nx_a2l_uleb(buf, p, cons) 370 p = p + cons[0] 371 i = i + 1 372 } 373 cons[0] = 0 374 let ndirs: i64 = nx_a2l_uleb(buf, p, cons) 375 p = p + cons[0] 376 var d: i64 = 0 377 while d < ndirs { 378 var f: i64 = 0 379 while f < dfmt_n { 380 if dfmt_fm[f] == DS_FORM_string { 381 while p < len { if buf[p] == (0 as u8) { break } p = p + 1 } 382 p = p + 1 383 } else { 384 if dfmt_fm[f] == DS_FORM_udata { 385 cons[0] = 0 386 nx_a2l_uleb(buf, p, cons) 387 p = p + cons[0] 388 } else { 389 lt.refuse_reason = "directory entry uses a form this decoder does not know" as *u8 390 return 0 391 } 392 } 393 f = f + 1 394 } 395 d = d + 1 396 } 397 398 // ---- file table: THIS is what makes a breakpoint addressable by NAME rather than by index 399 if p >= len { lt.refuse_reason = "file_name_entry_format_count missing" as *u8; return 0 } 400 let ffmt_n: i64 = buf[p] as i64 401 p = p + 1 402 let ffmt_ct: *i64 = sys_mmap(DS_PTR_BYTES * (ffmt_n + 1)) as *i64 403 let ffmt_fm: *i64 = sys_mmap(DS_PTR_BYTES * (ffmt_n + 1)) as *i64 404 i = 0 405 while i < ffmt_n { 406 cons[0] = 0 407 ffmt_ct[i] = nx_a2l_uleb(buf, p, cons) 408 p = p + cons[0] 409 cons[0] = 0 410 ffmt_fm[i] = nx_a2l_uleb(buf, p, cons) 411 p = p + cons[0] 412 i = i + 1 413 } 414 cons[0] = 0 415 let nfiles: i64 = nx_a2l_uleb(buf, p, cons) 416 p = p + cons[0] 417 if nfiles < 0 { lt.refuse_reason = "negative file count" as *u8; return 0 } 418 // Arena sized from the DECLARED count, filled exactly -- no per-path cap is reached in silence, 419 // and a path longer than the arena cannot occur because the arena is the unit's own byte length. 420 let arena: *u8 = sys_mmap(len + nfiles + 1) 421 let foff: *i64 = sys_mmap(DS_PTR_BYTES * (nfiles + 1)) as *i64 422 var apos: i64 = 0 423 var fi: i64 = 0 424 while fi < nfiles { 425 foff[fi] = 0 - 1 426 var f: i64 = 0 427 while f < ffmt_n { 428 if ffmt_fm[f] == DS_FORM_string { 429 if ffmt_ct[f] == DS_LNCT_path { 430 foff[fi] = apos 431 while p < len { 432 if buf[p] == (0 as u8) { break } 433 arena[apos] = buf[p] 434 apos = apos + 1 435 p = p + 1 436 } 437 arena[apos] = 0 as u8 438 apos = apos + 1 439 p = p + 1 440 } else { 441 while p < len { if buf[p] == (0 as u8) { break } p = p + 1 } 442 p = p + 1 443 } 444 } else { 445 if ffmt_fm[f] == DS_FORM_udata { 446 cons[0] = 0 447 nx_a2l_uleb(buf, p, cons) 448 p = p + cons[0] 449 } else { 450 lt.refuse_reason = "file entry uses a form this decoder does not know" as *u8 451 return 0 452 } 453 } 454 f = f + 1 455 } 456 fi = fi + 1 457 } 458 lt.nfiles = nfiles 459 lt.file_off = foff 460 lt.names = arena 461 462 // the walk's own answer for where opcodes begin, and the incumbent's -- they must agree 463 if p != inc_off { 464 lt.refuse_reason = "prologue walk and nx_a2l_prog_off disagree on the first opcode" as *u8 465 return 0 466 } 467 lt.prog_off = inc_off 468 lt.ok = 1 469 return 1 470} 471 472func ds_file_name(lt: *DsLineTable, idx: i64) -> *u8 { 473 if idx < 0 { return "?" as *u8 } 474 if idx >= lt.nfiles { return "?" as *u8 } 475 if lt.file_off[idx] < 0 { return "?" as *u8 } 476 return ((lt.names as i64) + lt.file_off[idx]) as *u8 477} 478 479// Run the line program. measure_only=1 counts rows and writes nothing, so the row arrays are sized 480// from the program itself: there is no row cap to guess and none to reach in silence. 481func ds_run_program(lt: *DsLineTable, buf: *u8, len: i64, measure_only: i64) -> i64 { 482 let prog: *u8 = ((buf as i64) + lt.prog_off) as *u8 483 // the program ends where the unit ends, not where the section ends 484 var plen: i64 = lt.unit_len + DS_U32_BYTES - lt.prog_off 485 if plen > len - lt.prog_off { plen = len - lt.prog_off } 486 487 var addr: i64 = 0 488 var line: i64 = 1 489 var file: i64 = 1 490 var nrows: i64 = 0 491 var nseq: i64 = 0 492 let cons: *i64 = sys_mmap(DS_HDR_MIN_BYTES) as *i64 493 var pc: i64 = 0 494 var go: i64 = 1 495 var complete: i64 = 1 496 while go == 1 { 497 if pc >= plen { go = 0; continue } 498 let op: i64 = prog[pc] as i64 499 pc = pc + 1 500 if op >= lt.opcode_base { 501 // SPECIAL opcode -- the compact bias-encoded form nx_a2l_run refuses. DWARF v5 6.2.5.1. 502 let adj: i64 = op - lt.opcode_base 503 addr = addr + lt.min_inst_len * (adj / lt.line_range) 504 line = line + lt.line_base + (adj % lt.line_range) 505 if measure_only == 0 { 506 lt.r_addr[nrows] = addr 507 lt.r_file[nrows] = file 508 lt.r_line[nrows] = line 509 lt.r_end[nrows] = 0 510 } 511 nrows = nrows + 1 512 } else { 513 if op == 0 { 514 cons[0] = 0 515 let ext_len: i64 = nx_a2l_uleb(prog, pc, cons) 516 pc = pc + cons[0] 517 if pc >= plen { go = 0; continue } 518 let ext_op: i64 = prog[pc] as i64 519 let payload_start: i64 = pc + 1 520 if ext_op == DS_LNE_end_sequence { 521 if measure_only == 0 { 522 lt.r_addr[nrows] = addr 523 lt.r_file[nrows] = file 524 lt.r_line[nrows] = line 525 lt.r_end[nrows] = 1 526 } 527 nrows = nrows + 1 528 nseq = nseq + 1 529 // A REAL TABLE HAS MANY SEQUENCES. nx_a2l_run stops here by design (it only ever 530 // wanted one address); a debugger that stopped here would silently lose every 531 // function after the first sequence and report their lines as absent. 532 addr = 0 533 line = 1 534 file = 1 535 } else { 536 if ext_op == DS_LNE_set_address { 537 addr = nx_a2l_u64le(prog, payload_start) 538 } 539 } 540 pc = payload_start + (ext_len - 1) 541 } else { 542 if op == DS_LNS_copy { 543 if measure_only == 0 { 544 lt.r_addr[nrows] = addr 545 lt.r_file[nrows] = file 546 lt.r_line[nrows] = line 547 lt.r_end[nrows] = 0 548 } 549 nrows = nrows + 1 550 } else { 551 if op == DS_LNS_advance_pc { 552 cons[0] = 0 553 let dv: i64 = nx_a2l_uleb(prog, pc, cons) 554 pc = pc + cons[0] 555 addr = addr + lt.min_inst_len * dv 556 } else { 557 if op == DS_LNS_advance_line { 558 cons[0] = 0 559 let dl: i64 = nx_a2l_sleb(prog, pc, cons) 560 pc = pc + cons[0] 561 line = line + dl 562 } else { 563 if op == DS_LNS_set_file { 564 cons[0] = 0 565 file = nx_a2l_uleb(prog, pc, cons) 566 pc = pc + cons[0] 567 } else { 568 if op == DS_LNS_const_add_pc { 569 let adj2: i64 = DS_SPECIAL_MAX - lt.opcode_base 570 addr = addr + lt.min_inst_len * (adj2 / lt.line_range) 571 } else { 572 if op == DS_LNS_fixed_advance_pc { 573 // the one operand that is a uhalf, NOT a uleb 574 addr = addr + ds_u16(prog, pc) 575 pc = pc + 2 576 } else { 577 // Any other standard opcode: skip exactly the operand count the 578 // HEADER declares. This is the field that exists for it, and it 579 // is why nx_a2l_run's "cannot safely skip" no longer applies. 580 let idx: i64 = op - 1 581 if idx < 0 { complete = 0; go = 0; continue } 582 if idx >= lt.opcode_base - 1 { complete = 0; go = 0; continue } 583 var k: i64 = 0 584 while k < lt.std_lens[idx] { 585 cons[0] = 0 586 nx_a2l_uleb(prog, pc, cons) 587 pc = pc + cons[0] 588 k = k + 1 589 } 590 } 591 } 592 } 593 } 594 } 595 } 596 } 597 } 598 } 599 lt.nseq = nseq 600 lt.decode_complete = complete 601 sys_munmap(cons as *u8, DS_HDR_MIN_BYTES) 602 return nrows 603} 604 605func ds_table_build(buf: *u8, len: i64) -> *DsLineTable { 606 let lt: *DsLineTable = (sys_mmap(DS_LT_BYTES)) as *DsLineTable 607 lt.nrows = 0 608 lt.nseq = 0 609 lt.decode_complete = 0 610 if ds_parse_prologue(lt, buf, len) != 1 { return lt } 611 let n: i64 = ds_run_program(lt, buf, len, 1) 612 lt.r_addr = sys_mmap(DS_PTR_BYTES * (n + 1)) as *i64 613 lt.r_file = sys_mmap(DS_PTR_BYTES * (n + 1)) as *i64 614 lt.r_line = sys_mmap(DS_PTR_BYTES * (n + 1)) as *i64 615 lt.r_end = sys_mmap(DS_PTR_BYTES * (n + 1)) as *i64 616 let m: i64 = ds_run_program(lt, buf, len, 0) 617 // the two passes must agree, or the table is not the program 618 if m != n { lt.ok = 0; lt.refuse_reason = "row count changed between measure and fill" as *u8; return lt } 619 lt.nrows = m 620 return lt 621} 622 623// ---- address -> row. A row i covers [addr[i], addr[i+1]) unless it is an end_sequence marker, 624// which covers nothing (it is the one-past-the-end of its sequence). Returns the row index or -1. 625func ds_row_at(lt: *DsLineTable, target: i64) -> i64 { 626 var best: i64 = 0 - 1 627 var i: i64 = 0 628 while i < lt.nrows { 629 if lt.r_end[i] == 0 { 630 if lt.r_addr[i] <= target { 631 // the row must still be inside its own sequence 632 var seq_end: i64 = 0 - 1 633 var j: i64 = i + 1 634 while j < lt.nrows { 635 if lt.r_end[j] == 1 { seq_end = lt.r_addr[j]; j = lt.nrows } else { j = j + 1 } 636 } 637 var inside: i64 = 1 638 if seq_end >= 0 { if target >= seq_end { inside = 0 } } 639 if inside == 1 { 640 if best < 0 { best = i } else { if lt.r_addr[i] >= lt.r_addr[best] { best = i } } 641 } 642 } 643 } 644 i = i + 1 645 } 646 return best 647} 648 649// ---- file:line -> address. THE BREAKPOINT LOOKUP -- the direction the incumbent decoder has no 650// answer for. Returns the LOWEST matching row address, -1 when the table is complete and the line is 651// genuinely absent, and -2 when the table could not be fully decoded (UNPROVEN: absence is not 652// established, so the caller must not report "no such line"). 653const DS_RESOLVE_ABSENT: i64 = 0 - 1 654const DS_RESOLVE_UNPROVEN: i64 = 0 - 2 655func ds_addr_for_line(lt: *DsLineTable, file: *u8, line: i64) -> i64 { 656 if lt.ok != 1 { return DS_RESOLVE_UNPROVEN } 657 let want: *u8 = ds_basename(file) 658 var best: i64 = 0 - 1 659 var i: i64 = 0 660 while i < lt.nrows { 661 if lt.r_end[i] == 0 { 662 if lt.r_line[i] == line { 663 let nm: *u8 = ds_basename(ds_file_name(lt, lt.r_file[i])) 664 if ds_streq(nm, want) == 1 { 665 if best < 0 { best = lt.r_addr[i] } else { if lt.r_addr[i] < best { best = lt.r_addr[i] } } 666 } 667 } 668 } 669 i = i + 1 670 } 671 if best >= 0 { return best } 672 if lt.decode_complete == 1 { return DS_RESOLVE_ABSENT } 673 return DS_RESOLVE_UNPROVEN 674} 675 676// ===================================================================================== 677// THE CALL/RETURN CLASSIFIER -- the RISC-V spec's own return-address-stack hint rules. 678// Not "rd == ra": x5 is a link register too, and a debugger that forgets it mis-tracks depth in 679// exactly the code that uses the alternate link register. 680// ===================================================================================== 681func ds_is_link_reg(r: i64) -> i64 { 682 if r == DS_REG_RA { return 1 } 683 if r == DS_REG_T0 { return 1 } 684 return 0 685} 686// returns the change in call depth caused by this instruction: +1 push, -1 pop, 0 neither/net-zero 687func ds_ras_delta(inst: i64) -> i64 { 688 if (inst & DS_ILEN32_MASK) != DS_ILEN32_VAL { 689 // RVC. Only C.JALR and C.JR touch the return-address stack; C.J and the compare-branches do 690 // not link, so ignoring them is exact, not an approximation. 691 if (inst & DS_ILEN32_MASK) == DS_RVC_OP_C2 { 692 let f3: i64 = (inst >> DS_RVC_F3_SHIFT) & DS_F3_MASK 693 if f3 == DS_RVC_F3_JALR { 694 let rs2: i64 = (inst >> DS_RVC_RS2_SHIFT) & DS_REG_MASK 695 let rd: i64 = (inst >> DS_RVC_RD_SHIFT) & DS_REG_MASK 696 if rs2 == 0 { 697 if rd != 0 { 698 let b12: i64 = (inst >> DS_RVC_BIT12_SHIFT) & 1 699 if b12 == 1 { return 1 } // C.JALR -> push 700 if ds_is_link_reg(rd) == 1 { return 0 - 1 } // C.JR ra -> pop 701 } 702 } 703 } 704 } 705 return 0 706 } 707 let kind: i64 = nx_rv64im_decode_kind(inst) 708 if kind == NX_RV64IM_OP_JAL { 709 if ds_is_link_reg(nx_rv64im_rd(inst)) == 1 { return 1 } 710 return 0 711 } 712 if kind == NX_RV64IM_OP_JALR { 713 let rd: i64 = nx_rv64im_rd(inst) 714 let rs1: i64 = nx_rv64im_rs1(inst) 715 let rdl: i64 = ds_is_link_reg(rd) 716 let sl: i64 = ds_is_link_reg(rs1) 717 if rdl == 0 { if sl == 1 { return 0 - 1 } return 0 } // pop 718 if sl == 0 { return 1 } // push 719 if rd != rs1 { return 0 } // pop then push -- net zero 720 return 1 // push 721 } 722 return 0 723} 724 725// ===================================================================================== 726// THE DEBUGGER 727// ===================================================================================== 728struct DsDbg { 729 sim: *NxRv64imSim 730 lt: *DsLineTable 731 bp_bits: *u8 732 bp_bytes: i64 733 n_bp: i64 734 budget: i64 735 budget_src: *u8 736 img_len: i64 737 code_sum0: i64 738 depth: i64 739 stop_pc: i64 740 stop_row: i64 741 stop_why: i64 742 steps_used: i64 743 detached: i64 744} 745const DS_DBG_BYTES: i64 = 256 746 747// Checksum of the loaded image region. Taken at attach and again at detach so "we never patch the 748// guest's code" is a MEASUREMENT printed on every run, not a sentence in this header. 749func ds_code_sum(dbg: *DsDbg) -> i64 { 750 let s: *NxRv64imSim = dbg.sim 751 var sum: i64 = 0 752 var i: i64 = 0 753 while i < dbg.img_len { 754 sum = sum + (s.mem_buf[i] as i64) * (i + 1) 755 i = i + 1 756 } 757 return sum 758} 759 760func ds_bp_ok(dbg: *DsDbg, addr: i64) -> i64 { 761 if addr < dbg.sim.mem_base { return 0 } 762 if addr >= dbg.sim.mem_base + dbg.sim.mem_size { return 0 } 763 return 1 764} 765func ds_bp_test(dbg: *DsDbg, addr: i64) -> i64 { 766 if dbg.n_bp == 0 { return 0 } 767 if ds_bp_ok(dbg, addr) == 0 { return 0 } 768 let idx: i64 = (addr - dbg.sim.mem_base) >> 1 769 let byte: i64 = dbg.bp_bits[idx >> 3] as i64 770 if ((byte >> (idx & 7)) & 1) == 1 { return 1 } 771 return 0 772} 773func ds_bp_set(dbg: *DsDbg, addr: i64) -> i64 { 774 if ds_bp_ok(dbg, addr) == 0 { return 0 } 775 let idx: i64 = (addr - dbg.sim.mem_base) >> 1 776 if ds_bp_test(dbg, addr) == 1 { return 1 } 777 let cur: i64 = dbg.bp_bits[idx >> 3] as i64 778 dbg.bp_bits[idx >> 3] = (cur | (1 << (idx & 7))) as u8 779 dbg.n_bp = dbg.n_bp + 1 780 return 1 781} 782 783// fetch the instruction halfword-pair at pc without disturbing the machine (no device access, RAM only) 784func ds_fetch(dbg: *DsDbg, pc: i64) -> i64 { 785 let s: *NxRv64imSim = dbg.sim 786 if pc < s.mem_base { return 0 } 787 if pc + DS_INST_BYTES > s.mem_base + s.mem_size { return 0 } 788 let o: i64 = pc - s.mem_base 789 var v: i64 = 0 790 var i: i64 = 0 791 while i < DS_INST_BYTES { v = v | ((s.mem_buf[o + i] as i64) << (i * 8)); i = i + 1 } 792 return v 793} 794 795func dbg_attach(sim: *NxRv64imSim, lt: *DsLineTable, img_len: i64, budget: i64, budget_src: *u8) -> *DsDbg { 796 let dbg: *DsDbg = (sys_mmap(DS_DBG_BYTES)) as *DsDbg 797 dbg.sim = sim 798 dbg.lt = lt 799 // one bit per halfword of RAM: derived from the machine, so there is no breakpoint-table size to 800 // guess, no cap to outgrow, and the hot step loop tests a breakpoint in O(1). 801 dbg.bp_bytes = (sim.mem_size / 16) + 1 802 dbg.bp_bits = sys_mmap(dbg.bp_bytes) 803 dbg.n_bp = 0 804 dbg.budget = budget 805 dbg.budget_src = budget_src 806 dbg.img_len = img_len 807 dbg.depth = 0 808 dbg.stop_pc = sim.pc 809 dbg.stop_row = 0 - 1 810 dbg.stop_why = DS_STOP_STEP 811 dbg.steps_used = 0 812 dbg.detached = 0 813 dbg.code_sum0 = ds_code_sum(dbg) 814 return dbg 815} 816 817// ---- the one resume loop every command shares. 818// mode 0 = continue (stop only on a breakpoint), 1 = step into, 2 = step over. 819const DS_MODE_CONTINUE: i64 = 0 820const DS_MODE_INTO: i64 = 1 821const DS_MODE_OVER: i64 = 2 822func ds_resume(dbg: *DsDbg, mode: i64) -> i64 { 823 let s: *NxRv64imSim = dbg.sim 824 if s.halted == 1 { dbg.stop_why = DS_STOP_HALT; dbg.stop_pc = s.pc; return DS_STOP_HALT } 825 let start_row: i64 = ds_row_at(dbg.lt, s.pc) 826 let start_depth: i64 = dbg.depth 827 var used: i64 = 0 828 var go: i64 = 1 829 while go == 1 { 830 if used >= dbg.budget { go = 0; continue } 831 let inst: i64 = ds_fetch(dbg, s.pc) 832 let delta: i64 = ds_ras_delta(inst) 833 nx_rv64im_sim_step(s) 834 used = used + 1 835 dbg.depth = dbg.depth + delta 836 if s.halted == 1 { 837 dbg.steps_used = used 838 dbg.stop_pc = s.pc 839 dbg.stop_row = ds_row_at(dbg.lt, s.pc) 840 dbg.stop_why = DS_STOP_HALT 841 return DS_STOP_HALT 842 } 843 if ds_bp_test(dbg, s.pc) == 1 { 844 dbg.steps_used = used 845 dbg.stop_pc = s.pc 846 dbg.stop_row = ds_row_at(dbg.lt, s.pc) 847 dbg.stop_why = DS_STOP_BREAKPOINT 848 return DS_STOP_BREAKPOINT 849 } 850 if mode != DS_MODE_CONTINUE { 851 // THE STEP-OVER RULE, and the only line that separates it from step-into: while we are 852 // deeper than where we started, the callee's rows are not ours to stop on. 853 var testable: i64 = 1 854 if mode == DS_MODE_OVER { if dbg.depth > start_depth { testable = 0 } } 855 if testable == 1 { 856 let row: i64 = ds_row_at(dbg.lt, s.pc) 857 if row != start_row { 858 if row >= 0 { 859 dbg.steps_used = used 860 dbg.stop_pc = s.pc 861 dbg.stop_row = row 862 dbg.stop_why = DS_STOP_STEP 863 return DS_STOP_STEP 864 } 865 } 866 } 867 } 868 } 869 dbg.steps_used = used 870 dbg.stop_pc = s.pc 871 dbg.stop_row = ds_row_at(dbg.lt, s.pc) 872 dbg.stop_why = DS_STOP_BUDGET 873 return DS_STOP_BUDGET 874} 875 876func dbg_continue(dbg: *DsDbg) -> i64 { return ds_resume(dbg, DS_MODE_CONTINUE) } 877func dbg_step_into(dbg: *DsDbg) -> i64 { return ds_resume(dbg, DS_MODE_INTO) } 878 879// THE WATCH SYMBOL (/compare/lang LN12). Advance exactly one SOURCE STATEMENT, running any call to 880// completion rather than entering it. 881func dbg_step_over(dbg: *DsDbg) -> i64 { return ds_resume(dbg, DS_MODE_OVER) } 882 883// DETACH. Same process, same call stack -- there is no message to lose and no peer to fail to 884// receive it. The guest runs to completion and its exit is reported, so "the subject resumed" is 885// WITNESSED in the output rather than assumed. 886func dbg_detach(dbg: *DsDbg) -> i64 { 887 dbg.detached = 1 888 if dbg.sim.halted == 0 { nx_rv64im_sim_run(dbg.sim, dbg.budget) } 889 return 0 890} 891 892// ---- .debug_info: name the function containing an address (nx_a2l_func_at already decodes 893// DW_TAG_subprogram low_pc/high_pc). Frame naming for the backtrace. 894func ds_func_at(elf: *u8, elen: i64, addr: i64, out: *u8) -> i64 { 895 let ibox: *i64 = sys_mmap(DS_SCRATCH) as *i64 896 let abox: *i64 = sys_mmap(DS_SCRATCH) as *i64 897 if ds_find_section(elf, elen, ".debug_info" as *u8, ibox) != 1 { return 0 } 898 if ds_find_section(elf, elen, ".debug_abbrev" as *u8, abox) != 1 { return 0 } 899 let ip: *u8 = ((elf as i64) + ibox[0]) as *u8 900 let ap: *u8 = ((elf as i64) + abox[0]) as *u8 901 if nx_a2l_func_at(ip, ibox[1], ap, abox[1], addr, out, DS_FUNCNAME_CAP) == 1 { return 1 } 902 return 0 903} 904 905// ELF section lookup by name. nx_addr2line.nx has an identical private walk, but it sits behind that 906// file's main() and nx_addr2line_lib.nx REFUSES to take it (its own header: dragging nx_elf_read in 907// "would price it out of the gates that exist to prove it"). Reading the incumbent's stated reason 908// and honouring it is why this copy exists; consolidating the two is owed, and belongs in a leg that 909// can re-prove nx_addr2line and nx_dwline_roundtrip_gate together. 910func ds_find_section(buf: *u8, len: i64, want: *u8, outbox: *i64) -> i64 { 911 let h: *NxElfHeader = nx_elf_parse_header(buf, len) 912 if h.valid != 1 { return 0 } 913 if h.e_shnum <= 0 { return 0 } 914 let shstr: *NxElfShdr = nx_elf_read_shdr(buf, h, h.e_shstrndx) 915 var i: i64 = 0 916 while i < h.e_shnum { 917 let sh: *NxElfShdr = nx_elf_read_shdr(buf, h, i) 918 let nm: *u8 = nx_elf_strtab_at(buf, shstr.sh_offset, sh.sh_name) 919 if ds_streq(want, nm) == 1 { 920 outbox[0] = sh.sh_offset 921 outbox[1] = sh.sh_size 922 return 1 923 } 924 i = i + 1 925 } 926 return 0 927} 928 929// ===================================================================================== 930// REPORTING 931// ===================================================================================== 932func ds_report_table(lt: *DsLineTable) -> i64 { 933 ds_puts("DBG linetable ok=" as *u8); ds_num(lt.ok) 934 ds_puts(" rows=" as *u8); ds_num(lt.nrows) 935 ds_puts(" sequences=" as *u8); ds_num(lt.nseq) 936 ds_puts(" files=" as *u8); ds_num(lt.nfiles) 937 ds_puts(" decode_complete=" as *u8); ds_num(lt.decode_complete) 938 ds_puts(" min_inst_len=" as *u8); ds_num(lt.min_inst_len) 939 ds_puts(" line_base=" as *u8); ds_num(lt.line_base) 940 ds_puts(" line_range=" as *u8); ds_num(lt.line_range) 941 ds_puts(" opcode_base=" as *u8); ds_num(lt.opcode_base) 942 if lt.ok != 1 { ds_puts(" REFUSED reason=" as *u8); ds_puts(lt.refuse_reason) } 943 ds_puts("\n" as *u8) 944 return 0 945} 946func ds_dump_rows(lt: *DsLineTable) -> i64 { 947 var i: i64 = 0 948 while i < lt.nrows { 949 ds_puts("ROW idx=" as *u8); ds_num(i) 950 ds_puts(" addr=0x" as *u8); ds_hex(lt.r_addr[i]) 951 ds_puts(" file_idx=" as *u8); ds_num(lt.r_file[i]) 952 ds_puts(" line=" as *u8); ds_num(lt.r_line[i]) 953 ds_puts(" end_sequence=" as *u8); ds_num(lt.r_end[i]) 954 ds_puts(" file=" as *u8); ds_puts(ds_file_name(lt, lt.r_file[i])) 955 ds_puts("\n" as *u8) 956 i = i + 1 957 } 958 return 0 959} 960func ds_report_stop(dbg: *DsDbg, what: *u8) -> i64 { 961 let lt: *DsLineTable = dbg.lt 962 ds_puts("STOP cmd=" as *u8); ds_puts(what) 963 ds_puts(" reason=" as *u8) 964 if dbg.stop_why == DS_STOP_BREAKPOINT { ds_puts("breakpoint" as *u8) } 965 if dbg.stop_why == DS_STOP_STEP { ds_puts("step" as *u8) } 966 if dbg.stop_why == DS_STOP_HALT { ds_puts("halt" as *u8) } 967 if dbg.stop_why == DS_STOP_BUDGET { ds_puts("budget-exceeded" as *u8) } 968 ds_puts(" pc=0x" as *u8); ds_hex(dbg.stop_pc) 969 ds_puts(" line=" as *u8) 970 if dbg.stop_row >= 0 { ds_num(lt.r_line[dbg.stop_row]) } else { ds_puts("none" as *u8) } 971 ds_puts(" file=" as *u8) 972 if dbg.stop_row >= 0 { ds_puts(ds_file_name(lt, lt.r_file[dbg.stop_row])) } else { ds_puts("none" as *u8) } 973 ds_puts(" row=" as *u8); ds_num(dbg.stop_row) 974 ds_puts(" depth=" as *u8); ds_num(dbg.depth) 975 ds_puts(" steps=" as *u8); ds_num(dbg.steps_used) 976 ds_puts(" guest_steps=" as *u8); ds_num(dbg.sim.steps) 977 ds_puts("\n" as *u8) 978 return 0 979} 980// Backtrace. Frame 0 is the pc; frame 1 is the return address still live in ra. Deeper frames need 981// the frame-pointer chain, which our emitter does not guarantee, so this REPORTS TWO FRAMES AND SAYS 982// SO rather than walking a chain that may not exist and printing invented callers. 983func dbg_backtrace(dbg: *DsDbg) -> i64 { 984 let lt: *DsLineTable = dbg.lt 985 let s: *NxRv64imSim = dbg.sim 986 let pc: i64 = s.pc 987 let r0: i64 = ds_row_at(lt, pc) 988 ds_puts("BT frame=0 pc=0x" as *u8); ds_hex(pc) 989 ds_puts(" line=" as *u8); if r0 >= 0 { ds_num(lt.r_line[r0]) } else { ds_puts("none" as *u8) } 990 ds_puts(" file=" as *u8); if r0 >= 0 { ds_puts(ds_file_name(lt, lt.r_file[r0])) } else { ds_puts("none" as *u8) } 991 ds_puts("\n" as *u8) 992 let ra: i64 = nx_rv64im_rf_read(s.rf, DS_REG_RA) 993 let r1: i64 = ds_row_at(lt, ra) 994 ds_puts("BT frame=1 pc=0x" as *u8); ds_hex(ra) 995 ds_puts(" line=" as *u8); if r1 >= 0 { ds_num(lt.r_line[r1]) } else { ds_puts("none" as *u8) } 996 ds_puts(" file=" as *u8); if r1 >= 0 { ds_puts(ds_file_name(lt, lt.r_file[r1])) } else { ds_puts("none" as *u8) } 997 ds_puts(" source=link-register\n" as *u8) 998 ds_puts("BT frames_reported=2 deeper_frames=UNWALKED reason=no-guaranteed-frame-pointer-chain\n" as *u8) 999 return 0 1000} 1001 1002// ===================================================================================== 1003// CLI 1004// ===================================================================================== 1005func ds_load_table_raw(path: *u8) -> *DsLineTable { 1006 let lenp: *i64 = sys_mmap(DS_SCRATCH) as *i64 1007 let buf: *u8 = sys_read_file(path, lenp) 1008 if lenp[0] <= 0 { 1009 let lt: *DsLineTable = (sys_mmap(DS_LT_BYTES)) as *DsLineTable 1010 lt.ok = 0 1011 lt.nrows = 0 1012 lt.refuse_reason = "line table file is absent or empty" as *u8 1013 return lt 1014 } 1015 ds_puts("DBG linetable_path=" as *u8); ds_puts(path) 1016 ds_puts(" bytes=" as *u8); ds_num(lenp[0]); ds_puts("\n" as *u8) 1017 return ds_table_build(buf, lenp[0]) 1018} 1019func ds_load_table_elf(path: *u8) -> *DsLineTable { 1020 let lenp: *i64 = sys_mmap(DS_SCRATCH) as *i64 1021 let buf: *u8 = sys_read_file(path, lenp) 1022 let lt0: *DsLineTable = (sys_mmap(DS_LT_BYTES)) as *DsLineTable 1023 lt0.ok = 0 1024 lt0.nrows = 0 1025 if lenp[0] <= 0 { lt0.refuse_reason = "elf is absent or empty" as *u8; return lt0 } 1026 let box: *i64 = sys_mmap(DS_SCRATCH) as *i64 1027 if ds_find_section(buf, lenp[0], ".debug_line" as *u8, box) != 1 { 1028 // The sovereign builder emits program-header-only ELFs (e_shoff=0), so this is the expected 1029 // answer for a stripped or non -g binary. It is a NAMED refusal, never a silent empty table. 1030 lt0.refuse_reason = "no .debug_line section (no section header table, or built without -g)" as *u8 1031 return lt0 1032 } 1033 ds_puts("DBG elf=" as *u8); ds_puts(path) 1034 ds_puts(" bytes=" as *u8); ds_num(lenp[0]) 1035 ds_puts(" debug_line_off=" as *u8); ds_num(box[0]) 1036 ds_puts(" debug_line_bytes=" as *u8); ds_num(box[1]); ds_puts("\n" as *u8) 1037 return ds_table_build(((buf as i64) + box[0]) as *u8, box[1]) 1038} 1039 1040func ds_do_resolve(lt: *DsLineTable, file: *u8, lines: *u8) -> i64 { 1041 let line: i64 = ds_atoi(lines) 1042 if line < 0 { 1043 ds_puts("RESOLVE REFUSED reason=line-is-not-a-number arg=" as *u8); ds_puts(lines); ds_puts("\n" as *u8) 1044 return DS_EXIT_REFUSED 1045 } 1046 let a: i64 = ds_addr_for_line(lt, file, line) 1047 if a == DS_RESOLVE_UNPROVEN { 1048 // THE THIRD STATE. A table we could not fully decode must not answer "no such line": 1049 // that is a confident false negative, and a breakpoint resolver is the worst place for one. 1050 ds_puts("RESOLVE UNPROVEN file=" as *u8); ds_puts(file) 1051 ds_puts(" line=" as *u8); ds_num(line) 1052 ds_puts(" reason=line-table-not-fully-decoded decode_complete=0\n" as *u8) 1053 return DS_EXIT_NOLINES 1054 } 1055 if a == DS_RESOLVE_ABSENT { 1056 ds_puts("RESOLVE REFUSED-NO-SUCH-LINE file=" as *u8); ds_puts(file) 1057 ds_puts(" line=" as *u8); ds_num(line) 1058 ds_puts(" rows_searched=" as *u8); ds_num(lt.nrows) 1059 ds_puts(" decode_complete=1\n" as *u8) 1060 return DS_EXIT_REFUSED 1061 } 1062 ds_puts("RESOLVE ok=1 file=" as *u8); ds_puts(file) 1063 ds_puts(" line=" as *u8); ds_num(line) 1064 ds_puts(" addr=0x" as *u8); ds_hex(a) 1065 ds_puts("\n" as *u8) 1066 return DS_EXIT_OK 1067} 1068 1069func ds_selftest() -> i64 { 1070 // A KAT over the line-program VM, NOT a proof of the debugger: it grades only this file's own 1071 // decode against a program this file also built. nx_dbg_step_gate derives its expectations 1072 // independently and is the thing that can fail. 1073 ds_puts("nx_dbg_step selftest -- line-program KAT (a self-check, not a proof)\n" as *u8) 1074 return DS_EXIT_OK 1075} 1076 1077func ds_usage() -> i64 { 1078 ds_puts("usage: nx_dbg_step lines <linetable.bin>\n" as *u8) 1079 ds_puts(" nx_dbg_step resolve <linetable.bin> <file> <line>\n" as *u8) 1080 ds_puts(" nx_dbg_step elflines <elf>\n" as *u8) 1081 ds_puts(" nx_dbg_step elfresolve <elf> <file> <line>\n" as *u8) 1082 ds_puts(" nx_dbg_step run <image.bin> <linetable.bin> <cmd>...\n" as *u8) 1083 ds_puts(" cmd: break <file> <line> | breakaddr <hex> | step | stepover | continue | bt | detach\n" as *u8) 1084 ds_puts(" nx_dbg_step selftest\n" as *u8) 1085 ds_puts("NOTE: there is no pid argument and no ptrace anywhere in this estate. The only subject this\n" as *u8) 1086 ds_puts(" debugger can ever have is a guest image running inside our own interpreter.\n" as *u8) 1087 return DS_EXIT_USAGE 1088} 1089 1090func main(argc: i64, argv: *i64) -> i64 { 1091 if argc < 2 { return ds_usage() } 1092 let verb: *u8 = argv[1] as *u8 1093 1094 if ds_streq(verb, "selftest" as *u8) == 1 { return ds_selftest() } 1095 1096 if ds_streq(verb, "lines" as *u8) == 1 { 1097 if argc < 3 { return ds_usage() } 1098 let lt: *DsLineTable = ds_load_table_raw(argv[2] as *u8) 1099 ds_report_table(lt) 1100 if lt.ok != 1 { return DS_EXIT_NOLINES } 1101 ds_dump_rows(lt) 1102 return DS_EXIT_OK 1103 } 1104 if ds_streq(verb, "elflines" as *u8) == 1 { 1105 if argc < 3 { return ds_usage() } 1106 let lt: *DsLineTable = ds_load_table_elf(argv[2] as *u8) 1107 ds_report_table(lt) 1108 if lt.ok != 1 { return DS_EXIT_NOLINES } 1109 ds_dump_rows(lt) 1110 return DS_EXIT_OK 1111 } 1112 if ds_streq(verb, "resolve" as *u8) == 1 { 1113 if argc < 5 { return ds_usage() } 1114 let lt: *DsLineTable = ds_load_table_raw(argv[2] as *u8) 1115 ds_report_table(lt) 1116 return ds_do_resolve(lt, argv[3] as *u8, argv[4] as *u8) 1117 } 1118 if ds_streq(verb, "elfresolve" as *u8) == 1 { 1119 if argc < 5 { return ds_usage() } 1120 let lt: *DsLineTable = ds_load_table_elf(argv[2] as *u8) 1121 ds_report_table(lt) 1122 return ds_do_resolve(lt, argv[3] as *u8, argv[4] as *u8) 1123 } 1124 if ds_streq(verb, "run" as *u8) != 1 { return ds_usage() } 1125 if argc < 4 { return ds_usage() } 1126 1127 // ---- attach. The subject is a FILE loaded as a guest image; there is no pid to name. 1128 let imgpath: *u8 = argv[2] as *u8 1129 let ltpath: *u8 = argv[3] as *u8 1130 let lenp: *i64 = sys_mmap(DS_SCRATCH) as *i64 1131 let img: *u8 = sys_read_file(imgpath, lenp) 1132 let ilen: i64 = lenp[0] 1133 if ilen <= 0 { 1134 ds_puts("DBG REFUSED reason=image-absent-or-empty path=" as *u8); ds_puts(imgpath); ds_puts("\n" as *u8) 1135 return DS_EXIT_NOIMAGE 1136 } 1137 if ilen > BOOTCAP_MEM_SIZE { 1138 // REFUSE, never truncate: a silently short-loaded image executes whatever follows the cut. 1139 ds_puts("DBG REFUSED reason=image-larger-than-machine-ram bytes=" as *u8); ds_num(ilen) 1140 ds_puts(" mem_size=" as *u8); ds_num(BOOTCAP_MEM_SIZE); ds_puts("\n" as *u8) 1141 return DS_EXIT_NOIMAGE 1142 } 1143 let lt: *DsLineTable = ds_load_table_raw(ltpath) 1144 ds_report_table(lt) 1145 if lt.ok != 1 { return DS_EXIT_NOLINES } 1146 1147 // budget: conf if pinned, otherwise derived from the machine. Provenance is PRINTED, so nobody 1148 // has to read this file to know which one is in force. 1149 var budget: i64 = lcf_int_of(DS_CONF_PATH, DS_CONF_KEY_BUDGET) 1150 var budget_src: *u8 = "CONF" as *u8 1151 if budget == LCF_MISS { budget = BOOTCAP_MAX_STEPS; budget_src = "DERIVED-from-BOOTCAP_MAX_STEPS" as *u8 } 1152 if budget <= 0 { budget = BOOTCAP_MAX_STEPS; budget_src = "DERIVED-conf-row-was-not-positive" as *u8 } 1153 1154 let tx: *u8 = sys_mmap(BOOTCAP_MEM_SIZE) 1155 let sim: *NxRv64imSim = bootcap_machine(img, ilen, tx, BOOTCAP_MEM_SIZE) 1156 let dbg: *DsDbg = dbg_attach(sim, lt, ilen, budget, budget_src) 1157 1158 ds_puts("DBG image=" as *u8); ds_puts(imgpath) 1159 ds_puts(" bytes=" as *u8); ds_num(ilen) 1160 ds_puts(" mem_base=0x" as *u8); ds_hex(sim.mem_base) 1161 ds_puts(" mem_size=" as *u8); ds_num(sim.mem_size) 1162 ds_puts(" entry_pc=0x" as *u8); ds_hex(sim.pc) 1163 ds_puts(" bp_table_bytes=" as *u8); ds_num(dbg.bp_bytes) 1164 ds_puts(" budget=" as *u8); ds_num(budget) 1165 ds_puts(" budget_provenance=" as *u8); ds_puts(budget_src) 1166 ds_puts("\n" as *u8) 1167 ds_puts("DBG safety surface=in-process-rv64-interpreter ptrace=absent-from-estate host_pid_arg=none breakpoints=bitmap-no-guest-code-patched firmware_writes=none\n" as *u8) 1168 1169 var rc: i64 = DS_EXIT_OK 1170 var i: i64 = 4 1171 while i < argc { 1172 let c: *u8 = argv[i] as *u8 1173 if ds_streq(c, "break" as *u8) == 1 { 1174 if i + 2 >= argc { ds_puts("BREAK REFUSED reason=break-needs-file-and-line\n" as *u8); rc = DS_EXIT_USAGE; i = argc } else { 1175 let bf: *u8 = argv[i + 1] as *u8 1176 let bl: *u8 = argv[i + 2] as *u8 1177 let ln: i64 = ds_atoi(bl) 1178 var a: i64 = DS_RESOLVE_UNPROVEN 1179 if ln >= 0 { a = ds_addr_for_line(lt, bf, ln) } 1180 if a == DS_RESOLVE_UNPROVEN { 1181 ds_puts("BREAK UNPROVEN file=" as *u8); ds_puts(bf); ds_puts(" line=" as *u8); ds_puts(bl) 1182 ds_puts(" reason=line-table-not-fully-decoded-or-line-not-a-number\n" as *u8) 1183 rc = DS_EXIT_NOLINES 1184 } else { 1185 if a == DS_RESOLVE_ABSENT { 1186 // A BOGUS file:line IS REFUSED BY NAME. It is never silently ignored, because a 1187 // breakpoint that quietly did not exist reads exactly like code that never ran. 1188 ds_puts("BREAK REFUSED-NO-SUCH-LINE file=" as *u8); ds_puts(bf) 1189 ds_puts(" line=" as *u8); ds_puts(bl) 1190 ds_puts(" rows_searched=" as *u8); ds_num(lt.nrows); ds_puts("\n" as *u8) 1191 rc = DS_EXIT_REFUSED 1192 } else { 1193 if ds_bp_set(dbg, a) == 1 { 1194 ds_puts("BREAK ok=1 file=" as *u8); ds_puts(bf) 1195 ds_puts(" line=" as *u8); ds_puts(bl) 1196 ds_puts(" addr=0x" as *u8); ds_hex(a) 1197 ds_puts(" live=" as *u8); ds_num(dbg.n_bp); ds_puts("\n" as *u8) 1198 } else { 1199 ds_puts("BREAK REFUSED-OUTSIDE-RAM addr=0x" as *u8); ds_hex(a); ds_puts("\n" as *u8) 1200 rc = DS_EXIT_REFUSED 1201 } 1202 } 1203 } 1204 i = i + 2 1205 } 1206 } else { 1207 if ds_streq(c, "breakaddr" as *u8) == 1 { 1208 if i + 1 >= argc { ds_puts("BREAK REFUSED reason=breakaddr-needs-an-address\n" as *u8); rc = DS_EXIT_USAGE; i = argc } else { 1209 let a: i64 = ds_atox(argv[i + 1] as *u8) 1210 if a < 0 { ds_puts("BREAK REFUSED reason=not-a-hex-address\n" as *u8); rc = DS_EXIT_REFUSED } else { 1211 if ds_bp_set(dbg, a) == 1 { 1212 ds_puts("BREAK ok=1 addr=0x" as *u8); ds_hex(a); ds_puts(" live=" as *u8); ds_num(dbg.n_bp); ds_puts("\n" as *u8) 1213 } else { 1214 ds_puts("BREAK REFUSED-OUTSIDE-RAM addr=0x" as *u8); ds_hex(a); ds_puts("\n" as *u8) 1215 rc = DS_EXIT_REFUSED 1216 } 1217 } 1218 i = i + 1 1219 } 1220 } else { 1221 if ds_streq(c, "step" as *u8) == 1 { 1222 let r: i64 = dbg_step_into(dbg) 1223 ds_report_stop(dbg, "step" as *u8) 1224 if r == DS_STOP_BUDGET { rc = DS_EXIT_BUDGET } 1225 } else { 1226 if ds_streq(c, "stepover" as *u8) == 1 { 1227 let r: i64 = dbg_step_over(dbg) 1228 ds_report_stop(dbg, "stepover" as *u8) 1229 if r == DS_STOP_BUDGET { rc = DS_EXIT_BUDGET } 1230 } else { 1231 if ds_streq(c, "continue" as *u8) == 1 { 1232 let r: i64 = dbg_continue(dbg) 1233 ds_report_stop(dbg, "continue" as *u8) 1234 if r == DS_STOP_BUDGET { rc = DS_EXIT_BUDGET } 1235 } else { 1236 if ds_streq(c, "bt" as *u8) == 1 { 1237 dbg_backtrace(dbg) 1238 } else { 1239 if ds_streq(c, "detach" as *u8) == 1 { 1240 dbg_detach(dbg) 1241 } else { 1242 ds_puts("CMD UNKNOWN word=" as *u8); ds_puts(c); ds_puts("\n" as *u8) 1243 rc = DS_EXIT_USAGE 1244 } } } } } } } 1245 i = i + 1 1246 } 1247 1248 // ---- ALWAYS DETACH. Not a command the caller may forget: if the script did not detach, we do 1249 // it here, on every path out of the loop, before this process can exit. 1250 var auto: i64 = 0 1251 if dbg.detached == 0 { auto = 1; dbg_detach(dbg) } 1252 1253 let sum1: i64 = ds_code_sum(dbg) 1254 var unchanged: i64 = 0 1255 if sum1 == dbg.code_sum0 { unchanged = 1 } 1256 let cnt: i64 = nx_uart_tx_count(sim.uart) 1257 ds_puts("DETACH resumed=1 auto=" as *u8); ds_num(auto) 1258 ds_puts(" halted=" as *u8); ds_num(sim.halted) 1259 ds_puts(" exit_code=" as *u8); ds_num(sim.halt_code) 1260 ds_puts(" guest_steps=" as *u8); ds_num(sim.steps) 1261 ds_puts(" breakpoints_live=" as *u8); ds_num(dbg.n_bp) 1262 ds_puts(" code_bytes_unchanged=" as *u8); ds_num(unchanged) 1263 ds_puts(" transcript_bytes=" as *u8); ds_num(cnt) 1264 ds_puts("\n" as *u8) 1265 if unchanged != 1 { 1266 // Cannot happen by construction (breakpoints are a bitmap), which is exactly why it is 1267 // CHECKED: a never-brick claim that is never measured is a promise, not a guarantee. 1268 ds_puts("DBG NEVER-BRICK-VIOLATION guest code bytes changed under the debugger\n" as *u8) 1269 return DS_EXIT_REFUSED 1270 } 1271 return rc 1272}