use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use iced_x86::{Code, Decoder, DecoderOptions, Instruction, Mnemonic};
use single_instance::SingleInstance;
use std::sync::Arc;
use crate::backend::MemoryOps;
use crate::bugchecks::{CURRENT_KERNEL_RELOAD_WINDOW, looks_like_kernel_pointer};
use crate::dbg_backend::{
BackendCapability, BugcheckInfo, DebugBackend, DebugCapability, DebugOutputPage,
HW_BREAKPOINT_SLOTS, HwBreakpointAccess, StopEvent, WatchpointAccess,
};
use crate::disasm::{DisasmRow, decode_rows, disasm_formatter};
use crate::error::{Error, Result};
use crate::gdb::breakpoints::Breakpoint;
use crate::gdb::{BreakpointHitResult, BreakpointManager, RegisterMap};
use crate::kd::trace_enabled;
use crate::memory::{AddressSpace, DTB_IDENTITY};
use crate::phys::PhysMem;
use crate::target::{ReloadReport, Target, ThreadInfo};
use crate::types::VirtAddr;
use crate::unwind::{
StackTrace, build_stacktrace, preferred_code_dtb, resolve_thread_trace_context,
};
macro_rules! reload_trace {
($($arg:tt)*) => {
if trace_enabled() {
eprintln!("reload: {}", format_args!($($arg)*));
}
};
}
#[derive(Debug, Clone)]
pub enum ContinueOutcome {
Breakpoint {
id: u32,
address: u64,
symbol: Option<String>,
temporary: bool,
rip: u64,
condition_error: Option<String>,
},
Bugcheck {
rip: Option<u64>,
info: Option<BugcheckInfo>,
},
Stopped {
rip: u64,
exception_code: Option<u32>,
},
Step { rip: u64 },
TargetReloaded {
kernel_base: Option<u64>,
coherent: bool,
},
Running,
Halted { rip: u64 },
}
#[derive(Debug, Clone)]
pub struct RunStatus {
pub running: bool,
pub current_thread: String,
pub rip: Option<u64>,
pub symbol: Option<String>,
pub process: Option<(u64, String, u64)>,
pub coherent: bool,
pub kernel_base: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReloadDisposition {
Ordinary,
Reloaded { coherent: bool },
ReloadCompleted,
PendingRediscovery,
ResumePastAssist,
}
#[derive(Debug, Clone, Copy)]
pub enum StepKind {
Single,
RunTo(VirtAddr),
}
#[derive(Debug, Clone)]
pub enum BreakpointStopAction {
Hit {
id: u32,
address: u64,
symbol: Option<String>,
temporary: bool,
condition_error: Option<String>,
},
Resumed,
NotBreakpoint,
}
#[derive(Debug, Clone)]
pub enum WatchpointStopAction {
Hit {
breakpoint: Breakpoint,
condition_error: Option<String>,
},
Resumed,
NotBreakpoint,
}
fn update_target_context_from_registers(
target: &mut Target,
register_map: &RegisterMap,
registers: Result<Vec<u8>>,
) {
let Ok(registers) = registers else {
target.registers = None;
target.clear_context_dtb_override();
return;
};
target.registers = Some(register_map.to_hashmap(®isters));
match register_map.read_u64("cr3", ®isters) {
Ok(cr3) if cr3 != 0 && target.guest.is_some() && target.kernel_dtb() != DTB_IDENTITY => {
target.set_context_dtb_override(cr3)
}
_ => target.clear_context_dtb_override(),
}
}
#[derive(Debug, Clone)]
pub struct VcpuInfo {
pub id: String,
pub rip: Option<u64>,
pub context: String,
pub symbol: Option<String>,
pub error: Option<String>,
}
const DTB_PAGE_MASK: u64 = 0x000F_FFFF_FFFF_F000;
const CONTINUE_POLL_INTERVAL: Duration = Duration::from_millis(200);
const SERVICE_IDLE_BUDGET: Duration = Duration::from_millis(5);
const STATUS_BREAKPOINT: u32 = 0x8000_0003;
const STATUS_SINGLE_STEP: u32 = 0x8000_0004;
pub struct Session {
id: usize,
pub target: Target,
pub backend: Box<dyn DebugBackend>,
pub breakpoints: BreakpointManager,
pub register_map: RegisterMap,
pub current_thread: String,
pub reload_module_list_pending: bool,
reload_surface_pending: bool,
parked_stop: Option<ContinueOutcome>,
_instance_guard: Option<InstanceGuard>,
}
impl Session {
pub fn connect<F>(phys: Arc<PhysMem>, target: Option<&str>, make_backend: F) -> Result<Self>
where
F: FnOnce() -> Result<Box<dyn DebugBackend>>,
{
let guard = target.map(acquire_instance_guard).transpose()?;
let backend = make_backend()?;
let mut session = Self::new(phys, backend)?;
session._instance_guard = guard;
Ok(session)
}
pub fn new(phys: Arc<PhysMem>, mut backend: Box<dyn DebugBackend>) -> Result<Self> {
let target = Target::with_phys(phys)?;
backend.initialize_from_target(&target);
let register_map = backend.register_map().clone();
let has_register_context = backend
.capabilities()
.iter()
.any(|c| c.capability == DebugCapability::ReadRegisters && c.supported);
let current_thread = if has_register_context {
backend
.stopped_thread_id()
.unwrap_or_else(|_| "1".to_string())
} else {
"1".to_string()
};
static NEXT_SESSION_ID: AtomicUsize = AtomicUsize::new(1);
let mut session = Self {
id: NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed),
target,
backend,
breakpoints: BreakpointManager::new(),
register_map,
current_thread,
reload_module_list_pending: false,
reload_surface_pending: false,
parked_stop: None,
_instance_guard: None,
};
if has_register_context {
session.refresh_context_for_current_thread();
}
Ok(session)
}
pub fn step(&mut self) -> Result<()> {
self.parked_stop = None;
self.backend.set_current_thread(&self.current_thread)?;
if !step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)? {
step_one_and_clear_tf(self.backend.as_mut(), &self.register_map)?;
}
let _ = self
.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target);
if let Ok(tid) = self.backend.stopped_thread_id() {
self.current_thread = tid;
}
self.refresh_context_for_current_thread();
Ok(())
}
pub fn set_current_thread(&mut self, id: &str) -> Result<()> {
self.backend.set_current_thread(id)?;
self.current_thread = id.to_string();
self.refresh_context_for_current_thread();
Ok(())
}
pub fn interrupt(&mut self) -> Result<StopEvent> {
let event = self.backend.interrupt()?;
if event.target_reloaded {
let _ = self.reload_with_hint(event.target_kernel_base_hint);
self.reload_surface_pending = true;
}
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
self.refresh_context_for_current_thread();
Ok(event)
}
fn refresh_context_for_current_thread(&mut self) {
if self.backend.is_running() {
return;
}
let registers = self
.backend
.set_current_thread(&self.current_thread)
.and_then(|_| self.backend.read_registers());
update_target_context_from_registers(&mut self.target, &self.register_map, registers);
}
pub fn current_instruction(&mut self) -> Result<Instruction> {
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
let rip = self.register_map.read_u64("rip", ®s)?;
let cr3 = self.register_map.read_u64("cr3", ®s).unwrap_or(0);
let trace = resolve_thread_trace_context(&self.target, cr3);
let code_dtb = preferred_code_dtb(&trace, rip);
let memory = AddressSpace::new(&self.target.phys, code_dtb);
let mut bytes = [0u8; 16];
memory.read_bytes(VirtAddr(rip), &mut bytes)?;
self.breakpoints
.mask_breakpoint_bytes(VirtAddr(rip), &mut bytes, trace.active_dtb);
let mut decoder = Decoder::with_ip(64, &bytes, rip, DecoderOptions::NONE);
let instruction = decoder.decode();
if instruction.code() == Code::INVALID {
return Err(Error::DebugInfo(format!(
"failed to decode instruction at {rip:#x}"
)));
}
Ok(instruction)
}
pub fn step_over_target(&mut self) -> Result<StepKind> {
let instruction = self.current_instruction()?;
if instruction.mnemonic() == Mnemonic::Call {
Ok(StepKind::RunTo(VirtAddr(instruction.next_ip())))
} else {
Ok(StepKind::Single)
}
}
pub fn step_out_target(&mut self) -> Result<VirtAddr> {
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
let trace = build_stacktrace(&self.target, &self.register_map, ®s, 4);
let caller = trace
.frames
.get(1)
.ok_or_else(|| Error::DebugInfo("could not find caller return address".to_string()))?;
if caller.ip == 0 {
return Err(Error::DebugInfo(
"caller return address is null".to_string(),
));
}
Ok(VirtAddr(caller.ip))
}
pub fn run_to(&mut self, address: VirtAddr, cancel: &AtomicBool) -> Result<ContinueOutcome> {
if self
.breakpoints
.enabled_breakpoint_id_for_current_context(&self.target, address)
.is_some()
{
return self.continue_until_break(None, cancel);
}
let temp_id =
self.breakpoints
.add_temporary_code(self.backend.as_mut(), &self.target, address)?;
let outcome = self.continue_until_break(None, cancel);
if self.backend.is_running() {
let _ = self.backend.interrupt();
}
let _ = self
.breakpoints
.remove(self.backend.as_mut(), &self.target, temp_id);
match outcome? {
ContinueOutcome::Breakpoint { id, rip, .. } if id == temp_id => {
Ok(ContinueOutcome::Step { rip })
}
other => Ok(other),
}
}
pub fn step_over(&mut self, cancel: &AtomicBool) -> Result<ContinueOutcome> {
match self.step_over_target()? {
StepKind::Single => {
self.step()?;
Ok(ContinueOutcome::Step {
rip: self.current_rip(),
})
}
StepKind::RunTo(addr) => self.run_to(addr, cancel),
}
}
pub fn step_out(&mut self, cancel: &AtomicBool) -> Result<ContinueOutcome> {
let target = self.step_out_target()?;
self.run_to(target, cancel)
}
fn current_rip(&mut self) -> u64 {
self.backend
.read_registers()
.ok()
.and_then(|r| self.register_map.read_u64("rip", &r).ok())
.unwrap_or(0)
}
pub fn write_register(&mut self, name: &str, value: u64) -> Result<()> {
let mut regs = self.backend.read_registers()?;
self.register_map.write_u64(name, &mut regs, value)?;
self.backend.write_registers(®s)
}
pub fn capabilities(&self) -> Vec<BackendCapability> {
self.backend.capabilities()
}
pub fn read_debug_output(&self, since_seq: u64) -> DebugOutputPage {
self.backend.read_debug_output(since_seq)
}
pub fn kernel_coherent(&self) -> bool {
!self.reload_module_list_pending && self.target.current_kernel_mapping_is_valid()
}
pub fn settle_pending_stop(&mut self) -> Result<()> {
if !self.backend.has_pending_stop() {
return Ok(());
}
let event = self.backend.wait_for_stop()?;
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
if !event.target_reloaded && !event.is_bugcheck {
let stray = stop_is_stray_single_step(&event, &self.breakpoints);
if stray || stop_is_assisted_refresh_breakin(&self.target, &self.breakpoints, &event) {
if stray {
let _ = clear_trap_flag(self.backend.as_mut(), &self.register_map);
}
self.try_finish_rediscovery_from_memory();
self.backend.continue_execution()?;
return Ok(());
}
if self.breakpoints.has_enabled_breakpoints() {
rewind_threads_off_breakpoints(
self.backend.as_mut(),
&self.register_map,
&self.breakpoints,
&self.current_thread,
);
if let Ok(regs) = self.backend.read_registers() {
let rip = self.register_map.read_u64("rip", ®s).unwrap_or(0);
let cr3 = self.register_map.read_u64("cr3", ®s).unwrap_or(0);
self.target.registers = Some(self.register_map.to_hashmap(®s));
if cr3 != 0 {
self.target.set_context_dtb_override(cr3);
}
if matches!(
self.resolve_breakpoint_stop(rip, cr3),
Ok(BreakpointStopAction::Resumed)
) {
return Ok(());
}
}
}
}
if event.target_reloaded {
let _ = self.reload_with_hint(event.target_kernel_base_hint);
self.reload_surface_pending = true;
}
self.refresh_context_for_current_thread();
Ok(())
}
pub fn service_idle(&mut self) {
if self.parked_stop.is_some() || !self.backend.has_pending_stop() {
return;
}
let never_cancel = AtomicBool::new(false);
match self.wait_for_stop_bounded(Some(SERVICE_IDLE_BUDGET), &never_cancel) {
Ok(ContinueOutcome::Running) | Err(_) => {}
Ok(ContinueOutcome::TargetReloaded { .. }) => {
self.reload_surface_pending = true;
}
Ok(outcome) => {
self.parked_stop = Some(outcome);
}
}
}
pub fn take_parked_stop(&mut self) -> Option<ContinueOutcome> {
self.parked_stop.take()
}
pub fn run_status(&mut self) -> RunStatus {
let _ = self.settle_pending_stop();
self.try_finish_rediscovery_from_memory();
self.clear_deferred_reload_surface();
let pending_stop = self.backend.has_pending_stop();
let running = self.backend.is_running() && !pending_stop;
let (rip, symbol) = if running || pending_stop {
(None, None)
} else {
let _ = self.backend.set_current_thread(&self.current_thread);
let rip = self
.backend
.read_registers()
.ok()
.and_then(|regs| self.register_map.read_u64("rip", ®s).ok());
let symbol = rip.and_then(|r| self.target.closest_symbol_current_context(VirtAddr(r)));
(rip, symbol)
};
let process = self
.target
.current_process_info
.as_ref()
.map(|p| (p.pid, p.name.clone(), p.eprocess_va.0));
RunStatus {
running,
current_thread: self.current_thread.clone(),
rip,
symbol,
process,
coherent: self.kernel_coherent(),
kernel_base: self.target.kernel_base().map(|a| a.0).unwrap_or(0),
}
}
pub fn add_breakpoint(&mut self, addr: VirtAddr) -> Result<u32> {
self.add_breakpoint_with_condition(addr, None)
}
pub fn add_breakpoint_with_condition(
&mut self,
addr: VirtAddr,
condition: Option<String>,
) -> Result<u32> {
self.add_breakpoint_with_symbol_condition(addr, None, condition)
}
pub fn add_breakpoint_with_symbol_condition(
&mut self,
addr: VirtAddr,
symbol: Option<String>,
condition: Option<String>,
) -> Result<u32> {
self.breakpoints
.add(self.backend.as_mut(), &self.target, addr, symbol, condition)
}
pub fn add_watchpoint(
&mut self,
addr: VirtAddr,
access: WatchpointAccess,
len: u8,
) -> Result<u32> {
self.add_watchpoint_with_condition(addr, access, len, None)
}
pub fn add_watchpoint_with_condition(
&mut self,
addr: VirtAddr,
access: WatchpointAccess,
len: u8,
condition: Option<String>,
) -> Result<u32> {
self.add_watchpoint_with_symbol_condition(addr, access, len, None, condition)
}
pub fn add_watchpoint_with_symbol_condition(
&mut self,
addr: VirtAddr,
access: WatchpointAccess,
len: u8,
symbol: Option<String>,
condition: Option<String>,
) -> Result<u32> {
self.breakpoints.add_hardware(
self.backend.as_mut(),
addr,
access.into(),
len,
symbol,
condition,
)
}
pub fn remove_breakpoint(&mut self, id: u32) -> Result<()> {
self.breakpoints
.remove(self.backend.as_mut(), &self.target, id)
}
pub fn enable_breakpoint(&mut self, id: u32) -> Result<()> {
self.breakpoints
.enable(self.backend.as_mut(), &self.target, id)
}
pub fn disable_breakpoint(&mut self, id: u32) -> Result<()> {
self.breakpoints
.disable(self.backend.as_mut(), &self.target, id)
}
pub fn list_breakpoints(&self) -> Vec<&Breakpoint> {
self.breakpoints.list()
}
pub fn id(&self) -> usize {
self.id
}
pub fn breakpoint(&self, id: u32) -> Option<&Breakpoint> {
self.breakpoints.list().into_iter().find(|bp| bp.id == id)
}
pub fn vcpus(&mut self) -> Result<Vec<VcpuInfo>> {
let original = self.backend.stopped_thread_id()?;
let threads = self.backend.thread_list()?;
let processes = self
.target
.guest
.as_ref()
.and_then(|g| g.enumerate_processes().ok())
.unwrap_or_default();
let kernel_dtb_masked = self
.target
.guest
.as_ref()
.map(|g| g.ntoskrnl.dtb() & DTB_PAGE_MASK);
let mut out = Vec::with_capacity(threads.len());
for thread in &threads {
let regs = self
.backend
.set_current_thread(thread)
.and_then(|_| self.backend.read_registers());
let regs = match regs {
Ok(regs) => regs,
Err(e) => {
out.push(VcpuInfo {
id: thread.clone(),
rip: None,
context: String::new(),
symbol: None,
error: Some(e.to_string()),
});
continue;
}
};
let (Ok(rip), Ok(cr3)) = (
self.register_map.read_u64("rip", ®s),
self.register_map.read_u64("cr3", ®s),
) else {
out.push(VcpuInfo {
id: thread.clone(),
rip: None,
context: String::new(),
symbol: None,
error: None,
});
continue;
};
if rip == 0 {
out.push(VcpuInfo {
id: thread.clone(),
rip: Some(0),
context: "no context".to_string(),
symbol: None,
error: None,
});
continue;
}
let cr3_masked = cr3 & DTB_PAGE_MASK;
let (context, symbol) = if kernel_dtb_masked.is_some_and(|k| cr3_masked == k) {
let sym = self
.target
.guest
.as_ref()
.and_then(|g| g.ntoskrnl.closest_symbol(VirtAddr(rip)).ok())
.map(|(s, o)| format!("{s}+{o:#x}"));
("kernel".to_string(), sym)
} else {
match processes
.iter()
.find(|p| (p.dtb & DTB_PAGE_MASK) == cr3_masked)
{
Some(proc) => {
let sym = self
.target
.symbols
.format_closest_symbol_for_address(proc.dtb, VirtAddr(rip));
(proc.name.clone(), sym)
}
None => {
let sym = self.target.closest_symbol_current_context(VirtAddr(rip));
let ctx = if sym.is_some() { "kernel" } else { "unknown" };
(ctx.to_string(), sym)
}
}
};
out.push(VcpuInfo {
id: thread.clone(),
rip: Some(rip),
context,
symbol,
error: None,
});
}
let _ = self.backend.set_current_thread(&original);
Ok(out)
}
pub fn active_thread_map(&mut self) -> HashMap<u64, (String, ThreadInfo)> {
let Ok(original) = self.backend.stopped_thread_id() else {
return HashMap::new();
};
let Ok(vcpus) = self.backend.thread_list() else {
return HashMap::new();
};
let mut active = HashMap::new();
for vcpu in &vcpus {
if self.backend.set_current_thread(vcpu).is_err() {
continue;
}
let Some(processor) = processor_index_from_backend_thread_id(vcpu) else {
continue;
};
if let Ok(thread) = self.target.current_windows_thread_for_processor(processor) {
active.insert(thread.ethread.0, (vcpu.clone(), thread));
}
}
let _ = self.backend.set_current_thread(&original);
active
}
pub fn windows_threads(&mut self) -> Result<(Vec<ThreadInfo>, HashMap<u64, String>)> {
let active = self.active_thread_map();
let mut threads = self.target.enumerate_threads()?;
for (_, thread) in active.values() {
if !threads.iter().any(|known| known.ethread == thread.ethread) {
threads.push(thread.clone());
}
}
threads.sort_by_key(|thread| (thread.pid.unwrap_or(u64::MAX), thread.tid));
let active_vcpus = active
.into_iter()
.map(|(ethread, (vcpu, _))| (ethread, vcpu))
.collect();
Ok((threads, active_vcpus))
}
pub fn disassemble(&self, addr: VirtAddr, count: usize) -> Result<Vec<DisasmRow>> {
let process = self.target.current_process()?;
let dtb = process.dtb();
let mut buf = vec![0u8; count * 16];
process.memory().read_bytes(addr, &mut buf)?;
self.breakpoints.mask_breakpoint_bytes(addr, &mut buf, dtb);
let symbols = &self.target.symbols;
let resolve = |target: u64| {
symbols
.format_closest_symbol_for_address(dtb, VirtAddr(target))
.unwrap_or_default()
};
let mut formatter = disasm_formatter();
Ok(decode_rows(
&buf,
addr.0,
Some(count),
&mut formatter,
resolve,
))
}
pub fn backtrace(&mut self, limit: usize) -> Result<StackTrace> {
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
Ok(build_stacktrace(
&self.target,
&self.register_map,
®s,
limit,
))
}
pub fn remove_all_breakpoints(&mut self) {
let ids: Vec<u32> = self.breakpoints.list().iter().map(|b| b.id).collect();
for id in ids {
let _ = self
.breakpoints
.remove(self.backend.as_mut(), &self.target, id);
}
}
pub fn cleanup_for_exit(&mut self) -> Result<()> {
if self.backend.is_running() {
let _ = self.backend.interrupt();
}
self.remove_all_breakpoints();
self.backend.prepare_for_exit(true)
}
pub fn resume(&mut self) -> Result<()> {
self.parked_stop = None;
self.try_finish_rediscovery_from_memory();
if self.breakpoints.has_enabled_breakpoints() {
self.backend.set_current_thread(&self.current_thread)?;
step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)?;
}
self.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target)?;
self.backend.continue_execution()?;
self.target.registers = None;
self.target.clear_context_dtb_override();
self.target.clear_current_windows_thread_context();
Ok(())
}
pub fn resolve_breakpoint_stop(&mut self, rip: u64, cr3: u64) -> Result<BreakpointStopAction> {
match self.breakpoints.check_breakpoint_hit(rip, cr3) {
BreakpointHitResult::Hit(bp) => {
let condition_error = match bp.evaluate_condition(&self.target) {
Ok(false) => {
step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)?;
self.backend.continue_execution()?;
return Ok(BreakpointStopAction::Resumed);
}
Ok(true) => None,
Err(error) => Some(error.to_string()),
};
let _ = self
.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target);
Ok(BreakpointStopAction::Hit {
id: bp.id,
address: bp.address.0,
symbol: bp.symbol.clone(),
temporary: bp.temporary,
condition_error,
})
}
BreakpointHitResult::NotBreakpoint => {
if self.breakpoints.breakpoint_id_at_address(rip).is_some() {
step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)?;
self.backend.continue_execution()?;
return Ok(BreakpointStopAction::Resumed);
}
Ok(BreakpointStopAction::NotBreakpoint)
}
}
}
pub fn continue_until_break(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
) -> Result<ContinueOutcome> {
if !self.backend.is_running() {
self.resume()?;
}
self.wait_for_stop_bounded(timeout, cancel)
}
pub fn wait_for_stop_bounded(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
) -> Result<ContinueOutcome> {
let deadline = timeout.map(|t| Instant::now() + t);
loop {
if cancel.load(Ordering::Relaxed) {
return Ok(ContinueOutcome::Running);
}
let poll = match deadline {
Some(dl) => {
let remaining = dl.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(ContinueOutcome::Running);
}
remaining.min(CONTINUE_POLL_INTERVAL)
}
None => CONTINUE_POLL_INTERVAL,
};
let mut event = match self.backend.try_wait_for_stop(poll)? {
Some(event) => event,
None => {
if !self.backend.is_running() {
if self.reload_surface_pending {
self.reload_surface_pending = false;
return Ok(ContinueOutcome::TargetReloaded {
kernel_base: self.target.kernel_base().map(|a| a.0),
coherent: self.kernel_coherent(),
});
}
let rip = self
.backend
.read_registers()
.ok()
.and_then(|regs| self.register_map.read_u64("rip", ®s).ok())
.unwrap_or(0);
return Ok(ContinueOutcome::Halted { rip });
}
continue;
}
};
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
if event.is_bugcheck && !event.target_reloaded {
self.target.registers = None;
return Ok(ContinueOutcome::Bugcheck {
rip: event.program_counter,
info: event.bugcheck.clone(),
});
}
match self.classify_reload_stop(&mut event) {
disposition @ (ReloadDisposition::Reloaded { .. }
| ReloadDisposition::ReloadCompleted) => {
let coherent =
!matches!(disposition, ReloadDisposition::Reloaded { coherent: false });
reload_trace!(
"continue: SURFACE target_reloaded base={} coherent={}",
self.target
.kernel_base()
.map_or_else(|| "none".to_string(), |a| format!("{:#x}", a.0),),
coherent,
);
self.refresh_context_for_current_thread();
return Ok(ContinueOutcome::TargetReloaded {
kernel_base: self.target.kernel_base().map(|a| a.0),
coherent,
});
}
ReloadDisposition::PendingRediscovery | ReloadDisposition::ResumePastAssist => {
reload_trace!("continue: absorb -> resume + keep waiting");
self.backend.continue_execution()?;
continue;
}
ReloadDisposition::Ordinary => {}
}
match resolve_watchpoint_stop(
self.backend.as_mut(),
&self.register_map,
&self.breakpoints,
&mut self.target,
&mut self.current_thread,
&event,
)? {
WatchpointStopAction::Hit {
breakpoint: bp,
condition_error,
} => {
let rip = self
.target
.registers
.as_ref()
.and_then(|regs| regs.get("rip").copied())
.unwrap_or(0);
return Ok(ContinueOutcome::Breakpoint {
id: bp.id,
address: bp.address.0,
symbol: bp.symbol,
temporary: bp.temporary,
rip,
condition_error,
});
}
WatchpointStopAction::Resumed => continue,
WatchpointStopAction::NotBreakpoint => {}
}
if stop_is_stray_single_step(&event, &self.breakpoints) {
let _ = clear_trap_flag(self.backend.as_mut(), &self.register_map);
self.backend.continue_execution()?;
continue;
}
if self.breakpoints.has_enabled_breakpoints() {
rewind_threads_off_breakpoints(
self.backend.as_mut(),
&self.register_map,
&self.breakpoints,
&self.current_thread,
);
}
let regs = self.backend.read_registers()?;
self.target.registers = Some(self.register_map.to_hashmap(®s));
let rip = self.register_map.read_u64("rip", ®s).unwrap_or(0);
let cr3 = self.register_map.read_u64("cr3", ®s).unwrap_or(0);
if cr3 != 0 {
self.target.set_context_dtb_override(cr3);
}
match self.resolve_breakpoint_stop(rip, cr3)? {
BreakpointStopAction::Hit {
id,
address,
symbol,
temporary,
condition_error,
} => {
return Ok(ContinueOutcome::Breakpoint {
id,
address,
symbol,
temporary,
rip,
condition_error,
});
}
BreakpointStopAction::Resumed => continue,
BreakpointStopAction::NotBreakpoint => {
return Ok(ContinueOutcome::Stopped {
rip,
exception_code: event.exception_code,
});
}
}
}
}
pub fn wait_for_stop(&mut self) -> Result<StopEvent> {
let event = self.backend.wait_for_stop()?;
if event.target_reloaded {
let _ = self.reload_with_hint(event.target_kernel_base_hint);
}
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
self.refresh_context_for_current_thread();
Ok(event)
}
pub fn reload_with_hint(&mut self, hint: Option<VirtAddr>) -> Result<()> {
let outcome = perform_target_reload(
self.backend.as_mut(),
&mut self.target,
&mut self.breakpoints,
hint,
);
self.reload_module_list_pending = !outcome
.report
.as_ref()
.map(reload_report_has_loaded_module_list)
.unwrap_or(false);
outcome.report.map(|_| ())
}
pub fn reload(&mut self) -> Result<()> {
self.reload_with_hint(None)
}
pub fn try_complete_pending_reload(&mut self) -> bool {
if !self.reload_module_list_pending {
return false;
}
let startup = match self.target.startup_message_data() {
Ok(startup) => startup,
Err(e) => {
reload_trace!("try_complete: startup read failed: {e}");
return false;
}
};
reload_trace!("try_complete: psmods={:#x}", startup.loaded_module_list.0);
if startup.loaded_module_list.is_zero() {
return false;
}
let _ = self.target.refresh_kernel_module_symbols();
self.backend.note_target_rediscovery_complete();
self.reload_module_list_pending = false;
true
}
pub fn try_finish_rediscovery_from_memory(&mut self) {
if !self.reload_surface_pending {
let _ = self.try_complete_pending_reload();
}
}
pub fn clear_deferred_reload_surface(&mut self) {
if self.target.current_kernel_mapping_is_valid() {
self.reload_surface_pending = false;
}
}
pub fn classify_reload_stop(&mut self, event: &mut StopEvent) -> ReloadDisposition {
reload_trace!(
"classify: pc={} exc={} assisted={} reloaded={} bugcheck={} pending={}",
event
.program_counter
.map_or_else(|| "none".to_string(), |p| format!("{p:#x}")),
event
.exception_code
.map_or_else(|| "none".to_string(), |c| format!("{c:#x}")),
event.assisted_breakin,
event.target_reloaded,
event.is_bugcheck,
self.reload_module_list_pending,
);
if stop_event_requires_target_reload(&self.target, event) {
event.target_reloaded = true;
let outcome = perform_target_reload(
self.backend.as_mut(),
&mut self.target,
&mut self.breakpoints,
event.target_kernel_base_hint,
);
return match outcome.report {
Ok(report) => {
let coherent = reload_report_has_loaded_module_list(&report);
self.reload_module_list_pending = !coherent;
self.reload_surface_pending = false;
reload_trace!(
"classify: reload ok hint={} new_base={} psmods={} coherent={}",
outcome
.hint
.map_or_else(|| "none".to_string(), |h| format!("{:#x}", h.0)),
self.target
.kernel_base()
.map_or_else(|| "none".to_string(), |a| format!("{:#x}", a.0),),
report.startup.as_ref().map_or_else(
|| "none".to_string(),
|s| format!("{:#x}", s.loaded_module_list.0),
),
coherent,
);
ReloadDisposition::Reloaded { coherent }
}
Err(ref e) => {
self.reload_module_list_pending = true;
self.reload_surface_pending = true;
reload_trace!("classify: reload err={e} -> pending_rediscovery");
ReloadDisposition::PendingRediscovery
}
};
}
if self.try_complete_pending_reload() {
if self.reload_surface_pending {
self.reload_surface_pending = false;
reload_trace!(
"classify: pending reload COMPLETED (unsurfaced) -> reload_completed"
);
return ReloadDisposition::ReloadCompleted;
}
if stop_is_assisted_refresh_breakin(&self.target, &self.breakpoints, event) {
reload_trace!("classify: pending reload COMPLETED silently -> resume_past_assist");
return ReloadDisposition::ResumePastAssist;
}
reload_trace!("classify: pending reload COMPLETED silently at a real stop");
return ReloadDisposition::Ordinary;
}
if stop_is_assisted_refresh_breakin(&self.target, &self.breakpoints, event) {
reload_trace!("classify: assisted refresh break-in -> resume_past_assist");
return ReloadDisposition::ResumePastAssist;
}
reload_trace!("classify: ordinary");
ReloadDisposition::Ordinary
}
}
struct InstanceGuard(#[allow(dead_code)] SingleInstance);
fn acquire_instance_guard(target: &str) -> Result<InstanceGuard> {
let canonical = canonicalize_target(target);
let key = format!("ntoseye-{:016x}", fnv1a_64(canonical.as_bytes()));
let instance = SingleInstance::new(&key).map_err(|err| {
Error::DebugInfo(format!("failed to create single-instance guard: {err:?}"))
})?;
if !instance.is_single() {
return Err(Error::AlreadyRunning(canonical));
}
Ok(InstanceGuard(instance))
}
fn canonicalize_target(target: &str) -> String {
if let Some(normalized) = normalize_host_port(target) {
return normalized;
}
let path = std::path::Path::new(target);
if let Ok(canon) = std::fs::canonicalize(path) {
return canon.to_string_lossy().into_owned();
}
let full = if path.is_relative() {
std::env::current_dir().unwrap_or_default().join(path)
} else {
path.to_path_buf()
};
let mut out = std::path::PathBuf::new();
for component in full.components() {
match component {
std::path::Component::RootDir => out.push("/"),
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.pop();
}
std::path::Component::Normal(s) => out.push(s),
_ => {}
}
}
out.to_string_lossy().into_owned()
}
fn normalize_host_port(target: &str) -> Option<String> {
let (host, port_str) = target.rsplit_once(':')?;
if host.contains('/') {
return None;
}
if host.contains(':') && !host.starts_with('[') {
return None;
}
let _port: u16 = port_str.parse().ok()?;
let host = host
.trim_start_matches('[')
.trim_end_matches(']')
.to_ascii_lowercase();
let host = match host.as_str() {
"localhost" | "ip6-localhost" | "::1" => "127.0.0.1",
other => other,
};
Some(format!("{host}:{port_str}"))
}
fn fnv1a_64(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
pub fn processor_index_from_backend_thread_id(thread_id: &str) -> Option<u16> {
let stripped = thread_id.strip_prefix("p1.")?;
let one_based = u16::from_str_radix(stripped, 16).ok()?;
one_based.checked_sub(1)
}
pub fn reload_report_has_loaded_module_list(report: &ReloadReport) -> bool {
report
.startup
.as_ref()
.is_some_and(|startup| !startup.loaded_module_list.is_zero())
}
pub struct TargetReloadOutcome {
pub report: Result<ReloadReport>,
pub hint: Option<VirtAddr>,
}
pub fn perform_target_reload(
backend: &mut dyn DebugBackend,
target: &mut Target,
breakpoints: &mut BreakpointManager,
event_hint: Option<VirtAddr>,
) -> TargetReloadOutcome {
breakpoints.clear_hardware_slots(backend);
*breakpoints = BreakpointManager::new();
let hint = event_hint.or_else(|| backend.target_kernel_base_hint().ok().flatten());
let report = target.reload_guest_with_kernel_base_hint(hint);
match &report {
Ok(_) => backend.note_target_rediscovery_complete(),
Err(_) => backend.note_target_rediscovery_pending(),
}
TargetReloadOutcome { report, hint }
}
pub fn stop_event_requires_target_reload(debugger: &Target, event: &StopEvent) -> bool {
if event.target_reloaded {
return true;
}
let Some(pc) = event.program_counter else {
return false;
};
if !looks_like_kernel_pointer(pc) {
return false;
}
if !debugger.current_kernel_mapping_is_valid() {
return true;
}
let current_dtb = debugger.kernel_dtb();
if debugger
.symbols
.find_module_for_address(current_dtb, VirtAddr(pc))
.is_some()
{
return false;
}
if !event.is_bugcheck
&& debugger
.kernel_base()
.is_some_and(|base| pc.abs_diff(base.0) < CURRENT_KERNEL_RELOAD_WINDOW)
{
return false;
}
debugger
.rediscovered_kernel_identity_changed()
.unwrap_or(false)
}
pub fn stop_is_assisted_refresh_breakin(
debugger: &Target,
breakpoints: &BreakpointManager,
event: &StopEvent,
) -> bool {
if event.bugcheck.is_some() || event.exception_code != Some(STATUS_BREAKPOINT) {
return false;
}
if event
.program_counter
.is_some_and(|pc| breakpoints.breakpoint_id_at_address(pc).is_some())
{
return false;
}
if event.assisted_breakin {
return true;
}
let Some(pc) = event.program_counter else {
return false;
};
debugger
.closest_symbol_current_context(VirtAddr(pc))
.as_deref()
.is_some_and(|symbol| {
symbol == "nt!DbgBreakPointWithStatus"
|| symbol.starts_with("nt!DbgBreakPointWithStatus+")
})
}
pub fn stop_is_stray_single_step(event: &StopEvent, breakpoints: &BreakpointManager) -> bool {
event.exception_code == Some(STATUS_SINGLE_STEP)
&& !event.is_bugcheck
&& event
.program_counter
.is_none_or(|pc| breakpoints.breakpoint_id_at_address(pc).is_none())
}
pub fn hardware_breakpoint_hit(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
event: &StopEvent,
) -> Result<Option<Breakpoint>> {
if event.exception_code != Some(STATUS_SINGLE_STEP)
|| event.is_bugcheck
|| !breakpoints.has_enabled_hardware_breakpoints()
{
return Ok(None);
}
let mut regs = backend.read_registers()?;
let dr6 = register_map.read_u64("dr6", ®s)?;
let hit = (0..HW_BREAKPOINT_SLOTS)
.filter(|slot| dr6 & (1u64 << slot) != 0)
.find_map(|slot| breakpoints.hardware_breakpoint_for_slot(slot));
let mut dirty = false;
let cleared = dr6 & !0b1111u64;
if cleared != dr6 {
register_map.write_u64("dr6", &mut regs, cleared)?;
dirty = true;
}
if hit
.as_ref()
.and_then(|bp| bp.hardware)
.is_some_and(|hw| hw.access == HwBreakpointAccess::Execute)
{
let eflags = register_map.read_u64("eflags", ®s)?;
const RF: u64 = 1 << 16;
if eflags & RF == 0 {
register_map.write_u64("eflags", &mut regs, eflags | RF)?;
dirty = true;
}
}
if dirty {
backend.write_registers(®s)?;
}
Ok(hit)
}
pub fn resolve_watchpoint_stop(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
target: &mut Target,
current_thread: &mut String,
event: &StopEvent,
) -> Result<WatchpointStopAction> {
let Some(breakpoint) = hardware_breakpoint_hit(backend, register_map, breakpoints, event)?
else {
return Ok(WatchpointStopAction::NotBreakpoint);
};
set_current_thread_from_stop(backend, event, current_thread);
let registers = backend.read_registers();
update_target_context_from_registers(target, register_map, registers);
let condition_error = match breakpoint.evaluate_condition(target) {
Ok(false) => {
backend.continue_execution()?;
return Ok(WatchpointStopAction::Resumed);
}
Ok(true) => None,
Err(error) => Some(error.to_string()),
};
Ok(WatchpointStopAction::Hit {
breakpoint,
condition_error,
})
}
pub fn rewind_threads_off_breakpoints(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
restore_thread: &str,
) {
let threads = match backend.thread_list() {
Ok(t) => t,
Err(_) => return,
};
for tid in &threads {
if backend.set_current_thread(tid).is_err() {
continue;
}
let Ok(regs) = backend.read_registers() else {
continue;
};
let rip = register_map.read_u64("rip", ®s).unwrap_or(0);
let cr3 = register_map.read_u64("cr3", ®s).unwrap_or(0);
let Some(prev) = rip.checked_sub(1) else {
continue;
};
if !matches!(
breakpoints.check_breakpoint_hit(prev, cr3),
BreakpointHitResult::Hit(_)
) {
continue;
}
let mut adjusted = regs.clone();
if register_map.write_u64("rip", &mut adjusted, prev).is_err() {
continue;
}
let _ = backend.write_registers(&adjusted);
}
let _ = backend.set_current_thread(restore_thread);
}
pub fn set_current_thread_from_stop(
backend: &mut dyn DebugBackend,
event: &StopEvent,
current: &mut String,
) {
let stopped_tid = event
.thread_id
.clone()
.or_else(|| backend.stopped_thread_id().ok());
if let Some(tid) = stopped_tid {
*current = tid;
let _ = backend.set_current_thread(current);
}
}
pub fn step_one_and_clear_tf(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
) -> Result<()> {
backend.step()?;
backend.wait_for_stop()?;
clear_trap_flag(backend, register_map)
}
pub fn clear_trap_flag(backend: &mut dyn DebugBackend, register_map: &RegisterMap) -> Result<()> {
if let Ok(mut regs) = backend.read_registers() {
let mut dirty = false;
if let Ok(eflags) = register_map.read_u64("eflags", ®s) {
let cleared = eflags & !(1u64 << 8);
if cleared != eflags && register_map.write_u64("eflags", &mut regs, cleared).is_ok() {
dirty = true;
}
}
if let Ok(dr6) = register_map.read_u64("dr6", ®s) {
let cleared = dr6 & !0b1111u64;
if cleared != dr6 && register_map.write_u64("dr6", &mut regs, cleared).is_ok() {
dirty = true;
}
}
if dirty {
backend.write_registers(®s)?;
}
}
Ok(())
}
pub fn step_over_current_breakpoint(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
debugger: &Target,
breakpoints: &mut BreakpointManager,
) -> Result<bool> {
let regs = backend.read_registers()?;
let rip = register_map.read_u64("rip", ®s)?;
let cr3 = register_map.read_u64("cr3", ®s)?;
let Some(bp_id) = breakpoints.breakpoint_id_at_address(rip) else {
return Ok(false);
};
if let Err(err) = breakpoints.disable(backend, debugger, bp_id) {
if matches!(
err,
Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)
) {
breakpoints
.disable_guest_memory_patch_in_address_space(backend, debugger, bp_id, cr3)?;
} else {
return Err(err);
}
}
step_one_and_clear_tf(backend, register_map)?;
if let Err(err) = breakpoints.enable(backend, debugger, bp_id) {
if matches!(
err,
Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)
) {
breakpoints.discard(bp_id)?;
} else {
return Err(err);
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gdb::breakpoints::HardwareBreakpoint;
use crate::kd::context::{REGISTER_BUFFER_SIZE, build_register_map};
const DR6_BS: u64 = 1 << 14;
const RF: u64 = 1 << 16;
const TF: u64 = 1 << 8;
const EFLAGS_BASE: u64 = 0x202;
struct MockBackend {
register_map: RegisterMap,
regs: Vec<u8>,
writes: usize,
fail_writes: bool,
}
impl MockBackend {
fn new() -> Self {
Self {
register_map: build_register_map(),
regs: vec![0u8; REGISTER_BUFFER_SIZE],
writes: 0,
fail_writes: false,
}
}
fn set(&mut self, name: &str, value: u64) {
self.register_map
.write_u64(name, &mut self.regs, value)
.unwrap();
}
fn get(&self, name: &str) -> u64 {
self.register_map.read_u64(name, &self.regs).unwrap()
}
}
impl DebugBackend for MockBackend {
fn register_map(&self) -> &RegisterMap {
&self.register_map
}
fn read_registers(&mut self) -> Result<Vec<u8>> {
Ok(self.regs.clone())
}
fn write_registers(&mut self, data: &[u8]) -> Result<()> {
self.writes += 1;
if self.fail_writes {
return Err(Error::Kd("injected register write failure".into()));
}
self.regs = data.to_vec();
Ok(())
}
fn set_breakpoint(&mut self, _addr: u64) -> Result<()> {
Err(Error::NotSupported)
}
fn remove_breakpoint(&mut self, _addr: u64) -> Result<()> {
Err(Error::NotSupported)
}
fn continue_execution(&mut self) -> Result<()> {
Err(Error::NotSupported)
}
fn step(&mut self) -> Result<()> {
Err(Error::NotSupported)
}
fn interrupt(&mut self) -> Result<StopEvent> {
Err(Error::NotSupported)
}
fn wait_for_stop(&mut self) -> Result<StopEvent> {
Err(Error::NotSupported)
}
fn try_wait_for_stop(&mut self, _timeout: Duration) -> Result<Option<StopEvent>> {
Ok(None)
}
fn thread_list(&mut self) -> Result<Vec<String>> {
Err(Error::NotSupported)
}
fn set_current_thread(&mut self, _thread_id: &str) -> Result<()> {
Err(Error::NotSupported)
}
fn stopped_thread_id(&mut self) -> Result<String> {
Err(Error::NotSupported)
}
fn is_running(&self) -> bool {
false
}
}
fn single_step_event() -> StopEvent {
StopEvent {
thread_id: None,
exception_code: Some(STATUS_SINGLE_STEP),
program_counter: None,
is_bugcheck: false,
bugcheck: None,
target_reloaded: false,
target_kernel_base_hint: None,
assisted_breakin: false,
}
}
fn manager_with_hw(slot: u8, access: HwBreakpointAccess, enabled: bool) -> BreakpointManager {
let mut manager = BreakpointManager::new();
let len = match access {
HwBreakpointAccess::Execute => 1,
_ => 4,
};
manager.insert_for_test(
7,
VirtAddr(0x1000),
enabled,
Some(HardwareBreakpoint { access, len, slot }),
);
manager
}
#[test]
fn hardware_breakpoint_hit_claims_matching_dr6_bit_and_clears_status() {
let manager = manager_with_hw(2, HwBreakpointAccess::Write, true);
let mut backend = MockBackend::new();
backend.set("dr6", (1 << 2) | DR6_BS);
backend.set("eflags", EFLAGS_BASE);
let map = build_register_map();
let hit = hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.expect("register update must succeed")
.expect("slot 2 #DB must be claimed by the registered watch");
assert_eq!(hit.id, 7);
assert_eq!(hit.hardware.expect("hw params").slot, 2);
assert_eq!(backend.get("dr6"), DR6_BS);
assert_eq!(backend.writes, 1);
assert_eq!(backend.get("eflags"), EFLAGS_BASE);
}
#[test]
fn hardware_breakpoint_hit_sets_resume_flag_only_for_execute_watches() {
for (access, want_rf) in [
(HwBreakpointAccess::Execute, true),
(HwBreakpointAccess::Write, false),
(HwBreakpointAccess::ReadWrite, false),
] {
let manager = manager_with_hw(0, access, true);
let mut backend = MockBackend::new();
backend.set("dr6", 1);
backend.set("eflags", EFLAGS_BASE);
let map = build_register_map();
let hit = hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.unwrap();
assert!(hit.is_some(), "{access:?} hit must be claimed");
let eflags = backend.get("eflags");
assert_eq!(eflags & RF != 0, want_rf, "{access:?}: RF mismatch");
assert_eq!(eflags & !RF, EFLAGS_BASE, "{access:?}: eflags clobbered");
assert_eq!(backend.get("dr6"), 0, "{access:?}: B0 not cleared");
}
}
#[test]
fn hardware_breakpoint_hit_propagates_required_register_write_failure() {
let manager = manager_with_hw(0, HwBreakpointAccess::Execute, true);
let mut backend = MockBackend::new();
backend.set("dr6", 1);
backend.set("eflags", EFLAGS_BASE);
backend.fail_writes = true;
let map = build_register_map();
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event()).is_err()
);
assert_eq!(backend.get("dr6"), 1);
assert_eq!(backend.get("eflags"), EFLAGS_BASE);
assert_eq!(backend.writes, 1);
}
#[test]
fn hardware_breakpoint_hit_ignores_non_single_step_stops() {
let manager = manager_with_hw(0, HwBreakpointAccess::Write, true);
let mut backend = MockBackend::new();
backend.set("dr6", 1); let before = backend.regs.clone();
let map = build_register_map();
let mut event = single_step_event();
event.exception_code = Some(0x8000_0003);
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &event)
.unwrap()
.is_none()
);
event.exception_code = None;
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &event)
.unwrap()
.is_none()
);
event.exception_code = Some(STATUS_SINGLE_STEP);
event.is_bugcheck = true;
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &event)
.unwrap()
.is_none()
);
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
}
#[test]
fn hardware_breakpoint_hit_requires_an_enabled_hardware_breakpoint() {
let map = build_register_map();
let mut backend = MockBackend::new();
backend.set("dr6", 1);
let before = backend.regs.clone();
let empty = BreakpointManager::new();
assert!(
hardware_breakpoint_hit(&mut backend, &map, &empty, &single_step_event())
.unwrap()
.is_none()
);
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
let manager = manager_with_hw(0, HwBreakpointAccess::Write, false);
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.unwrap()
.is_none()
);
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
}
#[test]
fn hardware_breakpoint_hit_clears_stale_dr6_bits_for_unregistered_slots() {
let manager = manager_with_hw(1, HwBreakpointAccess::Write, true);
let mut backend = MockBackend::new();
backend.set("dr6", (1 << 3) | DR6_BS);
let map = build_register_map();
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.unwrap()
.is_none()
);
assert_eq!(backend.get("dr6"), DR6_BS);
assert_eq!(backend.writes, 1);
}
#[test]
fn clear_trap_flag_clears_tf_and_dr6_status_in_one_write() {
let mut backend = MockBackend::new();
backend.set("eflags", TF | EFLAGS_BASE);
backend.set("dr6", 0b1011 | DR6_BS);
let map = build_register_map();
clear_trap_flag(&mut backend, &map).unwrap();
assert_eq!(backend.get("eflags"), EFLAGS_BASE);
assert_eq!(backend.get("dr6"), DR6_BS);
assert_eq!(backend.writes, 1);
}
#[test]
fn clear_trap_flag_skips_the_write_when_nothing_is_set() {
let mut backend = MockBackend::new();
backend.set("eflags", EFLAGS_BASE);
backend.set("dr6", DR6_BS);
let before = backend.regs.clone();
let map = build_register_map();
clear_trap_flag(&mut backend, &map).unwrap();
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
}
}