diff --git a/src/breakpoint.rs b/src/breakpoint.rs index 7d581bc..67fb4ca 100644 --- a/src/breakpoint.rs +++ b/src/breakpoint.rs @@ -69,6 +69,7 @@ impl BreakpointSet { self.by_addr.get(&addr).and_then(|id| self.get(*id)) } + #[allow(dead_code)] pub fn find_by_name(&self, name: &str) -> Option<&Breakpoint> { self.by_name.get(name).and_then(|id| self.get(*id)) } diff --git a/src/js.rs b/src/js.rs index 5128de9..1061168 100644 --- a/src/js.rs +++ b/src/js.rs @@ -1,15 +1,22 @@ //! QuickJS REPL: persistent context, ptrace target lives behind an Rc, //! globals are installed once at construction. //! -//! v0.1 surface: -//! spawn(path, ...args) -> { pid, status } -//! step(n=1) -> stop-reason string -//! continue() -> stop-reason string (exposed as `continue` and `cont`) -//! print(...args) -> writes to stdout -//! ctx -> persistent plain object -//! state.pc / .sp -> BigInt -//! state.registers -> object of BigInts -//! state.memory(addr) -> { u8(), u32(), u64(), i64(), bytes(n) } +//! Native globals installed here: +//! spawn(path, ...args) -> { pid, status } +//! step(n=1) / cont() -> stop-reason string +//! print(...args), ctx (persistent object) +//! state.pc, state.sp -> BigInt (live ptrace) +//! state.registers -> object of BigInts +//! state.memory(addr) -> { u8/u32/u64/i64/bytes } +//! state.disasm(n=8) -> [{addr, bytes, text, target?, target_sym?}] +//! state.stack -> { top, usable_base, guard_size } +//! prev -> register snapshot from before last resume +//! sym(addr), addr(name), symbols() +//! disasm(addr, n=8) +//! breakpoint / bp: .set, .remove, .list +//! +//! JS-side helpers (in prelude.js): hex, unhex, dump, repr, addrstr, here, +//! until, finish, show.{disasm,bp,regs}. use crate::target::Target; use rquickjs::function::Rest; @@ -314,6 +321,24 @@ fn install_globals<'js>( state_obj.set("disasm", f)?; } + // state.stack — accessor returning {top, usable_base, guard_size} as BigInts + { + let target = target.clone(); + state_obj.prop( + "stack", + 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 s = t.stack().ok_or_else(|| throw(&c, "no stack info"))?; + let obj = Object::new(c.clone())?; + obj.set("top", u64_to_bigint(&c, s.high)?)?; + obj.set("usable_base", u64_to_bigint(&c, s.low)?)?; + obj.set("guard_size", u64_to_bigint(&c, s.guard_size)?)?; + Ok(obj.into_value()) + }), + )?; + } + globals.set("state", state_obj)?; } diff --git a/src/prelude.js b/src/prelude.js index f1064a9..349077e 100644 --- a/src/prelude.js +++ b/src/prelude.js @@ -111,7 +111,38 @@ globalThis.here = function (label) { }; // --------------------------------------------------------------------------- -// show.disasm(n=8, [addr]) +// until(addr_or_sym) — run until pc reaches `loc`. Sets a one-shot bp, conts, +// removes the bp. Returns the stop reason. If the stop wasn't our bp (the +// program exited or hit another bp first), we still clean up our bp. +// +// `loc` accepts whatever bp.set accepts: a BigInt/Number address or a +// symbol name string. + +globalThis.until = function (loc) { + const id = bp.set(loc); + let reason; + try { + reason = cont(); + } finally { + // Remove our bp; ignore errors (target may have exited). + try { bp.remove(id); } catch (_) {} + } + return reason; +}; + +// --------------------------------------------------------------------------- +// finish() — run until the current function returns. Reads the return +// address from [rbp+8] (assumes frame pointer present, which is the case +// for code compiled without -fomit-frame-pointer). + +globalThis.finish = function () { + if (state.registers.rbp === 0n) { + throw new Error("finish(): rbp is 0 — no frame pointer to follow"); + } + const ret_addr = state.memory(state.registers.rbp + 8n).u64(); + return until(ret_addr); +}; + // Pretty-print N instructions starting at addr (default: pc). // Each line: ADDR BYTES MNEMONIC ARGS [-> sym] // diff --git a/src/symbols.rs b/src/symbols.rs index 10846bc..7f53c62 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -190,3 +190,48 @@ fn is_pie(elf: &object::File) -> bool { _ => false, } } + +/// Stack region info read from /proc//maps. +#[derive(Debug, Clone)] +pub struct StackRegion { + /// Lowest mapped stack address (anonymous mapping start). The kernel + /// places a guard page below this on the main thread. + pub low: u64, + /// Address one past the highest stack byte (mapping end). + pub high: u64, + /// Guard page size (typically 4 KiB). 0 if no guard could be found. + pub guard_size: u64, +} + +impl StackRegion { + /// Find `[stack]` for the main thread in /proc//maps. + pub fn from_pid(pid: i32) -> Result { + let maps = std::fs::read_to_string(format!("/proc/{}/maps", pid)) + .map_err(|e| format!("read maps: {}", e))?; + let mut prev_high: Option = None; + let mut prev_path: &str = ""; + for line in maps.lines() { + let mut it = line.splitn(6, ' ').filter(|s| !s.is_empty()); + let range = match it.next() { Some(s) => s, None => continue }; + let _perms = match it.next() { Some(s) => s, None => continue }; + let _off = it.next(); + let _dev = it.next(); + let _inode = it.next(); + let path = it.next().unwrap_or("").trim(); + let mut parts = range.split('-'); + let lo = parts.next().ok_or_else(|| "bad maps".to_string())?; + let hi = parts.next().ok_or_else(|| "bad maps".to_string())?; + let lo = u64::from_str_radix(lo, 16).map_err(|e| format!("bad lo: {}", e))?; + let hi = u64::from_str_radix(hi, 16).map_err(|e| format!("bad hi: {}", e))?; + if path == "[stack]" { + let guard_size = if let Some(ph) = prev_high { + if ph < lo && prev_path == "" { lo - ph } else { 0 } + } else { 0 }; + return Ok(StackRegion { low: lo, high: hi, guard_size }); + } + prev_high = Some(hi); + prev_path = if path.is_empty() { "" } else { "named" }; + } + Err("no [stack] mapping found".into()) + } +} diff --git a/src/target.rs b/src/target.rs index 6181a3b..2c17fc8 100644 --- a/src/target.rs +++ b/src/target.rs @@ -12,7 +12,7 @@ use std::ffi::CString; use std::io::IoSliceMut; use crate::breakpoint::BreakpointSet; -use crate::symbols::SymbolTable; +use crate::symbols::{StackRegion, SymbolTable}; /// Why the target stopped after a step/continue, or that it has exited. #[derive(Debug, Clone)] @@ -66,6 +66,8 @@ pub struct Target { symbols: Option, /// Software (INT3) breakpoints. breakpoints: BreakpointSet, + /// Stack region of the main thread, parsed once at spawn. + stack: Option, } impl Target { @@ -115,12 +117,14 @@ impl Target { match status { WaitStatus::Stopped(_, Signal::SIGTRAP) => { let symbols = SymbolTable::from_pid(child.as_raw()).ok(); + let stack = StackRegion::from_pid(child.as_raw()).ok(); Ok(Target { pid: child, state: State::Stopped(StopReason::Step), prev_regs: None, symbols, breakpoints: BreakpointSet::new(), + stack, }) } WaitStatus::Exited(_, code) => { @@ -144,6 +148,10 @@ impl Target { &self.breakpoints } + pub fn stack(&self) -> Option<&StackRegion> { + self.stack.as_ref() + } + // ----- breakpoint ops ------------------------------------------------- /// Install a software breakpoint at `addr`. Reads the original byte,