use dbgscope::dbgeng::{DebugEngine, Flow, FunctionExtent, Operand};
fn main() {
let mut args = std::env::args().skip(1);
let (Some(dump), Some(symbol)) = (args.next(), args.next()) else {
eprintln!("usage: typed_disassembly <dump> <module>!<symbol> [image search path]");
std::process::exit(2);
};
let image_path = args.next();
let e = DebugEngine::new();
e.open_dump(&dump).expect("opening the dump failed");
e.wait_for_event(30_000)
.expect("the dump did not load within thirty seconds");
if let Some(path) = &image_path {
e.execute_command(&format!(".exepath+ {path}"))
.expect("setting the image search path failed");
e.reload_symbols("/f").expect("reloading failed");
}
let set = e.instruction_set();
println!(
"instruction set: {set:?} (operands read: {})",
set.operands_are_read()
);
let entry = e
.symbol_offset(&symbol)
.unwrap_or_else(|error| panic!("{symbol} did not resolve: {error}"));
println!("{symbol} at {entry:#x}");
match e.function_extent(entry) {
Ok(FunctionExtent::Region { begin, end }) => println!(
"unwind region: {begin:#x}..{end:#x} ({} bytes) — a region, not the function",
end - begin
),
Ok(FunctionExtent::NoEntry) => println!("unwind region: no entry (leaf, or not code)"),
Ok(FunctionExtent::Unsupported(set)) => {
println!("unwind region: not decoded for {set:?}")
}
Err(error) => println!("unwind region: unavailable ({error})"),
}
let module = e
.module_at(entry)
.ok()
.flatten()
.expect("the entry is in no module");
let (low, high) = (module.base, module.base + module.size as u64);
let owner = e.symbol_for(entry).map(|(name, _)| name);
println!(
"ownership: {}",
match &owner {
Some(name) => format!("edges kept inside {name}"),
None => "no symbol for the entry — module bounds only".to_string(),
}
);
let mut seen = std::collections::HashSet::new();
let mut queue = vec![entry];
let mut instructions = Vec::new();
let mut left_the_function = Vec::new();
while let Some(at) = queue.pop() {
if at < low || at >= high || !seen.insert(at) || seen.len() > 20_000 {
continue;
}
let Ok(pair) = e.disassemble(at, 2) else {
continue;
};
let fall_through = pair.get(1).map(|next| next.address);
let Some(instruction) = pair.into_iter().next() else {
continue;
};
if let Some(target) = instruction.flow.target()
&& !matches!(instruction.flow, Flow::Call(_))
{
let stays = match &owner {
Some(owner) => e.symbol_for(target).is_some_and(|(name, _)| &name == owner),
None => true,
};
if stays {
queue.push(target);
} else {
left_the_function.push((instruction.address, target));
}
}
if instruction.flow.falls_through() {
queue.extend(fall_through);
}
instructions.push(instruction);
}
instructions.sort_by_key(|instruction| instruction.address);
println!("walked {} instructions", instructions.len());
println!(
"cross-function jumps declined: {}\n",
left_the_function.len()
);
let (mut unreadable, mut other_operands, mut unknown_flow, mut unread) =
(0usize, 0usize, 0usize, 0usize);
let mut other_kinds: std::collections::BTreeMap<String, usize> =
std::collections::BTreeMap::new();
let mut candidates: Vec<(u64, u64)> = Vec::new();
let mut calls: Vec<(u64, String)> = Vec::new();
for instruction in &instructions {
if instruction.text.starts_with('?') {
unreadable += 1;
continue;
}
if instruction.flow == Flow::Unknown {
unknown_flow += 1;
println!(
" UNKNOWN FLOW {:#x} {}",
instruction.address, instruction.text
);
}
for operand in &instruction.operands {
match operand {
Operand::Other(text) => {
other_operands += 1;
*other_kinds.entry(text.clone()).or_insert(0usize) += 1;
println!(
" OTHER OPERAND {:#x} {} <- {text:?}",
instruction.address, instruction.text
);
}
Operand::Undecoded(space) => {
unread += 1;
println!(
" UNREAD {:#x} {} <- {space:?}",
instruction.address, instruction.text
);
}
_ => {}
}
}
if matches!(
instruction.mnemonic.as_str(),
"cmp" | "sub" | "xor" | "add" | "subs" | "adds"
) && let Some(Operand::Immediate(value)) = instruction
.operands
.iter()
.find(|operand| matches!(operand, Operand::Immediate(_)))
&& plausible_ioctl(*value)
{
candidates.push((instruction.address, *value));
}
if let Flow::Call(Some(target)) = instruction.flow {
let name = e
.symbol_for(target)
.map(|(name, _)| name)
.unwrap_or_else(|| format!("{target:#x}"));
calls.push((instruction.address, name));
}
if let Some(Operand::Memory(memory)) = instruction.operands.first()
&& let (Flow::Call(None), Some(slot)) = (instruction.flow, memory.address)
{
let name = e
.symbol_for(slot)
.map(|(name, _)| name)
.unwrap_or_else(|| format!("{slot:#x}"));
calls.push((instruction.address, format!("[{name}]")));
}
}
if let Ok(FunctionExtent::Region { begin, end }) = e.function_extent(entry) {
match e.decode_range(begin, (end - begin) as usize) {
Ok(ranged) => {
let walked: std::collections::HashMap<u64, &dbgscope::dbgeng::Instruction> =
instructions
.iter()
.map(|instruction| (instruction.address, instruction))
.collect();
let mut compared = 0usize;
let mut disagreed = 0usize;
for one in &ranged {
let Some(other) = walked.get(&one.address) else {
continue;
};
compared += 1;
let mnemonics_differ = set.operands_are_read()
&& !one.mnemonic.is_empty()
&& !other.mnemonic.is_empty()
&& one.mnemonic != other.mnemonic;
if one.bytes != other.bytes || mnemonics_differ || one.flow != other.flow {
disagreed += 1;
println!(
" DISAGREE {:#x} ranged {} {:?} walked {} {:?}",
one.address, one.mnemonic, one.flow, other.mnemonic, other.flow
);
}
}
println!(
"\n--- the two decode paths over the first region ---\nrange-decoded {}, \
compared {compared}, disagreed {disagreed}",
ranged.len()
);
}
Err(error) => println!("\nrange decode unavailable: {error}"),
}
}
println!("\n--- what the reading could not read ---");
println!("instructions the engine could not render: {unreadable}");
println!("operands kept as Other: {other_operands}");
println!("instructions the decoder did not read: {unread}");
println!("instructions with Unknown flow: {unknown_flow}");
for (text, count) in &other_kinds {
println!(" {count:>5} {text}");
}
println!(
"\n--- plausible CTL_CODE immediates ({}) ---",
candidates.len()
);
for (address, value) in &candidates {
let value = *value;
println!(
" {address:#x} {value:#010x} device {:#06x} function {:#05x} method {} access {}",
(value >> 16) & 0xffff,
(value >> 2) & 0xfff,
value & 3,
(value >> 14) & 3
);
}
println!("\n--- direct and thunked calls ({}) ---", calls.len());
for (address, name) in &calls {
println!(" {address:#x} {name}");
}
}
fn plausible_ioctl(value: u64) -> bool {
if value == 0 || value > u32::MAX as u64 {
return false;
}
if value & 0x8000_0000 != 0 {
return false;
}
(value >> 16) & 0xffff != 0
}