diff --git a/Cargo.lock b/Cargo.lock index 3d80fc2..f4ca80a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,21 @@ dependencies = [ "foldhash", ] +[[package]] +name = "iced-x86" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.186" @@ -75,6 +90,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" name = "llmdbg" version = "0.1.0" dependencies = [ + "iced-x86", "libc", "nix", "object", diff --git a/Cargo.toml b/Cargo.toml index 8c62ce2..f4cad59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ nix = { version = "0.31", features = ["ptrace", "process", "uio", "signal"] } rquickjs = { version = "0.12", features = ["std"] } libc = "0.2" object = { version = "0.36", default-features = false, features = ["read", "std"] } +iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "nasm", "fast_fmt", "instr_info"] } diff --git a/src/disasm.rs b/src/disasm.rs new file mode 100644 index 0000000..54f6172 --- /dev/null +++ b/src/disasm.rs @@ -0,0 +1,78 @@ +//! x86-64 disassembly helper using iced-x86. +//! +//! Tiny wrapper: bytes → Vec of instruction records. Symbol resolution +//! (for JCC/CALL targets) is handled by the caller after decoding; we +//! emit the raw NASM-syntax text. + +use iced_x86::{Decoder, DecoderOptions, FastFormatter, Instruction}; + +#[derive(Debug, Clone)] +pub struct Decoded { + /// Virtual address of this instruction in the target. + pub addr: u64, + /// The raw instruction bytes (length = end_ip - addr). + pub bytes: Vec, + /// Disassembly text (NASM-ish), e.g. "mov rax, qword ptr [rsi+8]". + pub text: String, + /// The destination if this is a branch or call instruction (for symbol + /// resolution by the caller); None otherwise. + pub branch_target: Option, +} + +/// Decode up to `max` instructions starting at `addr` from `bytes`. Stops +/// early if the buffer runs out mid-instruction. Decoder will mark short +/// reads as `is_invalid`; we still emit them as `db 0x..` style for the +/// caller to format. +pub fn decode(addr: u64, bytes: &[u8], max: usize) -> Vec { + let mut decoder = Decoder::with_ip(64, bytes, addr, DecoderOptions::NONE); + let mut formatter = FastFormatter::new(); + formatter.options_mut().set_use_hex_prefix(true); + formatter.options_mut().set_always_show_memory_size(false); + + let mut out = Vec::with_capacity(max); + let mut instr = Instruction::default(); + let mut text_buf = String::with_capacity(64); + + while out.len() < max && decoder.can_decode() { + decoder.decode_out(&mut instr); + let start = instr.ip(); + let len = instr.len(); + let end = start.wrapping_add(len as u64); + let off_in_buf = (start - addr) as usize; + let raw = if off_in_buf + len <= bytes.len() { + bytes[off_in_buf..off_in_buf + len].to_vec() + } else { + bytes[off_in_buf..].to_vec() + }; + + text_buf.clear(); + formatter.format(&instr, &mut text_buf); + + let branch_target = match instr.flow_control() { + iced_x86::FlowControl::UnconditionalBranch + | iced_x86::FlowControl::ConditionalBranch + | iced_x86::FlowControl::Call => { + use iced_x86::OpKind; + match instr.op0_kind() { + OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64 => { + Some(instr.near_branch_target()) + } + _ => None, + } + } + _ => None, + }; + + out.push(Decoded { + addr: start, + bytes: raw, + text: text_buf.clone(), + branch_target, + }); + + // Defensive: decoder advances IP automatically, but bail if we + // would loop on a zero-length decode. + if end == start { break; } + } + out +} diff --git a/src/js.rs b/src/js.rs index d4606bb..5128de9 100644 --- a/src/js.rs +++ b/src/js.rs @@ -298,9 +298,41 @@ fn install_globals<'js>( 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| -> rquickjs::Result> { + 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)?; + } + 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| -> rquickjs::Result> { + 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 @@ -457,6 +489,34 @@ fn install_globals<'js>( // --- helpers -------------------------------------------------------------- +/// Convert a Vec 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> { + 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). diff --git a/src/main.rs b/src/main.rs index dbdaaa9..cc460eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod target; mod symbols; mod breakpoint; +mod disasm; mod js; use std::io::{BufRead, Write}; diff --git a/src/prelude.js b/src/prelude.js index 7acfb64..f1064a9 100644 --- a/src/prelude.js +++ b/src/prelude.js @@ -110,4 +110,46 @@ globalThis.here = function (label) { if (label) print(label, s); else print(s); }; +// --------------------------------------------------------------------------- +// show.disasm(n=8, [addr]) +// Pretty-print N instructions starting at addr (default: pc). +// Each line: ADDR BYTES MNEMONIC ARGS [-> sym] +// +// show.bp() — pretty-print breakpoint list +// show.regs([keys]) — pretty-print register subset (default: a useful set) + +globalThis.show = { + disasm(n, at) { + n = n ?? 8; + const ins = (at === undefined) ? state.disasm(n) : disasm(at, n); + for (const i of ins) { + const tgt = i.target_sym + ? ` -> ${i.target_sym}` + : (i.target !== undefined ? ` -> ${hex(i.target)}` : ""); + print(` ${hex(i.addr).padEnd(14)} ${i.bytes.padEnd(22)} ${i.text}${tgt}`); + } + }, + bp() { + const list = bp.list(); + if (list.length === 0) { print("(no breakpoints)"); return; } + for (const b of list) { + const sym = (() => { try { return addrstr(b.addr); } catch (_) { return hex(b.addr); } })(); + const name = b.name ? ` "${b.name}"` : ""; + print(` #${b.id}${name} ${sym} hits=${b.hits} ${b.enabled ? "enabled" : "DISABLED"}`); + } + }, + regs(keys) { + const r = state.registers; + const which = keys ?? ["rax","rbx","rcx","rdx","rsi","rdi","rbp","rsp","rip","r8","r9","eflags"]; + const parts = which.map(k => `${k}=${hex(r[k])}`); + // wrap loosely so a single line isn't 200 chars + let line = ""; + for (const p of parts) { + if (line.length + p.length + 2 > 100) { print(" " + line.trim()); line = ""; } + line += p + " "; + } + if (line) print(" " + line.trim()); + }, +}; + diff --git a/src/target.rs b/src/target.rs index ee74a0b..6181a3b 100644 --- a/src/target.rs +++ b/src/target.rs @@ -338,6 +338,25 @@ impl Target { } Ok(buf) } + + /// Like read_mem but with active breakpoints transparently restored to + /// their original bytes in the returned buffer. Use this for disassembly + /// or any inspection of *code* — otherwise the buffer will contain 0xCC + /// at our breakpoint addresses, which is misleading. + pub fn read_mem_pristine(&self, addr: u64, len: usize) -> Result, String> { + let mut buf = self.read_mem(addr, len)?; + let end = addr.saturating_add(len as u64); + for bp in self.breakpoints.iter() { + if !bp.enabled { continue; } + if bp.addr >= addr && bp.addr < end { + let idx = (bp.addr - addr) as usize; + if idx < buf.len() { + buf[idx] = bp.orig_byte; + } + } + } + Ok(buf) + } } impl Drop for Target {