// 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- 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).