v0.8: step-over (next/trace), symbol demangling, disasm len/is_call, ergonomics
Driven by the friction the v0.7 smarm session hit: single-stepping the
context-switch shim dove into every helper call, and every step needed
`nm | grep` for a mangled name whose hash changes each rebuild. This round
makes tracing the shim pleasant.
Rust (kept minimal — only what JS genuinely can't do):
- symbols.rs: demangle Rust symbols. addr("name") now resolves by exact
mangled/case-insensitive (unchanged), then by full demangled path
(smarm::context::switch_to_scheduler), then by *unique* final path
component (switch_to_scheduler). Ambiguous short names resolve to None and
bp.set reports "matches N symbols" rather than guessing. sym()/name_for_addr
return demangled names so traces are readable. Adds rustc-demangle dep.
- disasm.rs: Decoded gains `len` (instruction byte length) and `is_call`
(iced FlowControl::Call, incl. indirect). js.rs exposes both on disasm
records so step-over finds the return address without re-parsing text.
Prelude (everything else, like until/finish):
- next(n): step OVER calls — one-shot bp at pc+len, cont, cleanup; else
single-step. A stepped-over call fires the `step` listener once. Detects
calls that don't return to pc+len (stack-switching shims, longjmp): on a
non-matching stop it dispatches the real event and stops instead of hanging.
- trace(from, to, {over, range, capture, print, max}): run-to-A then
step(-over)-to-B with per-step capture; `range:[lo,hi]` stops when pc leaves
a range. Drives the native steppers directly (no listener coupling).
- watch(specs): a `step` listener printing named expressions each step.
- fmt(): BigInt-safe stringify. state.insn: disasm record at pc.
stackContains/stackDistance/region: stack-region legibility for cross-stack
handoffs. step(n, {summary}): first/last/count for large N.
Docs: README status row + prelude reference + sections on the next() caveat,
the conditional-breakpoint listener idiom, and BigInt ergonomics.
examples/smarm_ctx_switch_next.js traces a full switch_to_scheduler in 16
readable lines using trace({over:true}).
No-listener fast paths and the v0.7 example are unaffected (verified).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// examples/smarm_ctx_switch_next.js
|
||||
//
|
||||
// v0.8 worked session: trace one full smarm context-switch shim cleanly using
|
||||
// step-over (next) and the trace() helper. This is the workflow that was
|
||||
// awkward in v0.7 — single-stepping the shim dove into the two thread-local
|
||||
// accessor calls (set_actor_sp / get_scheduler_sp), so a flat step(N)
|
||||
// overshot and you hand-rolled return-address breakpoints. v0.8 makes it a
|
||||
// one-liner.
|
||||
//
|
||||
// Two v0.8 wins on display:
|
||||
// 1. Symbol demangling — addr("switch_to_scheduler") Just Works now; no more
|
||||
// `nm BIN | grep` for the mangled _ZN..E name whose hash changes every
|
||||
// rebuild. Pass the readable name straight to addr()/bp.set().
|
||||
// 2. next()/trace({over:true}) — step OVER the helper calls instead of into
|
||||
// them, so the trace stays on the shim's own instructions.
|
||||
//
|
||||
// Build the probe (frame pointers on, single-scheduler example):
|
||||
// cd smarm
|
||||
// RUSTFLAGS="-C force-frame-pointers=yes" cargo build --example ctx_switch_probe
|
||||
//
|
||||
// Then pipe this in, substituting SPAWN_PATH with the absolute path to the
|
||||
// built ctx_switch_probe-<hash> binary. Note: NO mangled-name substitution
|
||||
// needed anymore — that's the point.
|
||||
|
||||
spawn("SPAWN_PATH")
|
||||
|
||||
// Resolve by readable name (v0.8 demangling). Ambiguous short names raise a
|
||||
// clear "matches N symbols" error; these three are unique.
|
||||
ctx.sched = addr("switch_to_scheduler")
|
||||
print(`switch_to_scheduler @ ${hex(ctx.sched)}`)
|
||||
---END---
|
||||
|
||||
// Stop at the scheduler-bound shim entry.
|
||||
bp.set("switch_to_scheduler", "to_sched")
|
||||
cont()
|
||||
here("entered shim:")
|
||||
---END---
|
||||
|
||||
// The breakpoint disarm-step-rearm consumes the first instruction (push rbx),
|
||||
// so the first thing we observe is the second instruction. Step once to settle
|
||||
// onto a clean boundary, then trace OVER calls until pc leaves the shim's
|
||||
// address range. We don't know the exact exit address (the shim ends in `ret`
|
||||
// into a different stack), so use a range stop sized generously past the body.
|
||||
step(1)
|
||||
ctx.lo = ctx.sched
|
||||
ctx.hi = ctx.sched + 0x60n
|
||||
const r = trace(null, null, {
|
||||
over: true,
|
||||
range: [ctx.lo, ctx.hi],
|
||||
capture: e => ({ pc: hex(e.pc), insn: e.insn.text, sp_dist: hex(stackDistance(e.sp)) }),
|
||||
})
|
||||
print(`traced ${r.steps} insns, stopped: ${r.reason}`)
|
||||
for (const t of r.trace) print(` ${t.pc} ${t.insn.padEnd(20)} sp_dist=${t.sp_dist}`)
|
||||
---END---
|
||||
|
||||
// The trace above shows the two `call` lines (set_actor_sp / get_scheduler_sp)
|
||||
// as single entries — stepped over, not descended into — then the pop chain
|
||||
// and the final `ret` that swaps to the scheduler stack and leaves the range.
|
||||
//
|
||||
// Compare: walking the SAME shim with plain step() would have produced dozens
|
||||
// of lines diving through the TLS accessors. next()/trace({over:true}) is the
|
||||
// difference between a readable 16-line trace and an unreadable one.
|
||||
//
|
||||
// To step INTO a specific call (e.g. to inspect get_scheduler_sp itself), use
|
||||
// plain step() at that instruction instead of next(). And to walk *through*
|
||||
// the stack-switching `ret`, use step() — next() over a call that swaps rsp
|
||||
// would wait on a return that never arrives (next() detects this and falls
|
||||
// back / stops rather than hanging; see the README caveat).
|
||||
Reference in New Issue
Block a user