v0.2: prev snapshot of regs, refactored state/prev to share build_register_view

This commit is contained in:
claude
2026-05-27 21:10:23 +00:00
parent 912f5003fe
commit 19c6154831
2 changed files with 91 additions and 54 deletions
+79 -53
View File
@@ -218,60 +218,15 @@ fn install_globals<'js>(
// ---------- state ---------- // ---------- state ----------
{ {
let state_obj = Object::new(c.clone())?; let target_for_state = target.clone();
let state_obj = build_register_view(
// state.pc — accessor returning BigInt &c,
{ move |c| -> rquickjs::Result<libc::user_regs_struct> {
let target = target.clone(); let slot = target_for_state.borrow();
state_obj.prop( let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?;
"pc", t.regs().map_err(|e| throw(c, &e))
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> { },
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let r = t.regs().map_err(|e| throw(&c, &e))?;
u64_to_bigint(&c, r.rip)
}),
)?; )?;
}
// state.sp
{
let target = target.clone();
state_obj.prop(
"sp",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let r = t.regs().map_err(|e| throw(&c, &e))?;
u64_to_bigint(&c, r.rsp)
}),
)?;
}
// state.registers — accessor returning a fresh object each time
{
let target = target.clone();
state_obj.prop(
"registers",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let r = t.regs().map_err(|e| throw(&c, &e))?;
let obj = Object::new(c.clone())?;
let set = |name: &str, val: u64| -> rquickjs::Result<()> {
obj.set(name, u64_to_bigint(&c, val)?)
};
set("rax", r.rax)?; set("rbx", r.rbx)?; set("rcx", r.rcx)?;
set("rdx", r.rdx)?; set("rsi", r.rsi)?; set("rdi", r.rdi)?;
set("rbp", r.rbp)?; set("rsp", r.rsp)?; set("rip", r.rip)?;
set("r8", r.r8)?; set("r9", r.r9)?; set("r10", r.r10)?;
set("r11", r.r11)?; set("r12", r.r12)?; set("r13", r.r13)?;
set("r14", r.r14)?; set("r15", r.r15)?;
set("eflags", r.eflags)?;
Ok(obj.into_value())
}),
)?;
}
// state.memory(addr) → object with u8/u32/u64/i64/bytes // state.memory(addr) → object with u8/u32/u64/i64/bytes
{ {
@@ -346,6 +301,23 @@ fn install_globals<'js>(
globals.set("state", state_obj)?; globals.set("state", state_obj)?;
} }
// ---------- prev ----------
// Mirror of state for register-only data, backed by the pre-resume
// snapshot. Property accessors throw if no snapshot is available yet
// (i.e. immediately after spawn, before any step/cont).
{
let target_for_prev = target.clone();
let prev_obj = build_register_view(
&c,
move |c| -> rquickjs::Result<libc::user_regs_struct> {
let slot = target_for_prev.borrow();
let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?;
t.prev_regs().ok_or_else(|| throw(c, "no prev snapshot yet (step/cont first)"))
},
)?;
globals.set("prev", prev_obj)?;
}
// ---------- JS-side prelude --------------------------------------------- // ---------- JS-side prelude ---------------------------------------------
// Loaded last, so it can reference any of the native globals above. // Loaded last, so it can reference any of the native globals above.
// Lives in src/prelude.js, embedded at compile time. // Lives in src/prelude.js, embedded at compile time.
@@ -357,6 +329,60 @@ fn install_globals<'js>(
// --- helpers -------------------------------------------------------------- // --- helpers --------------------------------------------------------------
/// Build a JS object with `pc`, `sp`, and `registers` accessor properties,
/// each backed by a closure that fetches a `user_regs_struct` on demand.
/// Used for both `state` (live regs via ptrace) and `prev` (snapshot).
fn build_register_view<'js, F>(c: &Ctx<'js>, fetch: F) -> rquickjs::Result<Object<'js>>
where
F: Fn(&Ctx<'js>) -> rquickjs::Result<libc::user_regs_struct> + Clone + 'static,
{
let obj = Object::new(c.clone())?;
{
let fetch = fetch.clone();
obj.prop(
"pc",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let r = fetch(&c)?;
u64_to_bigint(&c, r.rip)
}),
)?;
}
{
let fetch = fetch.clone();
obj.prop(
"sp",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let r = fetch(&c)?;
u64_to_bigint(&c, r.rsp)
}),
)?;
}
{
let fetch = fetch;
obj.prop(
"registers",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let r = fetch(&c)?;
let inner = Object::new(c.clone())?;
let set = |name: &str, val: u64| -> rquickjs::Result<()> {
inner.set(name, u64_to_bigint(&c, val)?)
};
set("rax", r.rax)?; set("rbx", r.rbx)?; set("rcx", r.rcx)?;
set("rdx", r.rdx)?; set("rsi", r.rsi)?; set("rdi", r.rdi)?;
set("rbp", r.rbp)?; set("rsp", r.rsp)?; set("rip", r.rip)?;
set("r8", r.r8)?; set("r9", r.r9)?; set("r10", r.r10)?;
set("r11", r.r11)?; set("r12", r.r12)?; set("r13", r.r13)?;
set("r14", r.r14)?; set("r15", r.r15)?;
set("eflags", r.eflags)?;
Ok(inner.into_value())
}),
)?;
}
Ok(obj)
}
/// Create a JS Error and turn it into an rquickjs::Error::Exception. The idiom /// Create a JS Error and turn it into an rquickjs::Error::Exception. The idiom
/// in rquickjs for "throw an error from a Rust callback" is to call /// in rquickjs for "throw an error from a Rust callback" is to call
/// `Exception::throw_*(ctx, msg)`. /// `Exception::throw_*(ctx, msg)`.
+11
View File
@@ -52,6 +52,9 @@ pub enum State {
pub struct Target { pub struct Target {
pid: Pid, pid: Pid,
state: State, state: State,
/// Snapshot of registers from the previous stop. None until at least one
/// step/cont has happened after spawn.
prev_regs: Option<libc::user_regs_struct>,
} }
impl Target { impl Target {
@@ -102,6 +105,7 @@ impl Target {
WaitStatus::Stopped(_, Signal::SIGTRAP) => Ok(Target { WaitStatus::Stopped(_, Signal::SIGTRAP) => Ok(Target {
pid: child, pid: child,
state: State::Stopped(StopReason::Step), state: State::Stopped(StopReason::Step),
prev_regs: None,
}), }),
WaitStatus::Exited(_, code) => { WaitStatus::Exited(_, code) => {
Err(format!("child exited immediately with code {} (exec failed?)", code)) Err(format!("child exited immediately with code {} (exec failed?)", code))
@@ -117,6 +121,8 @@ impl Target {
if !self.is_stopped() { if !self.is_stopped() {
return Err("target is not stopped".into()); return Err("target is not stopped".into());
} }
// Snapshot regs before resuming so `prev` reflects the pre-step state.
self.prev_regs = ptrace::getregs(self.pid).ok();
ptrace::step(self.pid, None).map_err(|e| format!("ptrace step: {}", e))?; ptrace::step(self.pid, None).map_err(|e| format!("ptrace step: {}", e))?;
let reason = self.wait_for_stop()?; let reason = self.wait_for_stop()?;
Ok(reason) Ok(reason)
@@ -127,11 +133,16 @@ impl Target {
if !self.is_stopped() { if !self.is_stopped() {
return Err("target is not stopped".into()); return Err("target is not stopped".into());
} }
self.prev_regs = ptrace::getregs(self.pid).ok();
ptrace::cont(self.pid, None).map_err(|e| format!("ptrace cont: {}", e))?; ptrace::cont(self.pid, None).map_err(|e| format!("ptrace cont: {}", e))?;
let reason = self.wait_for_stop()?; let reason = self.wait_for_stop()?;
Ok(reason) Ok(reason)
} }
pub fn prev_regs(&self) -> Option<libc::user_regs_struct> {
self.prev_regs
}
fn wait_for_stop(&mut self) -> Result<StopReason, String> { fn wait_for_stop(&mut self) -> Result<StopReason, String> {
let status = waitpid(self.pid, None).map_err(|e| format!("waitpid: {}", e))?; let status = waitpid(self.pid, None).map_err(|e| format!("waitpid: {}", e))?;
let reason = match status { let reason = match status {