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}