use tabled::builder::Builder;
use tabled::settings::object::Rows;
use tabled::settings::{Alignment, Modify, Panel};
use owo_colors::OwoColorize;
use crate::error::{Error, Result};
use crate::expr::Expr;
use crate::symbols::LocalVariableLocation;
use crate::target::{irp_major_function_name, kthread_state_name, wait_reason_name};
use crate::trapframe::read_ktrap_frame_at_or_current;
use crate::triage_report::{
BlackboxState, FailureSignatureSource, TriageReport, WheaRecordState, exception_code_name,
filetime_to_iso,
};
use crate::types::VirtAddr;
use crate::ui;
use crate::unwind::{StackTrace, build_stacktrace, format_symbol, resolve_thread_trace_context};
use crate::repl::*;
repl_command! {
cmd_pte;
names: ["pte", "!pte"],
usage: "pte <address>",
summary: "Display page table entries for an address.",
completion: Expression,
}
repl_command! {
cmd_pool;
names: ["pool", "!pool"],
usage: "pool <address-expression>",
summary: "Inspect the pool page containing an address.",
completion: Expression,
}
repl_command! {
cmd_registers;
names: ["registers", "r"],
usage: "r [register[=expression]]",
summary: "Display CPU registers or assign one register.",
run_state: Halted,
}
repl_command! {
cmd_k;
names: ["k", "kb", "kp", "kv"],
usage: "k|kb|kp|kv [count]",
summary: "Display a stack; kp adds PDB parameter locations and kv provenance.",
run_state: Halted,
}
repl_command! {
cmd_status();
names: ["status"],
usage: "status",
summary: "Display current VM status.",
}
repl_command! {
cmd_capabilities();
names: ["capabilities"],
usage: "capabilities",
summary: "Display backend capabilities.",
}
repl_command! {
cmd_dbgprint;
names: ["dbgprint"],
usage: "dbgprint [count]",
summary: "Show captured guest debug output (DbgPrint).",
}
repl_command! {
cmd_irp;
names: ["irp", "!irp"],
usage: "irp <address-expression>",
summary: "Inspect an IRP and its current IO_STACK_LOCATION.",
completion: Expression,
}
repl_command! {
cmd_irps;
names: ["irps"],
usage: "irps [process-filter|driver-filter]",
summary: "Discover in-flight IRPs from thread IrpLists and device CurrentIrp.",
completion: Process,
}
repl_command! {
cmd_drvobj;
names: ["drvobj", "!drvobj"],
usage: "drvobj <driver-object-expression-or-name>",
summary: "Inspect a DRIVER_OBJECT, its device chain and dispatch table.",
completion: Driver,
}
repl_command! {
cmd_devobj;
names: ["devobj", "!devobj"],
usage: "devobj <device-object-expression>",
summary: "Inspect a DEVICE_OBJECT and its attached stack.",
completion: Expression,
}
repl_command! {
cmd_object;
names: ["object", "!object"],
usage: "object <object-expression>",
summary: "Inspect an executive object header and body.",
completion: Expression,
}
repl_command! {
cmd_callbacks;
names: ["callbacks"],
usage: "callbacks [symbol-filter]",
summary: "Enumerate process/thread/image notification callbacks.",
completion: Symbol,
}
repl_command! {
cmd_ssdt();
names: ["ssdt"],
usage: "ssdt",
summary: "Dump the SSDT and shadow SSDT.",
}
repl_command! {
cmd_address;
names: ["address"],
usage: "address <address-expression>",
summary: "Describe what an address belongs to (module+section, or VAD region).",
completion: Expression,
}
repl_command! {
cmd_trap;
names: ["trap", ".trap"],
usage: "trap [address-expression]",
summary: "Decode and display a _KTRAP_FRAME (defaults to the current thread's saved frame).",
completion: Expression,
}
repl_command! {
cmd_analyze();
names: ["analyze", "!analyze"],
usage: "analyze",
summary: "Display a coherent first-pass crash triage report.",
}
const ANALYZE_STACK_LIMIT: usize = 16;
const ANALYZE_MODULE_LIMIT: usize = 16;
const ANALYZE_UNLOADED_LIMIT: usize = 12;
fn print_triage_report(report: &TriageReport) {
println!("{}", ui::label("crash analysis"));
match &report.bugcheck {
Some(analysis) => {
println!();
print_bugcheck_analysis(analysis);
}
None => println!("{}", ui::muted("no recorded bugcheck")),
}
if let Some(exception) = &report.exception {
print_section("exception");
println!(
" {} {} ({:#010x})",
ui::muted("code "),
exception_code_name(exception.code),
exception.code
);
println!(" {} {}", ui::muted("address"), ui::addr(exception.address));
println!(" {} {:#x}", ui::muted("flags "), exception.flags);
for (index, parameter) in exception.parameters.iter().enumerate() {
println!(
" {} {}",
ui::muted(&format!("param {} ", index + 1)),
ui::addr(*parameter)
);
}
}
print_section("faulting context");
println!(
" {} {}",
ui::muted("state "),
if report.status.running {
"running"
} else {
"halted"
}
);
println!(
" {} {}",
ui::muted("thread "),
ui::thread_id(&report.status.current_thread)
);
if let Some(rip) = report.status.rip {
let symbol = report
.status
.symbol
.as_deref()
.map(|symbol| format!(" {}", ui::symbol(symbol)))
.unwrap_or_default();
println!(" {} {}{}", ui::muted("rip "), ui::addr(rip), symbol);
}
if let Some((pid, name, eprocess)) = &report.status.process {
println!(
" {} {} (pid {}, eprocess {})",
ui::muted("scope "),
name,
pid,
ui::addr(*eprocess)
);
}
if !report.status.coherent {
println!(
" {}",
ui::muted("target metadata is still being rebuilt after reload")
);
}
if let Some(context) = &report.crash_context {
let process = context.process_name.as_deref().unwrap_or("unknown");
let pid = context
.process_id
.map(|pid| pid.to_string())
.unwrap_or_else(|| "unknown".into());
let tid = context
.thread_id
.map(|tid| tid.to_string())
.unwrap_or_else(|| "unknown".into());
println!(
" {} {} (pid {}, tid {})",
ui::muted("crash "),
process,
pid,
tid
);
if let Some(parent) = context.parent_process_id {
println!(" {} {}", ui::muted("parent "), parent);
}
if let Some(status) = context.exit_status {
println!(" {} {:#x}", ui::muted("process exit"), status as u32);
}
if let Some(status) = context.thread_exit_status {
println!(" {} {:#x}", ui::muted("thread exit "), status as u32);
}
if let Some(time) = context.create_time
&& let Some(time) = filetime_to_iso(time)
{
println!(" {} {}", ui::muted("created"), time);
}
}
if let Some(prcb) = &report.prcb {
println!(
" {} #{} thread {} {} MHz {}",
ui::muted("processor"),
prcb.processor_number,
ui::addr(prcb.current_thread),
prcb.mhz,
prcb.vendor_string
);
}
match &report.backtrace {
Some(trace) => print_stacktrace_data(trace, ANALYZE_STACK_LIMIT, true),
None if report.status.running => {
print_section("stack");
println!(" {}", ui::muted("unavailable while target is running"));
}
None => {
print_section("stack");
println!(" {}", ui::muted("unavailable from captured context"));
}
}
if !report.warnings.is_empty() {
print_section("warnings");
for warning in &report.warnings {
println!(" {}", ui::muted(warning));
}
}
print_crash_intelligence(report);
print_report_modules(report);
print_dump_metadata(report);
}
fn print_crash_intelligence(report: &TriageReport) {
if let Some(signature) = &report.failure_signature {
print_section("failure signature");
println!(" {}", signature.bucket);
let source = match signature.source {
FailureSignatureSource::BugcheckFault => "bugcheck fault",
FailureSignatureSource::ExceptionAddress => "exception address",
FailureSignatureSource::CurrentInstruction => "current instruction",
FailureSignatureSource::TopFrame => "top frame",
FailureSignatureSource::CodeOnly => "code only",
};
println!(" {}", ui::muted(&format!("source: {source}")));
}
if let Some(culprit) = &report.culprit {
print_section("culprit attribution");
println!(
" {} {}",
ui::symbol(&culprit.module),
ui::muted(&format!("{:?} confidence", culprit.confidence).to_ascii_lowercase())
);
for evidence in &culprit.evidence {
match evidence.address {
Some(address) => println!(
" {} {} {}",
ui::muted(&format!("{:?}", evidence.kind)),
ui::addr(address),
evidence.detail
),
None => println!(
" {} {}",
ui::muted(&format!("{:?}", evidence.kind)),
evidence.detail
),
}
}
}
if let Some(verifier) = &report.verifier {
print_section("driver verifier");
println!(
" {} ({:#x}) subcode {:#x}: {}",
verifier.bugcheck_name,
verifier.bugcheck_code,
verifier.subcode,
verifier.subcode_description
);
if let Some(driver) = &verifier.associated_driver {
println!(" {} {}", ui::muted("driver"), ui::symbol(driver));
}
for address in &verifier.addresses {
println!(
" {} {}",
ui::muted(&address.role),
ui::addr(address.address)
);
}
for argument in &verifier.arguments {
println!(" {} {}", ui::addr(argument.value), argument.description);
}
}
if let Some(whea) = &report.whea {
print_section("WHEA");
if let Some(address) = whea.record_address {
println!(" {} {}", ui::muted("record"), ui::addr(address));
}
match &whea.state {
WheaRecordState::Decoded(record) => {
println!(
" revision {:#x}, severity {:#x}, length {:#x}, {} sections",
record.revision,
record.severity,
record.length,
record.sections.len()
);
for section in &record.sections {
println!(
" +{:#x} len {:#x} severity {:#x} {}",
section.offset, section.length, section.severity, section.section_type
);
}
}
WheaRecordState::Unavailable { reason } => {
println!(" {}", ui::muted(&format!("unavailable: {reason}")));
}
}
}
if !report.blackboxes.is_empty() {
print_section("blackbox streams");
for blackbox in &report.blackboxes {
let size = blackbox
.size
.map(|size| format!(", {size:#x} bytes"))
.unwrap_or_default();
match &blackbox.state {
BlackboxState::PresentUnparsed => {
println!(
" {}{} {}",
blackbox.name,
size,
ui::muted("present, unparsed")
);
}
BlackboxState::Unavailable { reason } => {
println!(" {}{} {}", blackbox.name, size, ui::muted(reason));
}
}
}
}
}
fn print_report_modules(report: &TriageReport) {
print_section("loaded modules");
let relevant_count = report
.modules
.iter()
.filter(|module| report.loaded_module_is_relevant(module))
.count();
if relevant_count == 0 {
println!(
" {}",
ui::muted(&format!(
"{} loaded; none contain a recorded fault or stack address",
report.modules.len()
))
);
} else {
for module in report
.modules
.iter()
.filter(|module| report.loaded_module_is_relevant(module))
.take(ANALYZE_MODULE_LIMIT)
{
println!(
" {:<24} {}-{} {:#x} bytes",
module.name,
ui::addr(module.base_address.0),
ui::addr(module.end_address().0),
module.size
);
}
if relevant_count > ANALYZE_MODULE_LIMIT {
println!(
" {}",
ui::muted(&format!(
"... {} more address-matched modules",
relevant_count - ANALYZE_MODULE_LIMIT
))
);
}
println!(
" {}",
ui::muted(&format!("{} loaded modules total", report.modules.len()))
);
}
if report.unloaded_drivers.is_empty() {
return;
}
print_section("unloaded modules");
let mut shown = 0;
for driver in report
.unloaded_drivers
.iter()
.filter(|driver| report.unloaded_driver_is_relevant(driver))
.take(ANALYZE_UNLOADED_LIMIT)
{
println!(
" {:<24} {}-{} {}",
driver.name,
ui::addr(driver.start_address),
ui::addr(driver.end_address),
ui::muted("recorded address/name match")
);
shown += 1;
}
for driver in report
.unloaded_drivers
.iter()
.filter(|driver| !report.unloaded_driver_is_relevant(driver))
.take(ANALYZE_UNLOADED_LIMIT - shown)
{
println!(
" {:<24} {}-{}",
driver.name,
ui::addr(driver.start_address),
ui::addr(driver.end_address)
);
shown += 1;
}
if report.unloaded_drivers.len() > shown {
println!(
" {}",
ui::muted(&format!(
"... {} more unloaded modules",
report.unloaded_drivers.len() - shown
))
);
}
}
fn print_dump_metadata(report: &TriageReport) {
if report.system_info.is_none()
&& report.broken_driver.is_none()
&& report.triage_overflowed.is_none()
{
return;
}
print_section("dump metadata");
if let Some(info) = &report.system_info {
let machine = match info.machine_image_type {
0x014c => "I386",
0x8664 => "AMD64",
0xAA64 => "ARM64",
_ => "Unknown",
};
println!(
" {} Windows {}.{} {} service-pack build {}",
ui::muted("system "),
info.major_version,
info.minor_version,
machine,
info.service_pack_build
);
if info.system_up_time > 0 {
println!(
" {} {} seconds",
ui::muted("uptime "),
info.system_up_time / 10_000_000
);
}
if info.system_time > 0
&& let Some(time) = filetime_to_iso(info.system_time as u64)
{
println!(" {} {}", ui::muted("time "), time);
}
println!(
" {} {} suite {:#x}",
ui::muted("product"),
match info.product_type {
1 => "Workstation",
2 => "DomainController",
3 => "Server",
_ => "Unknown",
},
info.suite_mask
);
}
if let Some(driver) = &report.broken_driver {
println!(" {} {}", ui::muted("recorded broken driver"), driver);
}
if let Some(overflowed) = report.triage_overflowed {
println!(
" {} {}",
ui::muted("triage overflow"),
if overflowed { "yes" } else { "no" }
);
}
}
impl ReplState<'_> {
fn cmd_pte(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let expr = require_arg!(invocation, 0, "pte");
let address = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(a) => a,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
match self.ctx.target.pte_traverse(address) {
Ok(result) => {
let mut levels = vec![result.pxe, result.ppe];
if let Some(x) = result.pde {
levels.push(x);
}
if let Some(x) = result.pte {
levels.push(x);
}
let header = format!(
"VA {} DTB {}",
ui::addr(result.address.0),
ui::addr(result.dtb)
);
let mut builder = Builder::default();
let row_strings: Vec<String> = levels.iter().map(|l| l.to_string()).collect();
builder.push_record(row_strings);
let mut table = builder.build();
table
.with(Panel::header(header))
.with(Modify::new(Rows::first()).with(Alignment::center()))
.with(tabled::settings::Style::empty());
println!("{}\n", table);
}
Err(e) => {
error!("{}\n", e);
}
}
Ok(())
}
fn cmd_trap(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let address = match invocation.arg(0) {
Some(expr) => match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(address) => Some(address),
Err(e) => {
error!("{}", e);
return Ok(());
}
},
None => None,
};
match read_ktrap_frame_at_or_current(&self.ctx.target, address) {
Ok(frame) => {
let trace =
resolve_thread_trace_context(&self.ctx.target, self.ctx.target.kernel_dtb());
let symbol = format_symbol(&self.ctx.target, &trace, frame.rip);
print_ktrap_frame(&frame, Some(&symbol));
println!();
}
Err(e) => {
error!("{}", e);
}
}
Ok(())
}
fn cmd_analyze(&mut self) -> Result<()> {
let report = TriageReport::build(self.ctx);
print_triage_report(&report);
println!();
Ok(())
}
fn cmd_pool(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(expr) = invocation.arg(0) else {
println!("{}\n", command_help("pool"));
return Ok(());
};
let target = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(target) => target,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let layout = match pool_layout(&self.ctx.target) {
Ok(l) => l,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
if target.0 & (POOL_PAGE_SIZE - 1) == 0
&& let Some(big) = find_big_pool(&self.ctx.target, &layout, target)
{
print_big_pool(target, &big);
return Ok(());
}
let region = classify_pool_region(&self.ctx.target, target);
let (blocks, idx, base) = locate_pool_block_in_page(&self.ctx.target, &layout, target);
println!("pool page {}", ui::addr(base.0));
println!(" target : {}", ui::addr(target.0));
if let Some((name, start, end)) = region {
println!(
" region : {} [{} - {}]",
name,
ui::addr(start.0),
ui::addr(end.0)
);
}
if let Some(idx) = idx {
println!(
" blocks in run : {} (target is #{})",
blocks.len(),
idx + 1
);
}
println!();
print_pool_page_listing(&blocks, idx, target);
if idx.is_none() {
if let Some(big) = find_big_pool(&self.ctx.target, &layout, target) {
println!();
print_big_pool(target, &big);
return Ok(());
}
println!(" address does not lie inside a recognizable _POOL_HEADER block.");
println!(" it may be segment heap, special pool, a mapped view, or image/stack.");
if let Some(hint) = segment_heap_hint(&self.ctx.target) {
println!(" hint : {}", hint);
}
if let Some(near) = annotate_near_symbol(&self.ctx.target, target) {
println!(" near symbol : {}", near);
}
}
Ok(())
}
fn cmd_registers(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
if self.ctx.parked_windows_thread().is_some() {
error!(
"selected Windows thread is parked and has no coherent register context; use `vcpu <id>`"
);
return Ok(());
}
if let Err(e) = self
.ctx
.backend
.set_current_thread(&self.ctx.current_thread)
{
error!("failed to select execution context: {:?}", e);
return Ok(());
}
let mut regs = match self.ctx.read_registers() {
Ok(r) => r,
Err(e) => {
error!("failed to read registers: {:?}", e);
return Ok(());
}
};
self.ctx.target.registers = Some(self.ctx.register_map.to_hashmap(®s));
if !invocation.raw_tail.trim().is_empty() {
let tail = invocation.raw_tail.trim();
let Some((name, expression)) = tail.split_once('=') else {
if tail.split_whitespace().count() != 1 {
println!("{}\n", command_help(invocation.name));
return Ok(());
}
let requested_name = tail.trim_start_matches('@').to_ascii_lowercase();
let name = match requested_name.as_str() {
"efl" | "rflags" => "eflags",
name => name,
};
match self.ctx.register_map.read_u64(name, ®s) {
Ok(value) => println!("{name}={}", ui::addr(value)),
Err(e) => error!("{e}"),
}
return Ok(());
};
let requested_name = name.trim().trim_start_matches('@').to_ascii_lowercase();
let name = match requested_name.as_str() {
"efl" | "rflags" => "eflags",
name => name,
};
let expression = expression.trim();
if name.is_empty() || expression.is_empty() {
println!("{}\n", command_help(invocation.name));
return Ok(());
}
let value = match Expr::eval_with_radix(expression, &self.ctx.target, self.radix) {
Ok(value) => value.0,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
if let Err(e) = self.ctx.write_register(name, value) {
error!("failed to write register {name}: {e}");
return Ok(());
}
regs = match self.ctx.read_registers() {
Ok(regs) => regs,
Err(e) => {
error!("register written, but refresh failed: {e}");
return Ok(());
}
};
self.ctx.target.registers = Some(self.ctx.register_map.to_hashmap(®s));
println!("@{name} = {}\n", ui::addr(value));
}
print_registers(&self.ctx.register_map, ®s, false);
let read_cr = |name: &str| -> String {
self.ctx
.register_map
.read_u64(name, ®s)
.map(ui::addr)
.unwrap_or_else(|_| "N/A".to_string())
};
let read_seg = |name: &str| -> String {
self.ctx
.register_map
.read_u64(name, ®s)
.map(|v| format!("{:04x}", v))
.unwrap_or_else(|_| "N/A".to_string())
};
println!();
println!(
" cr0 {} cr2 {} cr3 {}",
read_cr("cr0"),
read_cr("cr2"),
read_cr("cr3")
);
println!(" cr4 {} cr8 {}", read_cr("cr4"), read_cr("cr8"));
println!();
println!(
" cs {} ds {} es {}",
read_seg("cs"),
read_seg("ds"),
read_seg("es")
);
println!(
" fs {} gs {} ss {}",
read_seg("fs"),
read_seg("gs"),
read_seg("ss")
);
println!();
Ok(())
}
fn print_stack_parameters(&self, trace: &StackTrace) -> Result<()> {
let mut printed_header = false;
for (index, frame) in trace.frames.iter().enumerate() {
let address = VirtAddr(frame.ip);
let symbol_dtb = self.ctx.target.symbol_dtb_for_address(address);
let Some(locals) = self
.ctx
.target
.symbols
.procedure_locals(symbol_dtb, address)?
else {
continue;
};
let parameters: Vec<_> = locals
.into_iter()
.filter(|local| local.is_parameter)
.collect();
if parameters.is_empty() {
continue;
}
if !printed_header {
println!("{}", ui::label("parameters (PDB locations)"));
printed_header = true;
}
for parameter in parameters {
let location = match parameter.location {
LocalVariableLocation::Register { register } => register,
LocalVariableLocation::RegisterRelative { register, offset } => {
if offset >= 0 {
format!("[{register}+{offset:#x}]")
} else {
format!("[{register}-{:#x}]", offset.unsigned_abs())
}
}
LocalVariableLocation::FrameRelative { offset } => {
if offset >= 0 {
format!("[frame+{offset:#x}]")
} else {
format!("[frame-{:#x}]", offset.unsigned_abs())
}
}
LocalVariableLocation::Unavailable { reason } => {
format!("unavailable: {reason}")
}
};
println!(
" #{} {:<24} {:<20} {}",
index, parameter.name, parameter.type_name, location
);
}
}
if !printed_header {
println!(
"{}",
ui::muted("parameter locations unavailable from loaded private symbols")
);
}
Ok(())
}
fn cmd_k(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let frame_limit = match invocation.arg(0) {
Some(count) => match Expr::eval_with_radix(count, &self.ctx.target, self.radix) {
Ok(count) => usize::try_from(count.0).unwrap_or(usize::MAX).min(4096),
Err(e) => {
error!("{}", e);
return Ok(());
}
},
None => 64,
};
if self.ctx.parked_windows_thread().is_some() {
let trace = match self.ctx.backtrace(frame_limit) {
Ok(trace) => trace,
Err(error) => {
error!("failed to unwind parked thread stack: {error}");
return Ok(());
}
};
match invocation.name {
"kv" => print_stacktrace_data_with_provenance(&trace, frame_limit, false),
"kp" => {
print_stacktrace_data_with_provenance(&trace, frame_limit, false);
self.print_stack_parameters(&trace)?;
}
_ => print_stacktrace_data(&trace, frame_limit, false),
}
println!();
return Ok(());
}
if let Err(e) = self
.ctx
.backend
.set_current_thread(&self.ctx.current_thread)
{
error!("failed to select execution context: {:?}", e);
return Ok(());
}
let regs = match self.ctx.read_registers() {
Ok(r) => r,
Err(e) => {
error!("failed to read registers: {:?}", e);
return Ok(());
}
};
match invocation.name {
"kv" => print_stacktrace_verbose(
&self.ctx.target,
&self.ctx.register_map,
®s,
frame_limit,
frame_limit,
),
"kp" => {
let trace =
build_stacktrace(&self.ctx.target, &self.ctx.register_map, ®s, frame_limit);
print_stacktrace_data_with_provenance(&trace, frame_limit, false);
self.print_stack_parameters(&trace)?;
}
_ => print_stacktrace(
&self.ctx.target,
&self.ctx.register_map,
®s,
frame_limit,
frame_limit,
false,
),
}
println!();
Ok(())
}
fn cmd_status(&mut self) -> Result<()> {
if self.ctx.backend.is_running() {
println!("VM is running\n");
} else {
if let Err(e) = self
.ctx
.backend
.set_current_thread(&self.ctx.current_thread)
{
error!("failed to select execution context: {:?}", e);
return Ok(());
}
print_stop_separator();
print_break_context(
&mut *self.ctx.backend,
&self.ctx.register_map,
&mut self.ctx.target,
&self.ctx.breakpoints,
&self.ctx.current_thread,
);
}
Ok(())
}
fn cmd_capabilities(&mut self) -> Result<()> {
print_backend_capabilities(&self.ctx.capabilities());
Ok(())
}
fn cmd_dbgprint(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
const DEFAULT_TAIL: usize = 50;
let count = match invocation.arg(0) {
Some(arg) => arg
.parse::<usize>()
.map_err(|_| Error::DebugInfo(format!("invalid count: {arg}")))?,
None => DEFAULT_TAIL,
};
let page = self.ctx.read_debug_output(0);
if page.lines.is_empty() {
println!("{}\n", ui::muted("no debug output captured"));
return Ok(());
}
let start = if count == 0 {
0
} else {
page.lines.len().saturating_sub(count)
};
for line in &page.lines[start..] {
println!(
"{} {}",
ui::muted(&fmt_timestamp(line.timestamp_ms)),
line.text
);
}
println!();
Ok(())
}
fn cmd_irp(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(expr) = invocation.arg(0) else {
println!("{}\n", command_help("irp"));
return Ok(());
};
let addr = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(a) => a,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let irp = match self.ctx.target.inspect_irp(addr) {
Ok(irp) => irp,
Err(e) => {
error!("{} is not a readable _IRP: {}", ui::addr(addr.0), e);
return Ok(());
}
};
let mode = if irp.requestor_mode == 0 {
"KernelMode"
} else {
"UserMode"
};
println!("irp {}", ui::addr(irp.address.0));
println!(" type : {:#x}", irp.irp_type);
println!(" size : {:#x}", irp.size);
println!(" stack count : {}", irp.stack_count);
println!(" current loc : {}", irp.current_location);
println!(
" pending : {}",
if irp.pending_returned { "yes" } else { "no" }
);
println!(" requestor mode: {} ({:#x})", mode, irp.requestor_mode);
if let Some(status) = irp.io_status {
println!(" io status : {:#x}", status);
}
println!(" user event : {}", ui::addr(irp.user_event.0));
println!(" user buffer : {}", ui::addr(irp.user_buffer.0));
println!(" mdl : {}", ui::addr(irp.mdl_address.0));
println!(" thread : {}", ui::addr(irp.thread.0));
match irp.current_stack {
Some(ios) => {
println!(" current stack : {}", ui::addr(ios.address.0));
println!(
" major : IRP_MJ_{} ({:#x})",
irp_major_function_name(ios.major_function),
ios.major_function
);
println!(" minor : {:#x}", ios.minor_function);
println!(" device : {}", ui::addr(ios.device_object.0));
println!(" file : {}", ui::addr(ios.file_object.0));
let completion = self
.ctx
.target
.closest_symbol_current_context(ios.completion_routine)
.unwrap_or_else(|| format!("{:#x}", ios.completion_routine.0));
println!(" completion : {}", completion);
println!(" context : {}", ui::addr(ios.context.0));
}
None => println!(" current stack : {}", "unavailable".bright_black()),
}
println!();
Ok(())
}
fn fmt_kernel_symbol(&self, a: VirtAddr) -> String {
let dtb = self.ctx.target.kernel_dtb();
self.ctx
.target
.symbols
.format_closest_symbol_for_address(dtb, a)
.map(|s| ui::symbol(&s))
.unwrap_or_else(|| ui::addr(a.0))
}
fn resolve_driver_by_name(&self, name: &str) -> Option<VirtAddr> {
let full;
let needle = if name.starts_with("\\Driver\\") {
name
} else {
full = format!("\\Driver\\{name}");
full.as_str()
};
self.ctx
.target
.enumerate_driver_objects()
.ok()?
.into_iter()
.find(|d| d.name.eq_ignore_ascii_case(needle))
.map(|d| d.object)
}
fn cmd_drvobj(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(expr) = invocation.arg(0) else {
println!("{}\n", command_help("drvobj"));
return Ok(());
};
let input = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(a) => Some(a),
Err(_) => self.resolve_driver_by_name(expr),
};
let Some(input) = input else {
error!("unknown driver object expression or name: {}", expr);
return Ok(());
};
let drv = match self.ctx.target.inspect_driver_object(input) {
Ok(drv) => drv,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let mode = if drv.via_pointer { "pointer" } else { "direct" };
println!("driver object {} ({})", ui::addr(drv.object.0), mode);
if let Some(name) = &drv.name {
println!(" name : {}", name);
}
println!(" driver start : {}", ui::addr(drv.driver_start.0));
println!(" driver size : {:#x}", drv.driver_size);
println!(" driver section: {}", ui::addr(drv.driver_section.0));
println!(
" driver unload : {}",
self.fmt_kernel_symbol(drv.driver_unload)
);
println!(" devices:");
if drv.device_chain.is_empty() {
println!(" {}", "(none)".bright_black());
} else {
for d in &drv.device_chain {
println!(
" {} type={:#x} flags={:#x} characteristics={:#x} attached={} next={}",
ui::addr(d.device.0),
d.device_type,
d.flags,
d.characteristics,
ui::addr(d.attached.0),
ui::addr(d.next.0)
);
}
}
println!(" dispatch table:");
for (i, fn_ptr) in drv.dispatch.iter().enumerate() {
println!(
" IRP_MJ_{:<28} {}",
irp_major_function_name(i as u8),
self.fmt_kernel_symbol(*fn_ptr)
);
}
println!();
Ok(())
}
fn cmd_devobj(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(expr) = invocation.arg(0) else {
println!("{}\n", command_help("devobj"));
return Ok(());
};
let addr = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(a) => a,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let dev = match self.ctx.target.inspect_device_object(addr) {
Ok(dev) => dev,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
println!("device object {}", ui::addr(dev.object.0));
println!(" type : {:#x}", dev.device_type);
println!(" flags : {:#x}", dev.flags);
println!(" characteristics : {:#x}", dev.characteristics);
println!(" driver object : {}", ui::addr(dev.driver_object.0));
println!(" attached device : {}", ui::addr(dev.attached_device.0));
println!(" next device : {}", ui::addr(dev.next_device.0));
println!(" current irp : {}", ui::addr(dev.current_irp.0));
println!(" device extension: {}", ui::addr(dev.device_extension.0));
if !dev.attached_stack.is_empty() {
println!("attached stack:");
for (i, e) in dev.attached_stack.iter().enumerate() {
println!(
" #{} {} driver={} type={:#x} flags={:#x}",
i + 1,
ui::addr(e.device.0),
self.fmt_kernel_symbol(e.driver_object),
e.device_type,
e.flags
);
}
}
println!();
Ok(())
}
fn cmd_object(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(expr) = invocation.arg(0) else {
println!("{}\n", command_help("object"));
return Ok(());
};
let addr = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(a) => a,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let o = match self.ctx.target.inspect_object_header(addr) {
Ok(o) => o,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
println!("object {}", ui::addr(o.body.0));
println!(" input : {} ({})", ui::addr(o.input.0), o.mode);
println!(" header : {}", ui::addr(o.header.0));
println!(" pointer count : {}", o.pointer_count);
println!(" handle count : {}", o.handle_count);
if let Some(ti) = o.type_index {
println!(" type index : {:#x}", ti);
}
if let Some(to) = o.type_object {
println!(" type object : {}", ui::addr(to.0));
}
if let Some(tn) = &o.type_name {
println!(" type name : {}", tn);
}
if let Some(mask) = o.info_mask {
println!(" info mask : {:#x}", mask);
}
if let Some(ni) = o.name_info {
println!(" name info : {}", ui::addr(ni.0));
}
if let Some(name) = &o.name {
println!(" name : {}", name);
}
println!();
Ok(())
}
fn cmd_callbacks(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let filter = invocation.arg(0).map(|s| s.to_lowercase());
let callbacks = match self.ctx.target.enumerate_notify_callbacks() {
Ok(c) => c,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let dtb = self.ctx.target.kernel_dtb();
let mut printed = 0;
let mut last_kind = "";
for c in &callbacks {
let target = self
.ctx
.target
.symbols
.format_closest_symbol_for_address(dtb, c.function)
.unwrap_or_else(|| format!("0x{:x}", c.function.0));
if let Some(f) = &filter
&& !target.to_lowercase().contains(f)
{
continue;
}
if c.kind != last_kind {
println!("{} callbacks:", c.kind);
last_kind = c.kind;
}
println!(
" [{:02}] fn={} block={} raw={} ctx={}",
c.index,
ui::symbol(&target),
ui::addr(c.block.0),
ui::addr(c.raw.0),
ui::addr(c.context.0)
);
printed += 1;
}
if printed == 0 {
match invocation.arg(0) {
Some(f) => println!("no callbacks matching '{}'", f),
None => println!("no registered callbacks found"),
}
}
println!();
Ok(())
}
fn cmd_ssdt(&mut self) -> Result<()> {
let tables = match self.ctx.target.dump_ssdt() {
Ok(t) => t,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
for (i, t) in tables.iter().enumerate() {
if i > 0 {
println!();
}
println!("{}: base={} limit={}", t.label, ui::addr(t.base.0), t.limit);
let expected = if t.label.contains("win32k") {
"win32k"
} else {
"nt"
};
let mut hooks = 0;
for e in &t.entries {
let display = e
.symbol
.as_deref()
.map(ui::symbol)
.unwrap_or_else(|| ui::addr(e.target.0));
let hooked = e
.module
.as_deref()
.map(|m| !m.to_lowercase().contains(expected))
.unwrap_or(false);
let mark = if hooked {
hooks += 1;
" [HOOK]".red().to_string()
} else {
String::new()
};
println!(" [{:4}] {}{}", e.index, display, mark);
}
if hooks > 0 {
println!(" {} hook(s) detected", hooks);
}
}
println!();
Ok(())
}
fn cmd_irps(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let filter = invocation.arg(0);
let hits = match self.ctx.target.discover_irps(filter) {
Ok(h) => h,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
if hits.is_empty() {
match filter {
Some(f) => println!(" {}", format!("no IRPs found for '{}'", f).bright_black()),
None => println!(" {}", "no IRPs found".bright_black()),
}
println!();
return Ok(());
}
println!(" {:<16} {:<7} Details", "IRP", "Source");
for h in &hits {
let details = if h.source == "thread" {
format!(
"pid={} tid={} ethread={} state={} wait={}",
h.pid.map(|p| p.to_string()).unwrap_or_else(|| "?".into()),
h.tid.map(|t| t.to_string()).unwrap_or_else(|| "?".into()),
h.ethread
.map(|e| ui::addr(e.0))
.unwrap_or_else(|| "?".into()),
h.state.map(kthread_state_name).unwrap_or("?"),
h.wait_reason.map(wait_reason_name).unwrap_or("?"),
)
} else {
format!(
"driver={} device={}",
h.driver.as_deref().unwrap_or("?"),
h.device
.map(|d| ui::addr(d.0))
.unwrap_or_else(|| "?".into()),
)
};
println!(
" {} {:<7} stack={:<2} current={:<2} {}",
ui::addr(h.irp.0),
h.source,
h.stack_count,
h.current_location,
details
);
}
println!();
Ok(())
}
fn cmd_address(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(expr) = invocation.arg(0) else {
println!("{}\n", command_help("address"));
return Ok(());
};
let addr = match Expr::eval_with_radix(expr, &self.ctx.target, self.radix) {
Ok(a) => a,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
let d = match self.ctx.target.describe_address(addr) {
Ok(d) => d,
Err(e) => {
error!("{}", e);
return Ok(());
}
};
println!("address {}", ui::addr(d.address.0));
println!(" kind : {}", d.kind);
if let Some(m) = &d.module {
println!(
" module : {}+{:#x} (base {}, size {:#x})",
m.name,
m.offset,
ui::addr(m.base.0),
m.size
);
}
if let Some(s) = &d.section {
println!(" section : {}", s);
}
if let Some(va) = &d.va_type {
println!(" region : {}", va);
}
if let Some(r) = &d.region {
println!(
" region : {} - {}",
ui::addr(r.start.0),
ui::addr(r.end.0)
);
if let Some(p) = r.protection {
println!(" protection : {:#x}", p);
}
if let Some(t) = r.vad_type {
println!(" vad type : {:#x}", t);
}
if let Some(pm) = r.private_memory {
println!(" private : {}", pm);
}
if let Some(det) = &r.details {
println!(" details : {}", det);
}
}
if d.module.is_none() && d.region.is_none() && d.va_type.is_none() {
println!(
" {}",
"not inside any loaded module, kernel region, or VAD".bright_black()
);
}
println!();
Ok(())
}
}
fn fmt_timestamp(ms: u64) -> String {
let secs = ms / 1000;
let millis = ms % 1000;
let tod = secs % 86_400;
let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
format!("{h:02}:{m:02}:{s:02}.{millis:03}")
}