diff --git a/src/js.rs b/src/js.rs index 3e9ccb9..1624bb5 100644 --- a/src/js.rs +++ b/src/js.rs @@ -218,60 +218,15 @@ fn install_globals<'js>( // ---------- state ---------- { - let state_obj = Object::new(c.clone())?; - - // state.pc — accessor returning BigInt - { - let target = target.clone(); - state_obj.prop( - "pc", - Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { - 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> { - 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> { - 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()) - }), - )?; - } + let target_for_state = target.clone(); + let state_obj = build_register_view( + &c, + move |c| -> rquickjs::Result { + let slot = target_for_state.borrow(); + let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?; + t.regs().map_err(|e| throw(c, &e)) + }, + )?; // state.memory(addr) → object with u8/u32/u64/i64/bytes { @@ -346,6 +301,23 @@ fn install_globals<'js>( 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 { + 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 --------------------------------------------- // Loaded last, so it can reference any of the native globals above. // Lives in src/prelude.js, embedded at compile time. @@ -357,6 +329,60 @@ fn install_globals<'js>( // --- 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> +where + F: Fn(&Ctx<'js>) -> rquickjs::Result + Clone + 'static, +{ + let obj = Object::new(c.clone())?; + + { + let fetch = fetch.clone(); + obj.prop( + "pc", + Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { + 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> { + let r = fetch(&c)?; + u64_to_bigint(&c, r.rsp) + }), + )?; + } + { + let fetch = fetch; + obj.prop( + "registers", + Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { + 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 /// in rquickjs for "throw an error from a Rust callback" is to call /// `Exception::throw_*(ctx, msg)`. diff --git a/src/target.rs b/src/target.rs index ab92d26..824afcc 100644 --- a/src/target.rs +++ b/src/target.rs @@ -52,6 +52,9 @@ pub enum State { pub struct Target { pid: Pid, state: State, + /// Snapshot of registers from the previous stop. None until at least one + /// step/cont has happened after spawn. + prev_regs: Option, } impl Target { @@ -102,6 +105,7 @@ impl Target { WaitStatus::Stopped(_, Signal::SIGTRAP) => Ok(Target { pid: child, state: State::Stopped(StopReason::Step), + prev_regs: None, }), WaitStatus::Exited(_, code) => { Err(format!("child exited immediately with code {} (exec failed?)", code)) @@ -117,6 +121,8 @@ impl Target { if !self.is_stopped() { 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))?; let reason = self.wait_for_stop()?; Ok(reason) @@ -127,11 +133,16 @@ impl Target { if !self.is_stopped() { 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))?; let reason = self.wait_for_stop()?; Ok(reason) } + pub fn prev_regs(&self) -> Option { + self.prev_regs + } + fn wait_for_stop(&mut self) -> Result { let status = waitpid(self.pid, None).map_err(|e| format!("waitpid: {}", e))?; let reason = match status {