use std::cell::{Cell, RefCell};
use std::collections::BTreeSet;
use std::rc::Rc;
use std::time::{Duration, Instant};
use dbgscope::dbgeng::{
BreakpointAction, BreakpointAt, BreakpointCallback, BreakpointHit, BreakpointSpec, DebugEngine,
};
const TARGET: &str = "cmd.exe /c ping -n 600 127.0.0.1";
const USER_LOCATION: &str = "ntdll!RtlAllocateHeap";
const KERNEL_LOCATION: &str = "nt!ExAllocatePool2";
#[derive(Default)]
struct EngineReads {
clean: u32,
failed: u32,
first_failure: Option<String>,
ip: Duration,
thread: Duration,
threads: BTreeSet<u64>,
arguments: Duration,
memory: Duration,
stack: Duration,
full_bank: Option<(Duration, usize)>,
}
fn argument_registers(engine: &DebugEngine) -> [&'static str; 4] {
match engine.processor_type() {
Ok(0xaa64) => ["x0", "x1", "x2", "sp"],
_ => ["rcx", "rdx", "r8", "rsp"],
}
}
fn main() {
let mut args = std::env::args().skip(1);
let arm = args.next().unwrap_or_default();
let mut positional = Vec::new();
let mut hits = 200u32;
let mut seconds = 30u32;
while let Some(arg) = args.next() {
match arg.as_str() {
"--hits" => hits = args.next().and_then(|n| n.parse().ok()).unwrap_or(hits),
"--seconds" => seconds = args.next().and_then(|n| n.parse().ok()).unwrap_or(seconds),
other => positional.push(other.to_string()),
}
}
let hits = hits.max(2);
let phases = |engine: &DebugEngine, location: &str, phase: Phase| match phase {
Phase::DefaultIsNone => default_is_no_callback(engine, location, seconds),
Phase::Through(way) => let_hits_through(engine, location, hits, seconds, way),
Phase::CommandBeside => command_beside_callback(engine, location, seconds),
};
match arm.as_str() {
"user" => {
let location = positional
.first()
.cloned()
.unwrap_or_else(|| USER_LOCATION.into());
println!("location {location}, {hits} hits, {seconds}s cap, a fresh target per phase");
for phase in Phase::ALL {
let engine = DebugEngine::new();
if let Err(e) = engine.launch_process(TARGET) {
println!("could not launch {TARGET}: {e}");
return;
}
phases(&engine, &location, phase);
if let Err(e) = engine.end_session() {
println!(" end_session failed: {e}");
}
}
}
"kernel" => {
let Some(connection) = positional.first() else {
println!("kernel needs a connection string");
return;
};
let location = positional
.get(1)
.cloned()
.unwrap_or_else(|| KERNEL_LOCATION.into());
let engine = DebugEngine::new();
if let Err(e) = engine.attach_kernel(connection) {
println!("could not attach to {connection}: {e}");
return;
}
println!("location {location}, {hits} hits, {seconds}s cap");
for phase in Phase::ALL {
phases(&engine, &location, phase);
}
match engine.end_session() {
Ok(left) => println!("\nended: {left:?}"),
Err(e) => println!("\nend_session failed: {e}"),
}
}
_ => println!("arms: user [location], kernel <connection> [location]"),
}
}
#[derive(Clone, Copy)]
enum Phase {
DefaultIsNone,
Through(Way),
CommandBeside,
}
impl Phase {
const ALL: [Self; 6] = [
Self::DefaultIsNone,
Self::Through(Way::PassCount),
Self::Through(Way::Go),
Self::Through(Way::GoReadingEngine),
Self::Through(Way::CommandText),
Self::CommandBeside,
];
}
fn default_is_no_callback(engine: &DebugEngine, location: &str, seconds: u32) {
println!("\n======== 1. Default is what no callback is ========");
let seen = Rc::new(Cell::new(0u32));
let counter = Rc::clone(&seen);
let callback: BreakpointCallback = Box::new(move |_: &BreakpointHit<'_>| {
counter.set(counter.get() + 1);
BreakpointAction::Default
});
let Some(armed) = Armed::new(engine, location, None, None, Some(callback)) else {
return;
};
let ran = run(engine, seconds);
println!(
" hits seen {} (want 1), stopped at {} (breakpoint {:#x}), {:.1} ms",
seen.get(),
ran.stopped_at
.map_or_else(|| "?".into(), |a| format!("{a:#x}")),
armed.address,
ran.elapsed
);
}
#[derive(Clone, Copy, Debug)]
enum Way {
PassCount,
Go,
GoReadingEngine,
CommandText,
}
fn let_hits_through(engine: &DebugEngine, location: &str, hits: u32, seconds: u32, way: Way) {
println!("\n======== {way:?} ========");
let seen = Rc::new(Cell::new(0u32));
let reads = Rc::new(RefCell::new(EngineReads::default()));
let (pass_count, command, callback): (Option<u32>, Option<String>, Option<BreakpointCallback>) =
match way {
Way::PassCount => (Some(hits), None, None),
Way::CommandText => (
None,
Some(format!(
"r $t19 = @$t19 + 1; .if (@$t19 < 0n{hits}) {{ gc }} .else {{ .echo stopped }}"
)),
None,
),
Way::Go | Way::GoReadingEngine => {
let counter = Rc::clone(&seen);
let reads = Rc::clone(&reads);
let read_engine = matches!(way, Way::GoReadingEngine);
let arguments = argument_registers(engine);
(
None,
None,
Some(Box::new(move |hit: &BreakpointHit<'_>| {
let n = counter.get() + 1;
counter.set(n);
if read_engine {
let result =
read_through(hit, &arguments, &mut reads.borrow_mut(), n == 1);
match result {
Ok(()) => reads.borrow_mut().clean += 1,
Err(why) => {
let mut reads = reads.borrow_mut();
reads.failed += 1;
reads.first_failure.get_or_insert(why);
}
}
}
if n < hits {
BreakpointAction::Go
} else {
BreakpointAction::Break
}
})),
)
}
};
if matches!(way, Way::CommandText) {
let _ = engine.execute_command("r $t19 = 0");
}
let Some(armed) = Armed::new(engine, location, pass_count, command, callback) else {
return;
};
let ran = run(engine, seconds);
let elapsed = ran.elapsed;
let counted = match way {
Way::Go | Way::GoReadingEngine => Some(seen.get()),
Way::CommandText => {
let read = pseudo_register(engine, "$t19");
if read.is_none() {
println!(" could not read $t19, so the hits are unknown");
}
read
}
Way::PassCount => None,
};
let counted = counted.or(ran.finished_at(armed.address).then_some(hits));
let shown = counted.map_or_else(|| "?".into(), |n| n.to_string());
match counted {
Some(n) if n > 0 && ran.finished_at(armed.address) => println!(
" {n} hits in {elapsed:.1} ms = {:.3} ms/hit; stopped at the breakpoint on the last",
elapsed / f64::from(n)
),
_ => println!(
" INCOMPLETE: {shown} hits in {elapsed:.1} ms without stopping at the breakpoint, so \
no per-hit figure"
),
}
if matches!(way, Way::GoReadingEngine) {
let reads = reads.borrow();
println!(
" engine reads inside the callback: {} clean, {} failed{}",
reads.clean,
reads.failed,
reads
.first_failure
.as_ref()
.map_or_else(String::new, |why| format!(" (first: {why})"))
);
let per_hit = |d: Duration| d.as_secs_f64() * 1000.0 / f64::from(reads.clean.max(1));
println!(
" per clean hit: ip {:.3} ms, thread {:.3} ms, three argument registers and sp \
{:.3} ms, memory at ip {:.3} ms, 8 bytes at sp {:.3} ms",
per_hit(reads.ip),
per_hit(reads.thread),
per_hit(reads.arguments),
per_hit(reads.memory),
per_hit(reads.stack)
);
if let Some((took, count)) = reads.full_bank {
println!(
" one register_values() inside the callback: {count} registers in {:.1} ms",
took.as_secs_f64() * 1000.0
);
}
println!(
" distinct threads hit: {}; at the stop current_thread_data_offset {} against \
@$thread {}",
reads.threads.len(),
engine
.current_thread_data_offset()
.map_or_else(|e| e.to_string(), |at| format!("{at:#x}")),
pseudo_register_u64(engine, "$thread")
.map_or_else(|| "?".into(), |at| format!("{at:#x}"))
);
}
}
fn command_beside_callback(engine: &DebugEngine, location: &str, seconds: u32) {
println!("\n======== a command beside a callback that answers Go ========");
let seen = Rc::new(Cell::new(0u32));
let counter = Rc::clone(&seen);
let callback: BreakpointCallback = Box::new(move |_: &BreakpointHit<'_>| {
let n = counter.get() + 1;
counter.set(n);
if n < 3 {
BreakpointAction::Go
} else {
BreakpointAction::Break
}
});
let Some(_armed) = Armed::new(
engine,
location,
None,
Some(".echo PROBE-COMMAND-RAN".into()),
Some(callback),
) else {
return;
};
let started = Instant::now();
match engine.execute_and_wait("g", seconds * 1000) {
Ok(run) => println!(
" callback saw {} hits; the command ran {} times; {:.1} ms; cut_short={:?}",
seen.get(),
run.output.matches("PROBE-COMMAND-RAN").count(),
started.elapsed().as_secs_f64() * 1000.0,
run.cut_short
),
Err(e) => println!(" g failed: {e}"),
}
}
fn read_through(
hit: &BreakpointHit<'_>,
registers: &[&str],
reads: &mut EngineReads,
full_bank: bool,
) -> Result<(), String> {
let engine = hit.engine();
let started = Instant::now();
let offset = hit.address().map_err(|e| e.to_string())?;
let ip = engine
.instruction_pointer()
.map_err(|e| format!("ip: {e}"))?;
if ip != offset {
return Err(format!("ip {ip:#x} is not the breakpoint's {offset:#x}"));
}
let ip_read = Instant::now();
let thread = engine
.current_thread_data_offset()
.map_err(|e| format!("thread: {e}"))?;
let thread_read = Instant::now();
let mut stack_pointer = 0;
for name in registers {
stack_pointer = engine.integer_register(name).map_err(|e| e.to_string())?;
}
let arguments_read = Instant::now();
engine
.read_memory(ip, 4)
.map_err(|e| format!("memory at ip: {e}"))?;
let memory_read = Instant::now();
engine
.read_memory(stack_pointer, 8)
.map_err(|e| format!("memory at sp {stack_pointer:#x}: {e}"))?;
let stack_read = Instant::now();
reads.ip += ip_read - started;
reads.thread += thread_read - ip_read;
reads.threads.insert(thread);
reads.arguments += arguments_read - thread_read;
reads.memory += memory_read - arguments_read;
reads.stack += stack_read - memory_read;
if full_bank {
let bank_started = Instant::now();
let count = engine
.register_values()
.map_err(|e| format!("register_values: {e}"))?
.len();
reads.full_bank = Some((bank_started.elapsed(), count));
}
Ok(())
}
fn pseudo_register_u64(engine: &DebugEngine, name: &str) -> Option<u64> {
let text = engine.execute_command(&format!("? @{name}")).ok()?;
let hex = text.split('=').nth(1)?.trim();
u64::from_str_radix(&hex.replace('`', ""), 16).ok()
}
fn pseudo_register(engine: &DebugEngine, name: &str) -> Option<u32> {
let text = engine.execute_command(&format!("? @{name}")).ok()?;
let value = text
.split("Evaluate expression:")
.nth(1)?
.split('=')
.next()?;
value.trim().parse().ok()
}
fn run(engine: &DebugEngine, seconds: u32) -> Ran {
let started = Instant::now();
let on_its_own = match engine.execute_and_wait("g", seconds * 1000) {
Ok(run) => {
if run.cut_short.is_some() {
println!(
" the wait was cut short ({:?}): the cap, not a stop",
run.cut_short
);
}
if run.target_gone {
println!(" the target is gone");
}
run.cut_short.is_none() && !run.target_gone
}
Err(e) => {
println!(" g failed: {e}");
false
}
};
Ran {
elapsed: started.elapsed().as_secs_f64() * 1000.0,
stopped_at: engine.instruction_pointer().ok(),
on_its_own,
}
}
struct Ran {
elapsed: f64,
stopped_at: Option<u64>,
on_its_own: bool,
}
impl Ran {
fn finished_at(&self, address: u64) -> bool {
self.on_its_own && self.stopped_at == Some(address)
}
}
struct Armed<'a> {
engine: &'a DebugEngine,
id: u32,
address: u64,
}
impl<'a> Armed<'a> {
fn new(
engine: &'a DebugEngine,
location: &str,
pass_count: Option<u32>,
command: Option<String>,
callback: Option<BreakpointCallback>,
) -> Option<Self> {
let mut spec = BreakpointSpec::code(BreakpointAt::Expression(location.into()));
if let Some(command) = command {
spec = spec.with_command(command);
}
if let Some(passes) = pass_count {
spec = spec.with_pass_count(passes);
}
let set = match engine.set_breakpoint(&spec) {
Ok(set) => set,
Err(e) => {
println!(" could not set a breakpoint at {location}: {e}");
return None;
}
};
let Some(address) = set.breakpoint.address else {
println!(" {location} did not resolve");
let _ = engine.remove_breakpoint(set.breakpoint.id);
return None;
};
let armed = Self {
engine,
id: set.breakpoint.id,
address,
};
if let Some(callback) = callback {
if let Err(e) = engine.set_breakpoint_callback(callback) {
println!(" could not register the callbacks: {e}");
return None;
}
}
Some(armed)
}
}
impl Drop for Armed<'_> {
fn drop(&mut self) {
if let Err(e) = self.engine.clear_breakpoint_callback() {
println!(" could not unregister the callbacks: {e}");
}
if let Err(e) = self.engine.remove_breakpoint(self.id) {
println!(" could not remove breakpoint {}: {e}", self.id);
}
}
}