v0.1: spawn/step/cont, state.{pc,sp,registers,memory}, prelude.js
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+201
@@ -0,0 +1,201 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "llmdbg"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"nix",
|
||||
"rquickjs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0688f8b0192998cca685adefdfad3483da295fa40a0ec406b4c14ecd729e858"
|
||||
dependencies = [
|
||||
"rquickjs-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs-core"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fee8c5383f0cfda3b980a80ca4520e726e09b593c59562f579daa51b6c20411"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"relative-path",
|
||||
"rquickjs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs-sys"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "698077537c286a169de8693b216672bcef148bf2e2e112ebf50758c68e9afa09"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "llmdbg"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nix = { version = "0.31", features = ["ptrace", "process", "uio", "signal"] }
|
||||
rquickjs = { version = "0.12", features = ["std"] }
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,403 @@
|
||||
//! QuickJS REPL: persistent context, ptrace target lives behind an Rc<RefCell>,
|
||||
//! 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) }
|
||||
|
||||
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(n=1) ----------
|
||||
{
|
||||
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", f)?;
|
||||
}
|
||||
|
||||
// ---------- continue() / cont() ----------
|
||||
{
|
||||
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()` for v0.1.
|
||||
globals.set("cont", f)?;
|
||||
}
|
||||
|
||||
// ---------- 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<Value<'js>> {
|
||||
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<Value<'js>> {
|
||||
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<Value<'js>> {
|
||||
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())
|
||||
}),
|
||||
)?;
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
}
|
||||
|
||||
globals.set("state", state_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 --------------------------------------------------------------
|
||||
|
||||
/// 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,))
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
//! llmdbg v0.1 — stdin/stdout REPL daemon.
|
||||
//!
|
||||
//! Protocol:
|
||||
//! - Read lines from stdin until a line is exactly `---END---`.
|
||||
//! - Evaluate the accumulated buffer as one JS script in a persistent context.
|
||||
//! - Print any `print()` output, then the script's result expression (if not
|
||||
//! undefined), then a line `---DONE---`. On error, print `error: ...`
|
||||
//! before `---DONE---`.
|
||||
//! - Repeat.
|
||||
//! - EOF on stdin exits cleanly.
|
||||
|
||||
mod target;
|
||||
mod js;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
const SCRIPT_END: &str = "---END---";
|
||||
const RESPONSE_END: &str = "---DONE---";
|
||||
|
||||
fn main() {
|
||||
let mut repl = match js::Repl::new() {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("repl init failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let stdin = std::io::stdin();
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdin = stdin.lock();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
let mut script = String::new();
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = match stdin.read_line(&mut line) {
|
||||
Ok(n) => n,
|
||||
Err(e) => { eprintln!("read error: {}", e); break; }
|
||||
};
|
||||
if n == 0 {
|
||||
// EOF.
|
||||
if !script.is_empty() {
|
||||
// Treat trailing buffer as final script.
|
||||
eval_and_print(&mut repl, &script, &mut stdout);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let stripped = line.trim_end_matches(|c| c == '\n' || c == '\r');
|
||||
if stripped == SCRIPT_END {
|
||||
eval_and_print(&mut repl, &script, &mut stdout);
|
||||
script.clear();
|
||||
} else {
|
||||
script.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown: if a target is still alive, try to kill it so we don't leak.
|
||||
if let Some(pid) = repl.target_pid() {
|
||||
let _ = nix::sys::signal::kill(
|
||||
nix::unistd::Pid::from_raw(pid),
|
||||
nix::sys::signal::Signal::SIGKILL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_and_print(repl: &mut js::Repl, src: &str, out: &mut impl Write) {
|
||||
let r = repl.eval(src);
|
||||
if !r.printed.is_empty() {
|
||||
let _ = out.write_all(r.printed.as_bytes());
|
||||
}
|
||||
if let Some(e) = &r.error {
|
||||
let _ = writeln!(out, "{}", e);
|
||||
} else if !r.result.is_empty() {
|
||||
let _ = writeln!(out, "{}", r.result);
|
||||
}
|
||||
let _ = writeln!(out, "{}", RESPONSE_END);
|
||||
let _ = out.flush();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// llmdbg prelude.
|
||||
//
|
||||
// Loaded once at REPL startup, after the native (Rust-backed) globals
|
||||
// (spawn, step, cont, print, state, ctx, ...) are installed. Anything that
|
||||
// can be expressed in plain JS belongs here, not in Rust.
|
||||
//
|
||||
// Conventions:
|
||||
// - Attach exports to globalThis.
|
||||
// - Keep names short; this is a debugger REPL, not a library.
|
||||
// - Don't capture state in module scope — the REPL has one persistent
|
||||
// context, but `ctx` is the user's scratchpad. We don't touch it.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// hex / unhex — Number or BigInt → "0x..." and back.
|
||||
//
|
||||
// hex(v):
|
||||
// BigInt → "0x" + v.toString(16)
|
||||
// non-negative Number (finite) → same, after truncation
|
||||
// anything else → coerced via BigInt(v); throws if that fails
|
||||
//
|
||||
// unhex(s):
|
||||
// "0xff" | "ff" | "0XFF" → BigInt(255)
|
||||
// throws on anything else
|
||||
|
||||
globalThis.hex = function (v) {
|
||||
if (typeof v === "bigint") {
|
||||
if (v < 0n) return "-0x" + (-v).toString(16);
|
||||
return "0x" + v.toString(16);
|
||||
}
|
||||
if (typeof v === "number") {
|
||||
if (!Number.isFinite(v)) throw new Error("hex(): non-finite number");
|
||||
if (v < 0) return "-0x" + Math.trunc(-v).toString(16);
|
||||
return "0x" + Math.trunc(v).toString(16);
|
||||
}
|
||||
// Last resort: try BigInt coercion (handles boolean, numeric strings).
|
||||
return hex(BigInt(v));
|
||||
};
|
||||
|
||||
globalThis.unhex = function (s) {
|
||||
if (typeof s !== "string") throw new Error("unhex(): expected string");
|
||||
let neg = false;
|
||||
let t = s.trim();
|
||||
if (t.startsWith("-")) { neg = true; t = t.slice(1); }
|
||||
if (t.startsWith("0x") || t.startsWith("0X")) t = t.slice(2);
|
||||
if (!/^[0-9a-fA-F]+$/.test(t)) throw new Error("unhex(): bad hex: " + s);
|
||||
const v = BigInt("0x" + t);
|
||||
return neg ? -v : v;
|
||||
};
|
||||
|
||||
// JSON.stringify replacer that turns BigInt into "0x..." so nested addresses
|
||||
// don't crash the serializer. Used by repr() and dump().
|
||||
function _bigintReplacer(_key, value) {
|
||||
if (typeof value === "bigint") return hex(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// dump(label, v) — terse one-line formatter for inspection during a step loop.
|
||||
// Numbers and BigInts come out as hex; everything else gets the default
|
||||
// JS string coercion. Designed for token-efficient log lines.
|
||||
|
||||
globalThis.dump = function (label, v) {
|
||||
let s;
|
||||
if (typeof v === "bigint" || typeof v === "number") {
|
||||
s = hex(v);
|
||||
} else if (v === null || v === undefined) {
|
||||
s = String(v);
|
||||
} else if (typeof v === "object") {
|
||||
try { s = JSON.stringify(v, _bigintReplacer); } catch (_) { s = String(v); }
|
||||
} else {
|
||||
s = String(v);
|
||||
}
|
||||
print(label + "\t" + s);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// repr(v) — like hex() for numerics, JSON for objects, String() otherwise.
|
||||
// Useful as a building block when you want a single value rendered.
|
||||
|
||||
globalThis.repr = function (v) {
|
||||
if (typeof v === "bigint" || typeof v === "number") return hex(v);
|
||||
if (v === null || v === undefined) return String(v);
|
||||
if (typeof v === "object") {
|
||||
try { return JSON.stringify(v, _bigintReplacer); } catch (_) { return String(v); }
|
||||
}
|
||||
return String(v);
|
||||
};
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
//! Linux x86-64 ptrace wrapper for v0.1.
|
||||
//!
|
||||
//! Spawn-only, single-threaded debuggee. The `Target` owns the child PID
|
||||
//! and tracks whether the child is currently stopped or has exited.
|
||||
|
||||
use nix::sys::ptrace;
|
||||
use nix::sys::signal::Signal;
|
||||
use nix::sys::uio::{process_vm_readv, RemoteIoVec};
|
||||
use nix::sys::wait::{waitpid, WaitStatus};
|
||||
use nix::unistd::{execvp, fork, ForkResult, Pid};
|
||||
use std::ffi::CString;
|
||||
use std::io::IoSliceMut;
|
||||
|
||||
/// Why the target stopped after a step/continue, or that it has exited.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StopReason {
|
||||
/// Hit a SIGTRAP from single-stepping (or, in future, a breakpoint).
|
||||
Step,
|
||||
/// Caught some other signal — we don't yet inject it back, just report.
|
||||
Signal(Signal),
|
||||
/// Process exited normally with this code.
|
||||
Exited(i32),
|
||||
/// Process was killed by a signal.
|
||||
Killed(Signal),
|
||||
}
|
||||
|
||||
impl StopReason {
|
||||
/// Short tag for the REPL caller to print.
|
||||
pub fn tag(&self) -> String {
|
||||
match self {
|
||||
StopReason::Step => "step".into(),
|
||||
StopReason::Signal(s) => format!("signal:{}", s),
|
||||
StopReason::Exited(c) => format!("exited:{}", c),
|
||||
StopReason::Killed(s) => format!("killed:{}", s),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_alive(&self) -> bool {
|
||||
matches!(self, StopReason::Step | StopReason::Signal(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum State {
|
||||
/// Stopped, ready to read regs/memory and step.
|
||||
Stopped(StopReason),
|
||||
/// Dead. Subsequent operations should fail cleanly.
|
||||
Dead(StopReason),
|
||||
}
|
||||
|
||||
pub struct Target {
|
||||
pid: Pid,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid.as_raw()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn state(&self) -> &State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// True if the target is currently stopped and inspectable.
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
matches!(self.state, State::Stopped(_))
|
||||
}
|
||||
|
||||
/// fork() + PTRACE_TRACEME + execvp(). Returns once the child has been
|
||||
/// caught at its initial post-exec SIGTRAP.
|
||||
pub fn spawn(path: &str, args: &[String]) -> Result<Target, String> {
|
||||
let c_path = CString::new(path).map_err(|e| format!("bad path: {}", e))?;
|
||||
let mut c_args: Vec<CString> = Vec::with_capacity(args.len() + 1);
|
||||
c_args.push(c_path.clone());
|
||||
for a in args {
|
||||
c_args.push(CString::new(a.as_str()).map_err(|e| format!("bad arg: {}", e))?);
|
||||
}
|
||||
|
||||
// SAFETY: fork() must only call async-signal-safe functions in the
|
||||
// child. We use ptrace::traceme and execvp, both fine.
|
||||
let fork_result = unsafe { fork() }.map_err(|e| format!("fork failed: {}", e))?;
|
||||
|
||||
match fork_result {
|
||||
ForkResult::Child => {
|
||||
// In the child. Anything that goes wrong here we report via
|
||||
// exit code; the parent will see the abnormal exit on waitpid.
|
||||
if ptrace::traceme().is_err() {
|
||||
unsafe { libc::_exit(127) };
|
||||
}
|
||||
// execvp replaces the process image; on success it does not return.
|
||||
let _ = execvp(&c_path, &c_args);
|
||||
unsafe { libc::_exit(127) };
|
||||
}
|
||||
ForkResult::Parent { child } => {
|
||||
// Wait for the initial post-exec SIGTRAP.
|
||||
let status = waitpid(child, None)
|
||||
.map_err(|e| format!("waitpid (initial) failed: {}", e))?;
|
||||
match status {
|
||||
WaitStatus::Stopped(_, Signal::SIGTRAP) => Ok(Target {
|
||||
pid: child,
|
||||
state: State::Stopped(StopReason::Step),
|
||||
}),
|
||||
WaitStatus::Exited(_, code) => {
|
||||
Err(format!("child exited immediately with code {} (exec failed?)", code))
|
||||
}
|
||||
other => Err(format!("unexpected initial wait status: {:?}", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PTRACE_SINGLESTEP + waitpid. Updates internal state.
|
||||
pub fn step(&mut self) -> Result<StopReason, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
ptrace::step(self.pid, None).map_err(|e| format!("ptrace step: {}", e))?;
|
||||
let reason = self.wait_for_stop()?;
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
/// PTRACE_CONT + waitpid.
|
||||
pub fn cont(&mut self) -> Result<StopReason, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
ptrace::cont(self.pid, None).map_err(|e| format!("ptrace cont: {}", e))?;
|
||||
let reason = self.wait_for_stop()?;
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
fn wait_for_stop(&mut self) -> Result<StopReason, String> {
|
||||
let status = waitpid(self.pid, None).map_err(|e| format!("waitpid: {}", e))?;
|
||||
let reason = match status {
|
||||
WaitStatus::Stopped(_, Signal::SIGTRAP) => StopReason::Step,
|
||||
WaitStatus::Stopped(_, sig) => StopReason::Signal(sig),
|
||||
WaitStatus::Exited(_, code) => StopReason::Exited(code),
|
||||
WaitStatus::Signaled(_, sig, _) => StopReason::Killed(sig),
|
||||
other => return Err(format!("unexpected wait status: {:?}", other)),
|
||||
};
|
||||
self.state = if reason.is_alive() {
|
||||
State::Stopped(reason.clone())
|
||||
} else {
|
||||
State::Dead(reason.clone())
|
||||
};
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
pub fn regs(&self) -> Result<libc::user_regs_struct, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
ptrace::getregs(self.pid).map_err(|e| format!("getregs: {}", e))
|
||||
}
|
||||
|
||||
/// Read `len` bytes from the target's memory at `addr` via process_vm_readv.
|
||||
/// Much faster than peek loops for >8 bytes, and works for any size.
|
||||
pub fn read_mem(&self, addr: u64, len: usize) -> Result<Vec<u8>, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
let mut buf = vec![0u8; len];
|
||||
let mut local = [IoSliceMut::new(&mut buf)];
|
||||
let remote = [RemoteIoVec { base: addr as usize, len }];
|
||||
let n = process_vm_readv(self.pid, &mut local, &remote)
|
||||
.map_err(|e| format!("read_mem(0x{:x}, {}): {}", addr, len, e))?;
|
||||
if n != len {
|
||||
return Err(format!(
|
||||
"short read at 0x{:x}: got {} of {}",
|
||||
addr, n, len
|
||||
));
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Target {
|
||||
fn drop(&mut self) {
|
||||
// Best effort: if the target is still alive, detach so we don't leave
|
||||
// it stopped. If it's already dead, nothing to do.
|
||||
if self.is_stopped() {
|
||||
let _ = ptrace::detach(self.pid, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user