use dbgscope::dbgeng::{DebugEngine, Flow, Instruction, InstructionSet, Operand};
use std::collections::{BTreeMap, HashMap, HashSet};
const WINDOW: usize = 0x1000;
fn main() {
let mut args = std::env::args().skip(1);
let (Some(dump), Some(module)) = (args.next(), args.next()) else {
eprintln!("usage: decode_against_rendering <dump> <module> [max] [image search path]");
std::process::exit(2);
};
let max: usize = args
.next()
.map(|text| text.parse().expect("max must be a number"))
.unwrap_or(0);
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:?} (flow read: {}, operands read: {})",
set.flow_is_read(),
set.operands_are_read()
);
let loaded = e.modules().expect("the module list");
let Some(image) = loaded
.iter()
.find(|candidate| candidate.name.eq_ignore_ascii_case(&module))
else {
eprintln!("no module named `{module}` is loaded");
std::process::exit(1);
};
println!("{} at {:#x}, {} bytes", image.name, image.base, image.size);
let headers = dbgscope::pe::read_image(image.base, |at, len| e.read_memory(at, len).ok())
.expect("the image headers did not read — a driver needs an image search path");
let ranges = headers.executable_ranges();
println!(
"executable sections: {}",
ranges
.iter()
.map(|range| format!("{:#x}..{:#x}", range.start, range.end))
.collect::<Vec<_>>()
.join(" ")
);
let mut report = Report::default();
'sections: for range in &ranges {
let mut at = range.start;
while at < range.end {
let length = WINDOW.min((range.end - at) as usize);
report.window(&e, at, length, set);
at += length as u64;
if max != 0 && report.rendered >= max as u64 {
break 'sections;
}
}
}
report.print();
}
#[derive(Default)]
struct Report {
read: u64,
rendered: u64,
unreadable: u64,
unknown_flow: u64,
unshaped: u64,
shapeable: u64,
vocabulary: HashSet<String>,
other: Counted,
spaces: Counted,
spellings: Counted,
mnemonics: Counted,
registers: Counted,
immediates: Counted,
displacements: Counted,
addresses: Counted,
counts: Counted,
paths: Counted,
}
#[derive(Default)]
struct Counted {
total: u64,
checked: u64,
by_shape: BTreeMap<String, (u64, String)>,
}
impl Counted {
fn hit(&mut self, shape: String, sample: impl FnOnce() -> String) {
self.total += 1;
self.by_shape
.entry(shape)
.or_insert_with(|| (0, sample()))
.0 += 1;
}
fn print(&self, title: &str, limit: usize) {
match self.checked {
0 => println!("{title}: {}", self.total),
checked => println!("{title}: {} of {checked}", self.total),
}
let mut shapes: Vec<_> = self.by_shape.iter().collect();
shapes.sort_by_key(|(_, (count, _))| std::cmp::Reverse(*count));
for (shape, (count, sample)) in shapes.iter().take(limit) {
println!(" {count:>9} {shape:<28} {sample}");
}
if shapes.len() > limit {
println!(" {:>9} … {} more shapes", "", shapes.len() - limit);
}
}
}
impl Report {
fn window(&mut self, e: &DebugEngine, start: u64, length: usize, set: InstructionSet) {
let ranged: HashMap<u64, Instruction> = match e.decode_range(start, length) {
Ok(decoded) => decoded.into_iter().map(|one| (one.address, one)).collect(),
Err(_) => HashMap::new(),
};
let end = start + length as u64;
let mut at = start;
while at < end {
let Ok(batch) = e.disassemble(at, 65) else {
return;
};
let Some(next) = batch.get(64).map(|one| one.address) else {
for one in batch.iter().filter(|one| one.address < end) {
self.instruction(one, ranged.get(&one.address), set);
}
return;
};
for one in batch.iter().take(64).filter(|one| one.address < end) {
self.instruction(one, ranged.get(&one.address), set);
}
if next <= at {
return;
}
at = next;
}
}
fn instruction(
&mut self,
one: &Instruction,
ranged: Option<&Instruction>,
set: InstructionSet,
) {
self.read += 1;
if one.flow == Flow::Unreadable {
self.unreadable += 1;
return;
}
self.rendered += 1;
if one.flow == Flow::Unknown {
self.unknown_flow += 1;
}
for operand in &one.operands {
if let Operand::Register(register) = operand {
self.vocabulary.insert(register.name.clone());
}
}
let sample = || format!("{} {}", one.bytes, one.text);
let text = one.text.as_str();
let printed = tokens(text);
let numbers = numbers(text);
if let Some(other) = ranged {
self.paths.checked += 1;
let mnemonics_differ = !one.mnemonic.is_empty()
&& !other.mnemonic.is_empty()
&& one.mnemonic != other.mnemonic;
if one.bytes != other.bytes || mnemonics_differ || one.flow != other.flow {
self.paths
.hit(format!("{} / {}", one.mnemonic, other.mnemonic), || {
format!(
"{} walked {:?} ranged {:?}",
sample(),
one.flow,
other.flow
)
});
}
}
let shaped = !matches!(one.operands.first(), Some(Operand::Undecoded(_)));
if !text.starts_with('?') {
self.shapeable += 1;
self.unshaped += u64::from(!shaped);
}
if set.operands_are_read() && !text.starts_with('?') {
let engine_mnemonic = text.split(' ').next().unwrap_or_default();
self.mnemonics.checked += 1;
let condition_spelled_in =
one.condition.is_some() && engine_mnemonic.starts_with(&one.mnemonic);
if !one.mnemonic.is_empty()
&& engine_mnemonic != one.mnemonic
&& !condition_spelled_in
&& engine_mnemonic != one.mnemonic.replace('.', "")
{
self.mnemonics
.hit(format!("{} / {engine_mnemonic}", one.mnemonic), sample);
}
for operand in &one.operands {
match operand {
Operand::Other(name) => self.other.hit(name.clone(), sample),
Operand::Undecoded(space) => self.spaces.hit(space.clone(), sample),
Operand::Register(register) => {
self.spellings.checked += 1;
if !printed.contains(®ister.name) {
self.spellings
.hit(format!("{}/{}", one.mnemonic, register.name), sample);
}
}
Operand::Immediate(value) => {
self.immediates.checked += 1;
if !numbers.contains(value) {
self.immediates.hit(one.mnemonic.clone(), sample);
}
}
Operand::Memory(memory) => {
if memory.displacement != 0
&& !text.contains("],#")
&& !numbers.contains(&(memory.displacement as u64))
{
self.displacements.hit(one.mnemonic.clone(), sample);
}
self.displacements.checked += 1;
if let Some(address) = memory.address {
self.addresses.checked += 1;
if !text.replace('`', "").contains(&format!("{address:x}")) {
self.addresses.hit(one.mnemonic.clone(), sample);
}
}
}
Operand::Target(_) => {}
}
}
let seen: HashSet<&str> = one
.operands
.iter()
.flat_map(|operand| match operand {
Operand::Register(register) => vec![register],
Operand::Memory(memory) => memory
.base
.iter()
.chain(memory.index.iter())
.collect::<Vec<_>>(),
_ => Vec::new(),
})
.chain(one.reads.iter())
.chain(one.writes.iter())
.flat_map(|register| [register.name.as_str(), register.full.as_str()])
.collect();
if shaped {
for token in &printed {
if self.vocabulary.contains(token) && !seen.contains(token.as_str()) {
self.registers
.hit(format!("{} unseen {token}", one.mnemonic), sample);
}
}
}
if shaped && !text.contains('!') {
self.counts.checked += 1;
let groups = operand_groups(text);
if groups != one.operands.len() {
self.counts.hit(
format!("{} {}/{groups}", one.mnemonic, one.operands.len()),
sample,
);
}
}
}
}
fn print(&self) {
println!(
"\n--- {} instructions read, {} rendered, {} unreadable ---",
self.read, self.rendered, self.unreadable
);
println!(
"instructions with Unknown flow: {}",
self.unknown_flow
);
println!(
"unshaped, of those the engine rendered: {} of {} ({:.3}%)",
self.unshaped,
self.shapeable,
100.0 * self.unshaped as f64 / self.shapeable.max(1) as f64
);
println!(
"register spellings the decoder produced: {}",
self.vocabulary.len()
);
println!();
self.paths.print("the two decode paths disagreeing", 10);
self.mnemonics
.print("mnemonics differing from the rendering", 15);
self.spellings
.print("operand spellings differing from the rendering", 10);
self.registers.print(
"registers the rendering names that the decode did not see",
15,
);
self.addresses
.print("resolved addresses not in the rendering", 10);
self.immediates.print("immediates not in the rendering", 10);
self.displacements
.print("memory displacements not in the rendering", 10);
self.counts
.print("operand counts differing from the rendering", 15);
self.other.print("operands kept as Other", 20);
self.spaces.print("instructions left unread, by space", 10);
}
}
fn tokens(text: &str) -> HashSet<String> {
let mut out = HashSet::new();
let mut current = String::new();
for character in text.chars() {
if character.is_ascii_alphanumeric() || character == '_' {
current.push(character);
} else if !current.is_empty() {
out.insert(std::mem::take(&mut current));
}
}
if !current.is_empty() {
out.insert(current);
}
out
}
fn numbers(text: &str) -> HashSet<u64> {
let mut out = HashSet::new();
let characters: Vec<char> = text.chars().collect();
let mut index = 0;
while index < characters.len() {
let negative = characters[index] == '-';
let start = index + usize::from(negative);
if start >= characters.len() {
break;
}
if start + 1 < characters.len()
&& characters[start] == '0'
&& characters[start + 1].eq_ignore_ascii_case(&'x')
{
let mut end = start + 2;
while end < characters.len() && characters[end].is_ascii_hexdigit() {
end += 1;
}
if let Ok(value) =
u64::from_str_radix(&characters[start + 2..end].iter().collect::<String>(), 16)
{
offer(&mut out, value, negative);
}
index = end;
continue;
}
if characters[start].is_ascii_digit() {
let mut end = start;
while end < characters.len() && characters[end].is_ascii_digit() {
end += 1;
}
let introduced = start > 0 && characters[start - 1] == '#'
|| negative && start > 1 && characters[start - 2] == '#';
if introduced
&& let Ok(value) = characters[start..end]
.iter()
.collect::<String>()
.parse::<u64>()
{
offer(&mut out, value, negative);
}
index = end;
continue;
}
index += 1;
}
out
}
fn offer(out: &mut HashSet<u64>, value: u64, negative: bool) {
out.insert(value);
out.insert(!value);
if negative {
out.insert((value as i64).wrapping_neg() as u64);
out.insert((value as u32).wrapping_neg() as u64);
}
}
fn operand_groups(text: &str) -> usize {
let Some((_, rest)) = text.split_once(' ') else {
return 0;
};
let rest = rest.split(" (").next().unwrap_or(rest).trim();
if rest.is_empty() {
return 0;
}
let mut depth = 0i32;
let mut groups = 1;
for character in rest.chars() {
match character {
'[' | '{' => depth += 1,
']' | '}' => depth -= 1,
',' if depth == 0 => groups += 1,
_ => {}
}
}
groups
}