v0.5: x86-64 disassembly via iced-x86

- state.disasm(n=8), disasm(addr, n=8) -> [{addr, bytes, text, target?, target_sym?}]
- read_mem_pristine() overlays orig bytes through active INT3 breakpoints
- branch/call targets get symbolized when they land in known symbols
- show.disasm / show.bp / show.regs prelude helpers for ergonomic printing
This commit is contained in:
claude
2026-05-27 21:24:01 +00:00
parent 8ae6103ce7
commit 2ed17f0286
7 changed files with 217 additions and 0 deletions
Generated
+16
View File
@@ -65,6 +65,21 @@ dependencies = [
"foldhash", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@@ -75,6 +90,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
name = "llmdbg" name = "llmdbg"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"iced-x86",
"libc", "libc",
"nix", "nix",
"object", "object",
+1
View File
@@ -8,3 +8,4 @@ nix = { version = "0.31", features = ["ptrace", "process", "uio", "signal"] }
rquickjs = { version = "0.12", features = ["std"] } rquickjs = { version = "0.12", features = ["std"] }
libc = "0.2" libc = "0.2"
object = { version = "0.36", default-features = false, features = ["read", "std"] } 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"] }
+78
View File
@@ -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<u8>,
/// 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<u64>,
}
/// 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<Decoded> {
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
}
+60
View File
@@ -298,9 +298,41 @@ fn install_globals<'js>(
state_obj.set("memory", mem_fn)?; 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)?;
}
globals.set("state", state_obj)?; 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 ---------- // ---------- prev ----------
// Mirror of state for register-only data, backed by the pre-resume // Mirror of state for register-only data, backed by the pre-resume
// snapshot. Property accessors throw if no snapshot is available yet // snapshot. Property accessors throw if no snapshot is available yet
@@ -457,6 +489,34 @@ fn install_globals<'js>(
// --- helpers -------------------------------------------------------------- // --- 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, /// Build a JS object with `pc`, `sp`, and `registers` accessor properties,
/// each backed by a closure that fetches a `user_regs_struct` on demand. /// each backed by a closure that fetches a `user_regs_struct` on demand.
/// Used for both `state` (live regs via ptrace) and `prev` (snapshot). /// Used for both `state` (live regs via ptrace) and `prev` (snapshot).
+1
View File
@@ -12,6 +12,7 @@
mod target; mod target;
mod symbols; mod symbols;
mod breakpoint; mod breakpoint;
mod disasm;
mod js; mod js;
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
+42
View File
@@ -110,4 +110,46 @@ globalThis.here = function (label) {
if (label) print(label, s); else print(s); 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());
},
};
+19
View File
@@ -338,6 +338,25 @@ impl Target {
} }
Ok(buf) 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<Vec<u8>, 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 { impl Drop for Target {