use std::path::PathBuf;
use std::time::Instant;
use dbgscope::dbgeng::{
BreakpointAt, BreakpointSpec, DataAccess, DataWatch, DbgEngError, DebugEngine,
};
const TARGET: &str = "cmd.exe /c ping -n 60 127.0.0.1";
const COLD_SYMBOL: &str = "KERNELBASE!CreateFileW";
fn main() {
let mut args = std::env::args().skip(1);
let arm = args.next().unwrap_or_else(|| "all".into());
let mut symbol = None;
let mut store = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--store" => store = args.next().map(PathBuf::from),
other => symbol = Some(other.to_string()),
}
}
let symbol = symbol.unwrap_or_else(|| COLD_SYMBOL.to_string());
let run = |name: &str, f: &dyn Fn(&DebugEngine, &str)| {
println!("\n======== {name} ========");
let store = store.clone().unwrap_or_else(|| {
std::env::temp_dir().join(format!("dbgscope-bp-probe-{name}-{}", std::process::id()))
});
let _ = std::fs::remove_dir_all(&store);
if let Err(e) = std::fs::create_dir_all(&store) {
println!("could not make a symbol store at {}: {e}", store.display());
return;
}
let engine = DebugEngine::new();
let path = format!(
"srv*{}*https://msdl.microsoft.com/download/symbols",
store.display()
);
if let Err(e) = engine.set_symbol_path(&path) {
println!("could not set the symbol path: {e}");
return;
}
match engine.launch_process(TARGET) {
Ok(()) => f(&engine, &symbol),
Err(e) => println!("could not launch {TARGET}: {e}"),
}
};
match arm.as_str() {
"resolve" => run("resolve", &resolve),
"resolve-bounded" => run("resolve-bounded", &resolve_bounded),
"dedup" => run("dedup", &dedup),
"duplicate-cost" => run("duplicate-cost", &duplicate_cost),
"command" => run("command", &command),
"data" => run("data", &data),
"all" => {
run("resolve", &resolve);
run("resolve-bounded", &resolve_bounded);
run("dedup", &dedup);
run("duplicate-cost", &duplicate_cost);
run("command", &command);
run("data", &data);
}
other => {
println!("unknown arm {other}");
println!("arms: resolve, resolve-bounded, dedup, duplicate-cost, command, data, all");
}
}
}
fn held(engine: &DebugEngine, tag: &str) {
match engine.breakpoints() {
Ok(list) if list.is_empty() => println!(" [{tag}] no breakpoints"),
Ok(list) => {
for bp in list {
println!(
" [{tag}] id={} addr={} expr={:?} enabled={} deferred={} cmd={:?}",
bp.id,
bp.address
.map_or_else(|| "(unresolved)".into(), |a| format!("{a:#x}")),
bp.expression,
bp.enabled,
bp.deferred,
bp.command,
);
}
}
Err(e) => println!(" [{tag}] breakpoints() failed: {e}"),
}
}
fn resolve(engine: &DebugEngine, symbol: &str) {
let at = |what: &str, at: BreakpointAt| {
let started = Instant::now();
let result = engine.set_breakpoint(&BreakpointSpec::code(at));
let ms = started.elapsed().as_millis();
match result {
Ok(set) => println!(
" {what}: {ms} ms -> id={} addr={} deferred={} expr={:?}",
set.breakpoint.id,
set.breakpoint
.address
.map_or_else(|| "(unresolved)".into(), |a| format!("{a:#x}")),
set.breakpoint.deferred,
set.breakpoint.expression,
),
Err(e) => println!(" {what}: {ms} ms -> {e}"),
}
};
at(
"a symbol needing a cold PDB",
BreakpointAt::Expression(symbol.into()),
);
at("an address", BreakpointAt::Address(0x1000));
at(
"a symbol whose module is absent",
BreakpointAt::Expression("nosuchmod!NoSuchSymbol".into()),
);
at(
"the same symbol, warm",
BreakpointAt::Expression(symbol.into()),
);
held(engine, "held");
}
fn resolve_bounded(engine: &DebugEngine, symbol: &str) {
let started = Instant::now();
let result = engine.set_breakpoint_bounded(
&BreakpointSpec::code(BreakpointAt::Expression(symbol.into())),
250,
);
let ms = started.elapsed().as_millis();
match result {
Ok(set) => {
println!(
" bounded at 250 ms: returned after {ms} ms, cut_short={:?}",
set.cut_short
);
println!(
" the breakpoint exists either way: id={} addr={} deferred={}",
set.breakpoint.id,
set.breakpoint
.address
.map_or_else(|| "(unresolved)".into(), |a| format!("{a:#x}")),
set.breakpoint.deferred,
);
if set.cut_short.is_none() {
println!(
" NOTE: the resolve finished inside the bound, so this arm measured nothing. \
Empty the store, or lower the bound."
);
}
}
Err(e) => println!(" bounded at 250 ms: {ms} ms -> {e}"),
}
let started = Instant::now();
match engine.set_breakpoint(&BreakpointSpec::code(BreakpointAt::Address(0x1000))) {
Ok(set) => println!(
" a following set still works: id={} in {} ms",
set.breakpoint.id,
started.elapsed().as_millis()
),
Err(e) => println!(" a following set failed: {e}"),
}
match engine.execute_command("lm m KERNELBASE") {
Ok(out) => println!(" the module's symbol state after the break:\n{out}"),
Err(e) => println!(" lm failed: {e}"),
}
}
fn dedup(engine: &DebugEngine, _symbol: &str) {
let ntdll = "ntdll!NtCreateFile";
let spec = || BreakpointSpec::code(BreakpointAt::Expression(ntdll.into()));
println!("-- three typed sets, OnExisting::Add (the engine's own behaviour)");
for _ in 0..3 {
if let Err(e) = engine.set_breakpoint(&spec()) {
println!(" set failed: {e}");
}
}
held(engine, "after three adds");
println!("-- one typed set with OnExisting::Replace");
match engine.set_breakpoint(&spec().replacing_existing()) {
Ok(set) => println!(" id={} replaced={:?}", set.breakpoint.id, set.replaced),
Err(e) => println!(" set failed: {e}"),
}
held(engine, "after the replace");
println!("-- `bp` over the top of it");
match engine.execute_command(&format!("bp {ntdll}")) {
Ok(out) => println!(" bp said: {out:?}"),
Err(e) => println!(" bp failed: {e}"),
}
held(engine, "after bp");
println!("-- three deferred sets, OnExisting::Replace");
for _ in 0..3 {
match engine.set_breakpoint(
&BreakpointSpec::code(BreakpointAt::Expression("nosuchmod!Sym".into()))
.replacing_existing(),
) {
Ok(set) => println!(
" id={} deferred={} replaced={:?}",
set.breakpoint.id, set.breakpoint.deferred, set.replaced
),
Err(e) => println!(" set failed: {e}"),
}
}
held(engine, "after three deferred");
}
fn duplicate_cost(engine: &DebugEngine, _symbol: &str) {
let ntdll = "ntdll!NtCreateFile";
let mut ids = Vec::new();
for n in 0..2 {
match engine.set_breakpoint(
&BreakpointSpec::code(BreakpointAt::Expression(ntdll.into()))
.with_command(format!(".echo breakpoint {n} ran its command")),
) {
Ok(set) => ids.push(set.breakpoint.id),
Err(e) => println!(" set failed: {e}"),
}
}
println!(" armed two breakpoints at one address: {ids:?}");
held(engine, "armed");
match engine.execute_and_wait("g", 20_000) {
Ok(run) => {
println!(" the stop reported:");
for line in run
.output
.lines()
.filter(|l| l.contains("Breakpoint") || l.contains("ran its command"))
{
println!(" {line}");
}
println!(
" cut_short={:?} target_gone={}",
run.cut_short, run.target_gone
);
}
Err(e) => println!(" g failed: {e}"),
}
match engine.instruction_pointer() {
Ok(pc) => println!(" stopped once, at {pc:#x}"),
Err(e) => println!(" could not read the instruction pointer: {e}"),
}
if let Some(first) = ids.first() {
match engine.remove_breakpoint(*first) {
Ok(()) => println!(" removed breakpoint {first}"),
Err(e) => println!(" removing {first} failed: {e}"),
}
held(engine, "after removing one of the two");
}
}
fn command(engine: &DebugEngine, _symbol: &str) {
let wanted = r#".printf "IOCTL %08x\n", dwo(@rdx+0x18); gc"#;
match engine.set_breakpoint(
&BreakpointSpec::code(BreakpointAt::Expression(
"ntdll!NtDeviceIoControlFile".into(),
))
.with_command(wanted),
) {
Ok(set) => {
let got = set.breakpoint.command.as_deref();
println!(" asked for: {wanted}");
println!(" engine has: {}", got.unwrap_or("(nothing)"));
println!(
" identical: {}",
if got == Some(wanted) {
"yes — nothing was escaped, quoted or split"
} else {
"NO — the command did not survive"
}
);
}
Err(e) => println!(" could not set a command breakpoint: {e}"),
}
}
fn data(engine: &DebugEngine, _symbol: &str) {
let watch = DataWatch {
access: DataAccess::Write,
size: 8,
};
let Ok(address) = engine.instruction_pointer().map(|pc| pc & !7) else {
println!(" could not read the instruction pointer; skipping the live half");
return;
};
match engine.set_breakpoint(&BreakpointSpec::data(BreakpointAt::Address(address), watch)) {
Ok(set) => {
println!(
" 8-byte write watch at {address:#x}: id={} kind={:?} data={:?}",
set.breakpoint.id, set.breakpoint.kind, set.breakpoint.data
);
println!(
" the engine reports the same watch: {}",
if set.breakpoint.data == Some(watch) {
"yes"
} else {
"NO — the engine holds something else"
}
);
}
Err(e) => println!(" could not set a data breakpoint: {e}"),
}
held(engine, "held");
for (why, spec) in [
(
"a 3-byte watch",
BreakpointSpec::data(
BreakpointAt::Address(address),
DataWatch {
access: DataAccess::ReadWrite,
size: 3,
},
),
),
(
"an unaligned 8-byte watch",
BreakpointSpec::data(BreakpointAt::Address(address | 1), watch),
),
(
"an unaligned 8-byte watch reached through an expression",
BreakpointSpec::data(
BreakpointAt::Expression("ntdll!NtCreateFile+1".into()),
watch,
),
),
] {
match engine.set_breakpoint(&spec) {
Ok(_) => println!(" {why} was ACCEPTED — the validation has regressed"),
Err(DbgEngError::InvalidBreakpoint(why_not)) => {
println!(" {why} refused before the engine saw it: {why_not}")
}
Err(e) => println!(" {why} failed for another reason: {e}"),
}
}
}