v0.4: INT3 software breakpoints with by-name/by-id ops

- StopReason::Breakpoint(id), rewinds RIP on hit
- bp.set(addr_or_symbol, name?), bp.remove(id_or_name), bp.list()
- step/cont disarm-step-rearm when PC sits on an enabled bp
- fix: PIE load_base = mapping_base - mapping_file_offset (was using the
  first executable mapping with its offset, which is wrong on toolchains
  that split read-only header pages)
This commit is contained in:
claude
2026-05-27 21:21:29 +00:00
parent d587dbcd4c
commit 8ae6103ce7
5 changed files with 382 additions and 45 deletions
+81
View File
@@ -365,6 +365,87 @@ fn install_globals<'js>(
globals.set("symbols", f)?;
}
// ---------- breakpoint / bp ---------------------------------------------
// breakpoint.set(target, name?) → id (target = address number/BigInt or symbol name)
// breakpoint.remove(id_or_name)
// breakpoint.list() → [{id, addr, name, hits, enabled}]
{
let bp_obj = Object::new(c.clone())?;
// set(target, name?)
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, location: Value<'js>, name: rquickjs::function::Opt<String>| -> rquickjs::Result<u32> {
let mut slot = target.borrow_mut();
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
// location: string → resolve via symbols; number/BigInt → coerce_addr.
let addr = if let Some(s) = location.clone().into_string() {
let s = s.to_string()?;
let sym = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
sym.addr_for_name(&s).ok_or_else(|| throw(&c, &format!("unknown symbol: {}", s)))?
} else {
coerce_addr(&c, &location)?
};
let name = name.0;
t.breakpoint_set(addr, name).map_err(|e| throw(&c, &e))
})?;
bp_obj.set("set", f)?;
}
// remove(id_or_name)
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, id_or_name: Value<'js>| -> rquickjs::Result<()> {
let mut slot = target.borrow_mut();
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
let id = if let Some(s) = id_or_name.clone().into_string() {
let s = s.to_string()?;
t.breakpoint_id_by_name(&s).ok_or_else(|| throw(&c, &format!("no breakpoint named '{}'", s)))?
} else if let Some(n) = id_or_name.as_number() {
if n < 0.0 || n.fract() != 0.0 {
return Err(throw(&c, "bp id must be a non-negative integer"));
}
n as u32
} else if id_or_name.is_int() {
let i: i32 = id_or_name.get()?;
if i < 0 { return Err(throw(&c, "bp id must be non-negative")); }
i as u32
} else {
return Err(throw(&c, "bp.remove expects id (number) or name (string)"));
};
t.breakpoint_remove(id).map_err(|e| throw(&c, &e))
})?;
bp_obj.set("remove", f)?;
}
// list() → array of breakpoint records
{
let target = target.clone();
let f = Function::new(c.clone(), 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 arr = rquickjs::Array::new(c.clone())?;
for (i, bp) in t.breakpoints().iter().enumerate() {
let rec = Object::new(c.clone())?;
rec.set("id", bp.id)?;
rec.set("addr", u64_to_bigint(&c, bp.addr)?)?;
rec.set("hits", u64_to_bigint(&c, bp.hits)?)?;
rec.set("enabled", bp.enabled)?;
match &bp.name {
Some(n) => rec.set("name", n.as_str())?,
None => rec.set("name", Value::new_null(c.clone()))?,
};
arr.set(i, rec)?;
}
Ok(arr.into_value())
})?;
bp_obj.set("list", f)?;
}
globals.set("breakpoint", bp_obj.clone())?;
globals.set("bp", bp_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.