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",
|
"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",
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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_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).
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user