use std::collections::BTreeMap;
use std::time::Duration;
use dbgscope::dbgeng::{DebugEngine, VirtualState};
use dbgscope::heap::{self, HeapState, HeapWalk};
const BUDGET: Duration = Duration::from_secs(120);
fn open(target: &str) -> Result<DebugEngine, Box<dyn std::error::Error>> {
let engine = DebugEngine::new();
engine.set_symbol_path(&std::env::var("_NT_SYMBOL_PATH").unwrap_or_else(|_| {
"srv*C:\\ProgramData\\dbg\\sym*https://msdl.microsoft.com/download/symbols".into()
}))?;
match target.parse::<u32>() {
Ok(pid) => engine.attach_process(pid)?,
Err(_) => {
engine.open_dump(target)?;
engine.wait_for_event(60_000)?;
}
}
engine.execute_command(".reload /f ntdll.dll")?;
Ok(engine)
}
fn describe(state: VirtualState) -> String {
match state {
VirtualState::Committed => "committed".to_string(),
VirtualState::Reserved => "reserved".to_string(),
VirtualState::Free => "free".to_string(),
VirtualState::Unknown(state) => format!("unknown state {state:#x}"),
}
}
fn coverage(target: &str) -> Result<(), Box<dyn std::error::Error>> {
let engine = open(target)?;
let answer = heap::allocations(&engine, HeapWalk::refreshed().within(BUDGET))?;
println!(
"coverage {:?}: {} chunks, {} unreadable gaps, {} uncommitted gaps",
answer.walk.coverage,
answer.found.len(),
answer.walk.unreadable_gaps,
answer.walk.uncommitted_gaps
);
println!(
" refused {} headers, {:#x} unplaced bytes, {:?}",
answer.walk.refused_headers, answer.walk.unplaced_bytes, answer.walk.stalls
);
let mut tally: BTreeMap<String, (usize, u64)> = BTreeMap::new();
let mut note = |label: String, bytes: u64| {
let row = tally.entry(label).or_insert((0, 0));
row.0 += 1;
row.1 += bytes;
};
for gap in answer.found.iter().filter(|gap| !gap.state.is_chunk()) {
let mut cursor = gap.header_address;
let end = gap.end();
while cursor < end {
match engine.virtual_region(cursor) {
Ok(region) if region.contains(cursor) => {
let stop = region.end().unwrap_or(end).min(end).max(cursor + 1);
note(
format!("{:?} / {}", gap.state, describe(region.state)),
stop - cursor,
);
cursor = stop;
}
Ok(region) => {
note(
format!(
"{:?} / answered about {:#x}+{:#x}",
gap.state, region.base, region.size
),
end - cursor,
);
break;
}
Err(why) => {
note(format!("{:?} / no answer: {why}", gap.state), end - cursor);
break;
}
}
}
}
for (label, (runs, bytes)) in &tally {
println!(" {label}: {runs} runs, {bytes:#x} bytes");
}
for state in [HeapState::Allocated, HeapState::ReusableFree] {
if let Some(chunk) = answer.found.iter().find(|chunk| chunk.state == state) {
println!(
" control {state:?} {:#x}: {}",
chunk.user_address,
engine.virtual_region(chunk.user_address).map_or_else(
|why| format!("no answer: {why}"),
|region| describe(region.state)
)
);
}
}
let diagnostics = heap::diagnostics(&engine, HeapWalk::cached())?;
for shape in &diagnostics.found.categories {
println!(" {} x {}", shape.total, shape.shape);
}
engine.end_session()?;
Ok(())
}
fn one_address(target: &str, address: u64) -> Result<(), Box<dyn std::error::Error>> {
let engine = open(target)?;
println!("{address:#x}: {:?}", engine.virtual_region(address));
println!(
" read: {:?}",
engine.read_memory(address, 8).map(|bytes| bytes.len())
);
engine.end_session()?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arguments: Vec<String> = std::env::args().skip(1).collect();
match arguments.as_slice() {
[target] => coverage(target),
[target, address] => one_address(
target,
u64::from_str_radix(address.trim_start_matches("0x"), 16)?,
),
_ => Err("usage: heap_coverage <pid|dump> [address]".into()),
}
}