- Native step/cont moved to __step_native/__cont_native; public step()/cont()
defined in prelude.js wrap them and dispatch JS listeners on each stop.
- addEventListener/removeEventListener/clearListeners/listeners() registry on
globalThis, persistent for the daemon lifetime (independent of ctx.reset()).
- Event object carries {state, prev, stop_reason} plus breakpoint_id/signal/
exit_code as appropriate. Implements the SPEC 'Event Listeners' + 'Core Loop'.
- No-listener fast path keeps existing scripts zero-cost.
- examples/smarm_ctx_switch.js: worked session inspecting smarm's naked-asm
context switch; README updated (status table, listener API, deferred list).
652 lines
27 KiB
Rust
652 lines
27 KiB
Rust
//! QuickJS REPL: persistent context, ptrace target lives behind an Rc<RefCell>,
|
|
//! globals are installed once at construction.
|
|
//!
|
|
//! Native globals installed here:
|
|
//! spawn(path, ...args) -> { pid, status }
|
|
//! __step_native(n=1) / __cont_native() -> stop-reason string
|
|
//! (public step()/cont() are defined in prelude.js and fire listeners)
|
|
//! 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;
|
|
use rquickjs::object::Accessor;
|
|
use rquickjs::{CatchResultExt, CaughtError, Context, Ctx, Function, Object, Runtime, Value};
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
/// Shared handle. All JS closures clone this and `.borrow_mut()` inside.
|
|
pub type TargetSlot = Rc<RefCell<Option<Target>>>;
|
|
|
|
/// Result of evaluating a script.
|
|
pub struct EvalOutput {
|
|
/// Anything the script wrote via `print()`.
|
|
pub printed: String,
|
|
/// String form of the final expression's value, or empty if `undefined` /
|
|
/// the source had no trailing expression.
|
|
pub result: String,
|
|
/// Some(msg) if the eval threw. `printed` may still contain partial output.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
pub struct Repl {
|
|
_rt: Runtime,
|
|
ctx: Context,
|
|
target: TargetSlot,
|
|
/// Accumulates print() output during one eval. Lives in an Rc<RefCell<String>>
|
|
/// captured by the `print` closure.
|
|
print_buf: Rc<RefCell<String>>,
|
|
}
|
|
|
|
impl Repl {
|
|
pub fn new() -> Result<Self, String> {
|
|
let rt = Runtime::new().map_err(|e| format!("rt: {}", e))?;
|
|
let ctx = Context::full(&rt).map_err(|e| format!("ctx: {}", e))?;
|
|
let target: TargetSlot = Rc::new(RefCell::new(None));
|
|
let print_buf: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
|
|
|
|
ctx.with(|c| -> Result<(), String> {
|
|
install_globals(c, target.clone(), print_buf.clone())
|
|
.map_err(|e| format!("install globals: {}", e))
|
|
})?;
|
|
|
|
Ok(Repl { _rt: rt, ctx, target, print_buf })
|
|
}
|
|
|
|
/// Evaluate one script. Captures `print()` output. Always returns Ok with
|
|
/// an EvalOutput; JS exceptions become `error: Some(...)`.
|
|
pub fn eval(&mut self, source: &str) -> EvalOutput {
|
|
self.print_buf.borrow_mut().clear();
|
|
|
|
let mut result = String::new();
|
|
let mut error = None;
|
|
|
|
self.ctx.with(|c| {
|
|
match c.eval::<Value, _>(source).catch(&c) {
|
|
Ok(v) => {
|
|
if !v.is_undefined() {
|
|
result = value_to_display(&c, &v).unwrap_or_else(|e| format!("<display err: {}>", e));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error = Some(format_caught(&e));
|
|
}
|
|
}
|
|
});
|
|
|
|
EvalOutput {
|
|
printed: std::mem::take(&mut *self.print_buf.borrow_mut()),
|
|
result,
|
|
error,
|
|
}
|
|
}
|
|
|
|
/// If a target exists, return its PID so we can kill on shutdown.
|
|
pub fn target_pid(&self) -> Option<i32> {
|
|
self.target.borrow().as_ref().map(|t| t.pid())
|
|
}
|
|
}
|
|
|
|
/// Render a JS value as a one-line string. We delegate to the prelude's
|
|
/// `repr()` so BigInts (and BigInts nested inside objects) render correctly.
|
|
fn value_to_display<'js>(c: &Ctx<'js>, v: &Value<'js>) -> Result<String, String> {
|
|
let globals = c.globals();
|
|
let repr: Function = globals.get("repr").map_err(|e| e.to_string())?;
|
|
let s: String = repr.call((v.clone(),)).map_err(|e| e.to_string())?;
|
|
Ok(s)
|
|
}
|
|
|
|
fn format_caught(e: &CaughtError) -> String {
|
|
match e {
|
|
CaughtError::Error(err) => format!("error: {}", err),
|
|
CaughtError::Exception(exc) => {
|
|
let msg = exc.message().unwrap_or_else(|| "<no message>".into());
|
|
if let Some(stack) = exc.stack() {
|
|
format!("error: {}\n{}", msg, stack)
|
|
} else {
|
|
format!("error: {}", msg)
|
|
}
|
|
}
|
|
CaughtError::Value(v) => format!("error: {:?}", v),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn install_globals<'js>(
|
|
c: Ctx<'js>,
|
|
target: TargetSlot,
|
|
print_buf: Rc<RefCell<String>>,
|
|
) -> rquickjs::Result<()> {
|
|
let globals = c.globals();
|
|
|
|
// ---------- print(...) ----------
|
|
{
|
|
let buf = print_buf.clone();
|
|
let f = Function::new(c.clone(), move |rest: Rest<Value<'js>>| {
|
|
let mut s = String::new();
|
|
for (i, v) in rest.0.iter().enumerate() {
|
|
if i > 0 { s.push(' '); }
|
|
// Best-effort String() coercion using global String.
|
|
if let Ok(string_fn) = v.ctx().globals().get::<_, Function>("String") {
|
|
if let Ok(part) = string_fn.call::<_, String>((v.clone(),)) {
|
|
s.push_str(&part);
|
|
continue;
|
|
}
|
|
}
|
|
s.push_str("<?>");
|
|
}
|
|
s.push('\n');
|
|
buf.borrow_mut().push_str(&s);
|
|
})?;
|
|
globals.set("print", f)?;
|
|
}
|
|
|
|
// ---------- ctx (persistent JS object) ----------
|
|
{
|
|
// Only set if it doesn't already exist (so REPL re-init wouldn't clobber).
|
|
if globals.get::<_, Value>("ctx").map(|v| v.is_undefined()).unwrap_or(true) {
|
|
let obj = Object::new(c.clone())?;
|
|
globals.set("ctx", obj)?;
|
|
}
|
|
}
|
|
|
|
// ---------- spawn(path, ...args) ----------
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, path: String, rest: Rest<String>| -> rquickjs::Result<Value<'js>> {
|
|
let args: Vec<String> = rest.0;
|
|
match Target::spawn(&path, &args) {
|
|
Ok(t) => {
|
|
let pid = t.pid();
|
|
*target.borrow_mut() = Some(t);
|
|
let obj = Object::new(c.clone())?;
|
|
obj.set("pid", pid)?;
|
|
obj.set("status", "stopped_at_entry")?;
|
|
Ok(obj.into_value())
|
|
}
|
|
Err(e) => Err(throw(&c, &format!("spawn failed: {}", e))),
|
|
}
|
|
})?;
|
|
globals.set("spawn", f)?;
|
|
}
|
|
|
|
// ---------- __step_native(n=1) ----------
|
|
// Raw single-stepper. Steps up to n instructions, stopping early if the
|
|
// target dies. Returns the last stop-reason string. The public `step(n)`
|
|
// is defined in prelude.js: it loops one instruction at a time so it can
|
|
// fire 'step'/'breakpoint' listeners after each stop. We keep the native
|
|
// n-loop too, for the fast path when no listeners are registered.
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, n: rquickjs::function::Opt<i64>| -> rquickjs::Result<String> {
|
|
let n = n.0.unwrap_or(1);
|
|
if n < 1 {
|
|
return Err(throw(&c, "step(n): n must be >= 1"));
|
|
}
|
|
let mut slot = target.borrow_mut();
|
|
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let mut last = None;
|
|
for _ in 0..n {
|
|
match t.step() {
|
|
Ok(r) => {
|
|
let alive = r.is_alive();
|
|
last = Some(r);
|
|
if !alive { break; }
|
|
}
|
|
Err(e) => return Err(throw(&c, &e)),
|
|
}
|
|
}
|
|
Ok(last.map(|r| r.tag()).unwrap_or_else(|| "step".into()))
|
|
})?;
|
|
globals.set("__step_native", f)?;
|
|
}
|
|
|
|
// ---------- __cont_native() ----------
|
|
// Raw continue. Resumes until the next stop (breakpoint, signal, exit).
|
|
// The public `cont()` in prelude.js wraps this to fire listeners on the
|
|
// resulting stop.
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<String> {
|
|
let mut slot = target.borrow_mut();
|
|
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
match t.cont() {
|
|
Ok(r) => Ok(r.tag()),
|
|
Err(e) => Err(throw(&c, &e)),
|
|
}
|
|
})?;
|
|
// NOTE: `continue` is a reserved keyword in JS — `continue()` as an
|
|
// expression is a syntax error. We expose only `cont()`.
|
|
globals.set("__cont_native", f)?;
|
|
}
|
|
|
|
// ---------- state ----------
|
|
{
|
|
let target_for_state = target.clone();
|
|
let state_obj = build_register_view(
|
|
&c,
|
|
move |c| -> rquickjs::Result<libc::user_regs_struct> {
|
|
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
|
|
{
|
|
let target = target.clone();
|
|
let mem_fn = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result<Value<'js>> {
|
|
let addr = coerce_addr(&c, &addr)?;
|
|
let target = target.clone();
|
|
let obj = Object::new(c.clone())?;
|
|
|
|
// u8()
|
|
{
|
|
let target = target.clone();
|
|
obj.set("u8", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<u32> {
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let b = t.read_mem(addr, 1).map_err(|e| throw(&c, &e))?;
|
|
Ok(b[0] as u32)
|
|
})?)?;
|
|
}
|
|
// u32()
|
|
{
|
|
let target = target.clone();
|
|
obj.set("u32", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<u32> {
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let b = t.read_mem(addr, 4).map_err(|e| throw(&c, &e))?;
|
|
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
|
})?)?;
|
|
}
|
|
// u64() → BigInt
|
|
{
|
|
let target = target.clone();
|
|
obj.set("u64", 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 b = t.read_mem(addr, 8).map_err(|e| throw(&c, &e))?;
|
|
let mut a = [0u8; 8]; a.copy_from_slice(&b);
|
|
let v = u64::from_le_bytes(a);
|
|
u64_to_bigint(&c, v)
|
|
})?)?;
|
|
}
|
|
// i64() → BigInt
|
|
{
|
|
let target = target.clone();
|
|
obj.set("i64", 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 b = t.read_mem(addr, 8).map_err(|e| throw(&c, &e))?;
|
|
let mut a = [0u8; 8]; a.copy_from_slice(&b);
|
|
let v = i64::from_le_bytes(a);
|
|
i64_to_bigint(&c, v)
|
|
})?)?;
|
|
}
|
|
// bytes(n) → hex string
|
|
{
|
|
let target = target.clone();
|
|
obj.set("bytes", Function::new(c.clone(), move |c: Ctx<'js>, n: usize| -> rquickjs::Result<String> {
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let b = t.read_mem(addr, n).map_err(|e| throw(&c, &e))?;
|
|
let mut s = String::with_capacity(n * 2);
|
|
for byte in &b { s.push_str(&format!("{:02x}", byte)); }
|
|
Ok(s)
|
|
})?)?;
|
|
}
|
|
|
|
Ok(obj.into_value())
|
|
})?;
|
|
state_obj.set("memory", mem_fn)?;
|
|
}
|
|
|
|
// state.disasm(n=8) — disassemble n instructions starting at pc.
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, n: rquickjs::function::Opt<usize>| -> rquickjs::Result<Value<'js>> {
|
|
let n = n.0.unwrap_or(8).max(1);
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let pc = t.regs().map_err(|e| throw(&c, &e))?.rip;
|
|
let bytes = t.read_mem_pristine(pc, n.saturating_mul(15))
|
|
.map_err(|e| throw(&c, &e))?;
|
|
let decoded = crate::disasm::decode(pc, &bytes, n);
|
|
decoded_to_array(&c, &decoded, t.symbols())
|
|
})?;
|
|
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)?;
|
|
}
|
|
|
|
// ---------- disasm(addr, n=8) ----------
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>, n: rquickjs::function::Opt<usize>| -> rquickjs::Result<Value<'js>> {
|
|
let addr = coerce_addr(&c, &addr)?;
|
|
let n = n.0.unwrap_or(8).max(1);
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let bytes = t.read_mem_pristine(addr, n.saturating_mul(15))
|
|
.map_err(|e| throw(&c, &e))?;
|
|
let decoded = crate::disasm::decode(addr, &bytes, n);
|
|
decoded_to_array(&c, &decoded, t.symbols())
|
|
})?;
|
|
globals.set("disasm", f)?;
|
|
}
|
|
|
|
// ---------- 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)?;
|
|
}
|
|
|
|
// ---------- sym(addr) -> "name+0xN" | null ----------
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result<Value<'js>> {
|
|
let addr = coerce_addr(&c, &addr)?;
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
|
match s.name_for_addr(addr) {
|
|
Some(name) => Ok(rquickjs::String::from_str(c.clone(), &name)?.into_value()),
|
|
None => Ok(Value::new_null(c.clone())),
|
|
}
|
|
})?;
|
|
globals.set("sym", f)?;
|
|
}
|
|
|
|
// ---------- addr(name) -> BigInt | null ----------
|
|
{
|
|
let target = target.clone();
|
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, name: String| -> rquickjs::Result<Value<'js>> {
|
|
let slot = target.borrow();
|
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
|
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
|
match s.addr_for_name(&name) {
|
|
Some(a) => u64_to_bigint(&c, a),
|
|
None => Ok(Value::new_null(c.clone())),
|
|
}
|
|
})?;
|
|
globals.set("addr", f)?;
|
|
}
|
|
|
|
// ---------- symbols() -> { count, load_base, path } ----------
|
|
{
|
|
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 s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
|
let obj = Object::new(c.clone())?;
|
|
obj.set("count", s.len() as u32)?;
|
|
obj.set("load_base", u64_to_bigint(&c, s.load_base)?)?;
|
|
obj.set("path", s.path.to_string_lossy().into_owned())?;
|
|
Ok(obj.into_value())
|
|
})?;
|
|
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.
|
|
const PRELUDE: &str = include_str!("prelude.js");
|
|
c.eval::<(), _>(PRELUDE)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// --- helpers --------------------------------------------------------------
|
|
|
|
/// Convert a Vec<Decoded> from the disassembler into a JS Array of records:
|
|
/// [{addr: BigInt, bytes: "ffce..", text: "mov rax, ...", target?: BigInt, target_sym?: "name"}]
|
|
/// `bytes` is a hex string for compactness; `target` and `target_sym` are
|
|
/// only present for branch/call instructions.
|
|
fn decoded_to_array<'js>(
|
|
c: &Ctx<'js>,
|
|
decoded: &[crate::disasm::Decoded],
|
|
symbols: Option<&crate::symbols::SymbolTable>,
|
|
) -> rquickjs::Result<Value<'js>> {
|
|
let arr = rquickjs::Array::new(c.clone())?;
|
|
for (i, d) in decoded.iter().enumerate() {
|
|
let obj = Object::new(c.clone())?;
|
|
obj.set("addr", u64_to_bigint(c, d.addr)?)?;
|
|
let mut bytes_hex = String::with_capacity(d.bytes.len() * 2);
|
|
for b in &d.bytes { bytes_hex.push_str(&format!("{:02x}", b)); }
|
|
obj.set("bytes", bytes_hex)?;
|
|
obj.set("text", d.text.as_str())?;
|
|
if let Some(t) = d.branch_target {
|
|
obj.set("target", u64_to_bigint(c, t)?)?;
|
|
if let Some(s) = symbols.and_then(|s| s.name_for_addr(t)) {
|
|
obj.set("target_sym", s)?;
|
|
}
|
|
}
|
|
arr.set(i, obj)?;
|
|
}
|
|
Ok(arr.into_value())
|
|
}
|
|
|
|
/// 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
|
|
/// in rquickjs for "throw an error from a Rust callback" is to call
|
|
/// `Exception::throw_*(ctx, msg)`.
|
|
fn throw(c: &Ctx<'_>, msg: &str) -> rquickjs::Error {
|
|
rquickjs::Exception::throw_message(c, msg)
|
|
}
|
|
|
|
/// Accept either a JS Number or BigInt and produce a u64 address.
|
|
fn coerce_addr<'js>(c: &Ctx<'js>, v: &Value<'js>) -> rquickjs::Result<u64> {
|
|
if let Some(n) = v.as_number() {
|
|
if n < 0.0 || !n.is_finite() {
|
|
return Err(throw(c, "address must be a non-negative finite number"));
|
|
}
|
|
return Ok(n as u64);
|
|
}
|
|
if v.is_int() {
|
|
let i: i32 = v.get()?;
|
|
if i < 0 { return Err(throw(c, "address must be non-negative")); }
|
|
return Ok(i as u64);
|
|
}
|
|
// Try BigInt: route through global BigInt -> toString(10) -> parse.
|
|
let globals = c.globals();
|
|
let string_fn: Function = globals.get("String")?;
|
|
let s: String = string_fn.call((v.clone(),)).map_err(|_| throw(c, "bad address"))?;
|
|
// BigInt String() form is like "1234567" (no n suffix).
|
|
s.parse::<u64>().map_err(|_| throw(c, &format!("bad address: {}", s)))
|
|
}
|
|
|
|
/// Convert a u64 to a JS BigInt by evaluating `BigInt("<dec>")` in the context.
|
|
/// Not the fastest, but completely robust and avoids hunting the rquickjs API
|
|
/// surface in v0.1.
|
|
fn u64_to_bigint<'js>(c: &Ctx<'js>, v: u64) -> rquickjs::Result<Value<'js>> {
|
|
let globals = c.globals();
|
|
let bi: Function = globals.get("BigInt")?;
|
|
let s = v.to_string();
|
|
bi.call((s,))
|
|
}
|
|
|
|
fn i64_to_bigint<'js>(c: &Ctx<'js>, v: i64) -> rquickjs::Result<Value<'js>> {
|
|
let globals = c.globals();
|
|
let bi: Function = globals.get("BigInt")?;
|
|
let s = v.to_string();
|
|
bi.call((s,))
|
|
}
|