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).
69 lines
3.1 KiB
JavaScript
69 lines
3.1 KiB
JavaScript
// 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).
|