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:
Generated
+16
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<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)?;
|
||||
}
|
||||
|
||||
// ---------- 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
|
||||
@@ -457,6 +489,34 @@ fn install_globals<'js>(
|
||||
|
||||
// --- 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).
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
mod target;
|
||||
mod symbols;
|
||||
mod breakpoint;
|
||||
mod disasm;
|
||||
mod js;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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<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 {
|
||||
|
||||
Reference in New Issue
Block a user