v0.6: state.stack, until(loc), finish(), prelude show.* helpers

- StackRegion from /proc/<pid>/maps, exposed as state.stack
  ({top, usable_base, guard_size} BigInts per spec)
- until(loc) one-shot bp wrapper that cleans up via try/finally,
  works even if a different stop fires first
- finish() reads return address from [rbp+8] and uses until()
- updated js.rs module doc; allow(dead_code) on the kept-for-future
  BreakpointSet::find_by_name
This commit is contained in:
claude
2026-05-27 21:30:18 +00:00
parent 2ed17f0286
commit 2f64981729
5 changed files with 121 additions and 11 deletions
+1
View File
@@ -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))
}
+32 -7
View File
@@ -1,15 +1,22 @@
//! QuickJS REPL: persistent context, ptrace target lives behind an Rc<RefCell>,
//! globals are installed once at construction.
//!
//! v0.1 surface:
//! Native globals installed here:
//! 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
//! 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(n) }
//! 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<Value<'js>> {
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)?;
}
+32 -1
View File
@@ -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]
//
+45
View File
@@ -190,3 +190,48 @@ fn is_pie(elf: &object::File) -> bool {
_ => false,
}
}
/// Stack region info read from /proc/<pid>/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/<pid>/maps.
pub fn from_pid(pid: i32) -> Result<StackRegion, String> {
let maps = std::fs::read_to_string(format!("/proc/{}/maps", pid))
.map_err(|e| format!("read maps: {}", e))?;
let mut prev_high: Option<u64> = 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())
}
}
+9 -1
View File
@@ -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<SymbolTable>,
/// Software (INT3) breakpoints.
breakpoints: BreakpointSet,
/// Stack region of the main thread, parsed once at spawn.
stack: Option<StackRegion>,
}
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,