1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Scratch experiment (not part of the public API): check the typed context readers —
//! `register_values`, `modules` and `breakpoints` — against the text the debugger prints for
//! the same state.
//!
//! Each of the three replaces a parse a host would otherwise have to do on `r`, `lm` or `bl`,
//! and the failure mode being checked for is the quiet one: a union arm read by the wrong tag,
//! a name buffer truncated, a deferred breakpoint reported as an address. So every section
//! prints the engine's own rendering next to the values, and the two are compared by eye.
//!
//! Run against a launched process: cargo run --example typed_context
//! Run against a crash dump: cargo run --example typed_context -- C:\path\to\dump.dmp
use dbgscope::dbgeng::{DebugEngine, RegisterValue};
fn show(e: &DebugEngine, cmd: &str) {
match e.execute_command(cmd) {
Ok(out) => print!("{out}"),
Err(err) => println!("ERR: {err}"),
}
}
fn rendered(value: &RegisterValue) -> String {
match value {
RegisterValue::Int(v) => format!("{v:#018x}"),
RegisterValue::Float(v) => format!("{v}"),
RegisterValue::Bytes(bytes) => hex::encode(bytes),
RegisterValue::Unavailable => "(unavailable)".to_string(),
}
}
fn main() {
let e = DebugEngine::new();
let target = std::env::args().nth(1);
match &target {
Some(dump) => {
println!("=== opening dump {dump} ===");
if let Err(err) = e.open_dump(dump) {
println!("open_dump ERR: {err}");
return;
}
if let Err(err) = e.wait_for_event(60_000) {
println!("load wait ERR: {err}");
return;
}
}
None => {
println!("=== launching cmd.exe ===");
if let Err(err) = e.launch_process("cmd.exe /c exit 42") {
println!("launch ERR: {err}");
return;
}
}
}
// --- registers ------------------------------------------------------------------
println!("\n=== r (what a host would have had to parse) ===");
show(&e, "r");
println!("\n=== register_values() ===");
match e.register_values() {
Ok(registers) => {
let subs = registers.iter().filter(|r| r.subregister).count();
println!(
"{} registers ({subs} subregisters), {} unavailable",
registers.len(),
registers
.iter()
.filter(|r| r.value == RegisterValue::Unavailable)
.count()
);
// The general-purpose set, then a sample of everything else: the whole bank is
// hundreds of entries and the point is the shape, not the volume.
for register in registers.iter().filter(|r| !r.subregister).take(24) {
println!(" {:<10} {}", register.name, rendered(®ister.value));
}
for name in ["xmm0", "st0", "efl", "cs"] {
match registers.iter().find(|r| r.name == name) {
Some(register) => {
println!(" {:<10} {}", register.name, rendered(®ister.value))
}
None => println!(" {name:<10} (not present on this target)"),
}
}
}
Err(err) => println!("register_values ERR: {err}"),
}
// Compare against the same value the engine prints as the current instruction's address.
match e.instruction_pointer() {
Ok(ip) => println!(" instruction_pointer() = {ip:#018x}"),
Err(err) => println!(" instruction_pointer ERR: {err}"),
}
// --- where the debugger is ------------------------------------------------------
//
// `~.` is the text these two replace, and it is three different shapes: a user-mode thread
// prints an engine index and a system id, a kernel processor prints a processor number, and
// an engine with no thread context prints nothing useful at all. Read the pair against it.
println!("\n=== ~. ===");
show(&e, "~.");
println!("\n=== current_thread_system_id() / current_processor() ===");
match e.current_thread_system_id() {
Ok(tid) => println!(" current_thread_system_id() = {tid} ({tid:#x})"),
Err(err) => println!(" current_thread_system_id ERR: {err}"),
}
match e.current_processor() {
// Expected on every target this example can open: both of its openers are user-mode or
// a dump of one. A number here means the mapping resolved on a kernel target.
Ok(None) => println!(" current_processor() = none (no processor number applies here)"),
Ok(Some(cpu)) => println!(" current_processor() = {cpu}"),
// Distinct from `none` on purpose: this is a mapping that could not be read, not a target
// with no processor. Against either of this example's openers it should not happen.
Err(err) => println!(" current_processor ERR (unknown, not absent): {err}"),
}
// --- modules --------------------------------------------------------------------
println!("\n=== lm ===");
show(&e, "lm");
println!("\n=== modules() ===");
match e.modules() {
Ok(modules) => {
println!("{} loaded modules", modules.len());
for module in modules.iter().take(12) {
println!(
" {:#018x}-{:#018x} {:<16} {:<24} {:?}{}",
module.base,
module.end(),
module.name,
module.image_name,
module.symbols,
if module.user_mode { " [user]" } else { "" }
);
}
// A name that came back empty is the truncation bug this section is here to catch.
let nameless = modules.iter().filter(|m| m.name.is_empty()).count();
println!("modules with no name: {nameless} (expected 0)");
}
Err(err) => println!("modules ERR: {err}"),
}
// --- unloaded modules -----------------------------------------------------------
//
// The tail `lm` prints under `Unloaded modules:`, which `modules()` deliberately does not
// carry. Compared against that text by eye like everything else here: the count, the names
// and the ranges have to be the rows `lm` printed above.
println!("\n=== unloaded_modules() ===");
match e.unloaded_modules() {
Ok(modules) => {
println!("{} unloaded modules", modules.len());
for module in modules.iter().take(8) {
println!(
" {:#018x}-{:#018x} {:<16} {:<24} {:?}",
module.base,
module.end(),
module.name,
module.image_name,
module.symbols,
);
}
// `name` is empty for these *by design* — nothing is left to qualify a symbol with —
// so the name to check is the image's, which is what `lm` prints in its place and
// what the kernel truncates to twelve characters.
let nameless = modules.iter().filter(|m| m.image_name.is_empty()).count();
println!("unloaded modules with no image name: {nameless} (expected 0)");
let named = modules.iter().filter(|m| !m.name.is_empty()).count();
println!("unloaded modules with a module name: {named} (expected 0)");
let flagged = modules.iter().filter(|m| m.unloaded).count();
println!(
"flagged unloaded by the engine: {flagged} (expected {})",
modules.len()
);
}
Err(err) => println!("unloaded_modules ERR: {err}"),
}
// --- breakpoints ----------------------------------------------------------------
//
// Two on purpose: one that resolves now, and one on a module nothing has loaded, which is
// the case where the address is *absent* rather than zero.
println!("\n=== breakpoints ===");
let resolvable = match target {
Some(_) => "nt!KeBugCheckEx",
None => "ntdll!NtCreateFile",
};
show(&e, &format!("bp {resolvable}"));
show(&e, "bp nosuchmodule!NoSuchSymbol");
show(&e, "bl");
match e.breakpoints() {
Ok(breakpoints) => {
println!("{} breakpoints", breakpoints.len());
for bp in &breakpoints {
println!(
" #{} {:?} address={} expr={:?} enabled={} deferred={} passes={}/{}",
bp.id,
bp.kind,
bp.address
.map_or_else(|| "(unresolved)".to_string(), |a| format!("{a:#018x}")),
bp.expression,
bp.enabled,
bp.deferred,
bp.passes_remaining,
bp.pass_count,
);
}
}
Err(err) => println!("breakpoints ERR: {err}"),
}
let _ = e.end_session();
}