Files
llmdbg/examples/smarm_ctx_switch.js
llmdbg-dev ce96a98829 v0.7: event-listener system (addEventListener/step/breakpoint/signal/exit)
- Native step/cont moved to __step_native/__cont_native; public step()/cont()
  defined in prelude.js wrap them and dispatch JS listeners on each stop.
- addEventListener/removeEventListener/clearListeners/listeners() registry on
  globalThis, persistent for the daemon lifetime (independent of ctx.reset()).
- Event object carries {state, prev, stop_reason} plus breakpoint_id/signal/
  exit_code as appropriate. Implements the SPEC 'Event Listeners' + 'Core Loop'.
- No-listener fast path keeps existing scripts zero-cost.
- examples/smarm_ctx_switch.js: worked session inspecting smarm's naked-asm
  context switch; README updated (status table, listener API, deferred list).
2026-05-28 05:50:09 +00:00

69 lines
2.5 KiB
JavaScript

// examples/smarm_ctx_switch.js
//
// Demonstrates the v0.7 event-listener system by inspecting smarm's naked-asm
// context switch. smarm (https://git.kalsbeek.dev/Markk116/smarm) is a green-
// thread actor runtime whose core is two `#[naked]` switch shims that swap rsp
// between a scheduler stack and an actor's mmap'd stack.
//
// Build the probe binary (frame pointers on, single scheduler thread):
// cd smarm
// RUSTFLAGS="-C force-frame-pointers=yes" cargo build --example ctx_switch_probe
//
// Then pipe this script into llmdbg, substituting:
// - SPAWN_PATH: absolute path to the built ctx_switch_probe-<hash> binary
// - ACTOR_ASM: mangled name of switch_to_actor_asm (from `nm BIN | grep`)
// - SCHED: mangled name of switch_to_scheduler
//
// What it shows:
// 1. A 'breakpoint' listener that logs each switch with rsp + 16-alignment.
// 2. A 'step' listener that single-steps the shim's tail (mov rsp,rax; the
// six pops; ret) and proves the actor entry lands with rsp % 16 == 8 —
// the x86-64 ABI state expected at a function entry reached via `call`.
//
// This was the session that caught the off-by-one-slot error in
// init_actor_stack's layout comment (entry is at aligned_top-16, not -8).
spawn("SPAWN_PATH")
// --- (1) per-switch alignment trace via a breakpoint listener --------------
ctx.sched = addr("SCHED")
ctx.actor_asm = addr("ACTOR_ASM")
addEventListener('breakpoint', e => {
const sp = e.state.sp;
const where = (e.state.pc === ctx.sched) ? "to_sched" : "to_actor";
ctx.hits = (ctx.hits ?? 0) + 1;
print(`#${ctx.hits} ${where} rsp=${hex(sp)} rsp%16=${sp % 16n}`);
});
bp.set(ctx.sched, "to_sched")
bp.set(ctx.actor_asm, "to_actor")
---END---
// Drive through the first several switches. Each cont() fires the listener.
for (let i = 0; i < 6; i++) {
let r = cont();
if (!r.startsWith("breakpoint")) { print("stop: " + r); break; }
}
---END---
// --- (2) prove the ret lands at rsp%16==8 in the actor entry ---------------
// Remove the breakpoint listener; add a step listener for the tail trace.
clearListeners('breakpoint')
// Reach `mov rsp,rax` (offset 0x17 in the shim) on the next switch_to_actor.
ctx.movrsp = ctx.actor_asm + 0x17n
bp.remove("to_sched")
bp.remove("to_actor")
bp.set(ctx.movrsp, "movrsp")
cont()
---END---
clearListeners()
addEventListener('step', e => {
const ins = e.state.disasm(1)[0];
print(`${hex(e.state.pc)} rsp=${hex(e.state.sp)} %16=${e.state.sp % 16n} ${ins.text}`);
});
// mov rsp,rax ; pop r15..rbx (6) ; ret ; first insn of entry
step(8)
---END---