From af9a4ae8ee3749bb411a3ad476565dfb0b234e99 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 13 Jan 2025 00:30:47 -0500 Subject: [PATCH] Support watchpoints --- src/emulator.rs | 22 ++- src/emulator/address_set.rs | 231 ++++++++++++++++++++++++++++++++ src/emulator/shrooms_vb_core.rs | 193 ++++++++++++++++++++++++-- src/gdbserver.rs | 113 +++++++++++++--- 4 files changed, 527 insertions(+), 32 deletions(-) create mode 100644 src/emulator/address_set.rs diff --git a/src/emulator.rs b/src/emulator.rs index 5ddb560..9ec0380 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,8 +18,9 @@ use egui_toast::{Toast, ToastKind, ToastOptions}; use crate::{audio::Audio, graphics::TextureSink}; use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; -pub use shrooms_vb_core::{VBKey, VBRegister}; +pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; +mod address_set; mod shrooms_vb_core; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -421,6 +422,9 @@ impl Emulator { if let Some(reason) = sim.stop_reason() { let stop_reason = match reason { StopReason::Stepped => DebugStopReason::Trace, + StopReason::Watchpoint(watch, address) => { + DebugStopReason::Watchpoint(watch, address) + } StopReason::Breakpoint => DebugStopReason::Breakpoint, }; self.debug_stop(sim_id, stop_reason); @@ -547,6 +551,18 @@ impl Emulator { }; sim.remove_breakpoint(address); } + EmulatorCommand::AddWatchpoint(sim_id, address, length, watch) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.add_watchpoint(address, length, watch); + } + EmulatorCommand::RemoveWatchpoint(sim_id, address, length, watch) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.remove_watchpoint(address, length, watch); + } EmulatorCommand::SetAudioEnabled(p1, p2) => { self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); @@ -613,6 +629,8 @@ pub enum EmulatorCommand { ReadMemory(SimId, u32, usize, Vec, oneshot::Sender>), AddBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32), + AddWatchpoint(SimId, u32, usize, VBWatchpointType), + RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), SetAudioEnabled(bool, bool), Link, Unlink, @@ -645,6 +663,8 @@ pub enum DebugStopReason { Trace, // We hit a breakpoint Breakpoint, + // We hit a watchpoint + Watchpoint(VBWatchpointType, u32), // The debugger told us to pause Trapped, } diff --git a/src/emulator/address_set.rs b/src/emulator/address_set.rs new file mode 100644 index 0000000..fe8e5be --- /dev/null +++ b/src/emulator/address_set.rs @@ -0,0 +1,231 @@ +use std::{ + collections::{BTreeMap, HashSet}, + ops::Bound, +}; + +#[derive(Debug, Default)] +pub struct AddressSet { + ranges: HashSet<(u32, usize)>, + bounds: BTreeMap, +} + +impl AddressSet { + pub fn new() -> Self { + Self::default() + } + + pub fn add(&mut self, address: u32, length: usize) { + if length == 0 || !self.ranges.insert((address, length)) { + return; + } + let end = (address as usize) + .checked_add(length) + .and_then(|e| u32::try_from(e).ok()); + + if let Some(end) = end { + let val_before = self.bounds.range(..=end).next_back().map_or(0, |(_, &v)| v); + self.bounds.insert(end, val_before); + } + + let val_before = self + .bounds + .range(..address) + .next_back() + .map_or(0, |(_, &v)| v); + if let Some(&val_at) = self.bounds.get(&address) { + if val_before == val_at + 1 { + self.bounds.remove(&address); + } + } else { + self.bounds.insert(address, val_before); + } + + let start_bound = Bound::Included(address); + let end_bound = match end { + Some(e) => Bound::Excluded(e), + None => Bound::Unbounded, + }; + + for (_, val) in self.bounds.range_mut((start_bound, end_bound)) { + *val += 1; + } + } + + pub fn remove(&mut self, address: u32, length: usize) { + if !self.ranges.remove(&(address, length)) { + return; + } + let end = (address as usize) + .checked_add(length) + .and_then(|e| u32::try_from(e).ok()); + + if let Some(end) = end { + let val_before = self.bounds.range(..end).next_back().map_or(0, |(_, &v)| v); + if let Some(&val_at) = self.bounds.get(&end) { + if val_at + 1 == val_before { + self.bounds.remove(&end); + } + } else { + self.bounds.insert(end, val_before); + } + } + + let val_before = self + .bounds + .range(..address) + .next_back() + .map_or(0, |(_, &v)| v); + if let Some(&val_at) = self.bounds.get(&address) { + if val_before + 1 == val_at { + self.bounds.remove(&address); + } + } else { + self.bounds.insert(address, val_before); + } + + let start_bound = Bound::Included(address); + let end_bound = match end { + Some(e) => Bound::Excluded(e), + None => Bound::Unbounded, + }; + + for (_, val) in self.bounds.range_mut((start_bound, end_bound)) { + *val -= 1; + } + } + + pub fn clear(&mut self) { + self.ranges.clear(); + self.bounds.clear(); + } + + pub fn is_empty(&self) -> bool { + self.bounds.is_empty() + } + + pub fn contains(&self, address: u32) -> bool { + self.bounds + .range(..=address) + .next_back() + .is_some_and(|(_, &val)| val > 0) + } +} + +#[cfg(test)] +mod tests { + use super::AddressSet; + + #[test] + fn should_not_include_addresses_when_empty() { + let set = AddressSet::new(); + assert!(set.is_empty()); + assert!(!set.contains(0x13374200)); + } + + #[test] + fn should_include_addresses_when_full() { + let mut set = AddressSet::new(); + set.add(0x00000000, 0x100000000); + assert!(set.contains(0x13374200)); + } + + #[test] + fn should_ignore_empty_address_ranges() { + let mut set = AddressSet::new(); + set.add(0x13374200, 0); + assert!(set.is_empty()); + assert!(!set.contains(0x13374200)); + } + + #[test] + fn should_add_addresses_idempotently() { + let mut set = AddressSet::new(); + set.add(0x13374200, 1); + set.add(0x13374200, 1); + set.remove(0x13374200, 1); + assert!(set.is_empty()); + assert!(!set.contains(0x13374200)); + } + + #[test] + fn should_remove_addresses_idempotently() { + let mut set = AddressSet::new(); + set.add(0x13374200, 1); + set.remove(0x13374200, 1); + set.remove(0x13374200, 1); + assert!(set.is_empty()); + assert!(!set.contains(0x13374200)); + } + + #[test] + fn should_report_address_in_range() { + let mut set = AddressSet::new(); + set.add(0x13374200, 4); + assert!(!set.contains(0x133741ff)); + for address in 0x13374200..0x13374204 { + assert!(set.contains(address)); + } + assert!(!set.contains(0x13374204)); + } + + #[test] + fn should_allow_overlapping_addresses() { + let mut set = AddressSet::new(); + set.add(0x13374200, 4); + set.add(0x13374201, 1); + set.add(0x13374202, 2); + + assert!(!set.contains(0x133741ff)); + for address in 0x13374200..0x13374204 { + assert!(set.contains(address)); + } + assert!(!set.contains(0x13374204)); + + set.remove(0x13374200, 4); + assert!(!set.contains(0x13374200)); + for address in 0x13374201..0x13374204 { + assert!(set.contains(address)); + } + assert!(!set.contains(0x13374204)); + } + + #[test] + fn should_allow_removing_overlapped_address_ranges() { + let mut set = AddressSet::new(); + set.add(0x13374200, 8); + set.add(0x13374204, 8); + set.remove(0x13374204, 8); + + for address in 0x13374200..0x13374208 { + assert!(set.contains(address)); + } + for address in 0x13374208..0x1337420c { + assert!(!set.contains(address)); + } + } + + #[test] + fn should_merge_adjacent_ranges() { + let mut set = AddressSet::new(); + set.add(0x13374200, 4); + set.add(0x13374204, 4); + set.add(0x13374208, 4); + set.add(0x1337420c, 4); + + assert!(!set.contains(0x133741ff)); + for address in 0x13374200..0x13374210 { + assert!(set.contains(address)); + } + assert!(!set.contains(0x13374210)); + + set.remove(0x13374200, 4); + set.remove(0x13374204, 4); + set.remove(0x13374208, 4); + set.remove(0x1337420c, 4); + + assert!(set.is_empty()); + for address in 0x133741ff..=0x13374210 { + assert!(!set.contains(address)); + } + } +} diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 147992a..208f86a 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -5,6 +5,8 @@ use bitflags::bitflags; use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; +use super::address_set::AddressSet; + #[repr(C)] struct VB { _data: [u8; 0], @@ -62,9 +64,31 @@ pub enum VBRegister { PC, } -type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VBWatchpointType { + Read, + Write, + Access, +} + type OnExecute = extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int; +type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; +type OnRead = extern "C" fn( + sim: *mut VB, + address: u32, + type_: VBDataType, + value: *mut i32, + cycles: *mut u32, +) -> c_int; +type OnWrite = extern "C" fn( + sim: *mut VB, + address: u32, + type_: VBDataType, + value: *mut i32, + cycles: *mut u32, + cancel: *mut c_int, +) -> c_int; #[link(name = "vb")] extern "C" { @@ -112,15 +136,17 @@ extern "C" { #[link_name = "vbSetCartROM"] fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; #[link_name = "vbSetExecuteCallback"] - fn vb_set_execute_callback(sim: *mut VB, callback: Option); + fn vb_set_execute_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetFrameCallback"] - fn vb_set_frame_callback(sim: *mut VB, callback: Option); + fn vb_set_frame_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetKeys"] fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; #[link_name = "vbSetOption"] fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int); #[link_name = "vbSetPeer"] fn vb_set_peer(sim: *mut VB, peer: *mut VB); + #[link_name = "vbSetReadCallback"] + fn vb_set_read_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetSamples"] fn vb_set_samples( sim: *mut VB, @@ -130,6 +156,8 @@ extern "C" { ) -> c_int; #[link_name = "vbSetUserData"] fn vb_set_user_data(sim: *mut VB, tag: *mut c_void); + #[link_name = "vbSetWriteCallback"] + fn vb_set_write_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSizeOf"] fn vb_size_of() -> usize; } @@ -147,18 +175,73 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: // There is no way for the userdata to be null or otherwise invalid. let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; - let mut stopped = 0; + let mut stopped = data.stop_reason.is_some(); if data.step_from.is_some_and(|s| s != address) { data.step_from = None; data.stop_reason = Some(StopReason::Stepped); - stopped = 1; + stopped = true; } if data.breakpoints.binary_search(&address).is_ok() { data.stop_reason = Some(StopReason::Breakpoint); - stopped = 1; + stopped = true; } - stopped + if stopped { + 1 + } else { + 0 + } +} + +extern "C" fn on_read( + sim: *mut VB, + address: u32, + _type: VBDataType, + _value: *mut i32, + _cycles: *mut u32, +) -> c_int { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + + if data.read_watchpoints.contains(address) { + let watch = if data.write_watchpoints.contains(address) { + VBWatchpointType::Access + } else { + VBWatchpointType::Read + }; + data.stop_reason = Some(StopReason::Watchpoint(watch, address)); + } + + // Don't stop here, the debugger expects us to break after the memory access. + // We'll stop in on_execute instead. + 0 +} + +extern "C" fn on_write( + sim: *mut VB, + address: u32, + _type: VBDataType, + _value: *mut i32, + _cycles: *mut u32, + _cancel: *mut c_int, +) -> c_int { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + + if data.write_watchpoints.contains(address) { + let watch = if data.read_watchpoints.contains(address) { + VBWatchpointType::Access + } else { + VBWatchpointType::Write + }; + data.stop_reason = Some(StopReason::Watchpoint(watch, address)); + } + + // Don't stop here, the debugger expects us to break after the memory access. + // We'll stop in on_execute instead. + 0 } const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; @@ -170,6 +253,23 @@ struct VBState { stop_reason: Option, step_from: Option, breakpoints: Vec, + read_watchpoints: AddressSet, + write_watchpoints: AddressSet, +} + +impl VBState { + fn needs_execute_callback(&self) -> bool { + self.step_from.is_some() + || !self.breakpoints.is_empty() + || !self.read_watchpoints.is_empty() + || !self.write_watchpoints.is_empty() + } +} + +pub enum StopReason { + Breakpoint, + Watchpoint(VBWatchpointType, u32), + Stepped, } #[repr(transparent)] @@ -177,11 +277,6 @@ pub struct Sim { sim: *mut VB, } -pub enum StopReason { - Breakpoint, - Stepped, -} - impl Sim { pub fn new() -> Self { // init the VB instance itself @@ -201,6 +296,8 @@ impl Sim { stop_reason: None, step_from: None, breakpoints: vec![], + read_watchpoints: AddressSet::new(), + write_watchpoints: AddressSet::new(), }; unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; unsafe { vb_set_frame_callback(sim, Some(on_frame)) }; @@ -374,7 +471,71 @@ impl Sim { let data = self.get_state(); if let Ok(index) = data.breakpoints.binary_search(&address) { data.breakpoints.remove(index); - if data.step_from.is_none() && data.breakpoints.is_empty() { + if !data.needs_execute_callback() { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + } + } + + pub fn add_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) { + match watch { + VBWatchpointType::Read => self.add_read_watchpoint(address, length), + VBWatchpointType::Write => self.add_write_watchpoint(address, length), + VBWatchpointType::Access => { + self.add_read_watchpoint(address, length); + self.add_write_watchpoint(address, length); + } + } + } + + fn add_read_watchpoint(&mut self, address: u32, length: usize) { + let state = self.get_state(); + state.read_watchpoints.add(address, length); + if !state.read_watchpoints.is_empty() { + unsafe { vb_set_read_callback(self.sim, Some(on_read)) }; + unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) }; + } + } + + fn add_write_watchpoint(&mut self, address: u32, length: usize) { + let state = self.get_state(); + state.write_watchpoints.add(address, length); + if !state.write_watchpoints.is_empty() { + unsafe { vb_set_write_callback(self.sim, Some(on_write)) }; + unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) }; + } + } + + pub fn remove_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) { + match watch { + VBWatchpointType::Read => self.remove_read_watchpoint(address, length), + VBWatchpointType::Write => self.remove_write_watchpoint(address, length), + VBWatchpointType::Access => { + self.remove_read_watchpoint(address, length); + self.remove_write_watchpoint(address, length); + } + } + } + + fn remove_read_watchpoint(&mut self, address: u32, length: usize) { + let state = self.get_state(); + state.read_watchpoints.remove(address, length); + let needs_execute = state.needs_execute_callback(); + if state.read_watchpoints.is_empty() { + unsafe { vb_set_read_callback(self.sim, None) }; + if !needs_execute { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + } + } + + fn remove_write_watchpoint(&mut self, address: u32, length: usize) { + let state = self.get_state(); + state.write_watchpoints.remove(address, length); + let needs_execute = state.needs_execute_callback(); + if state.write_watchpoints.is_empty() { + unsafe { vb_set_write_callback(self.sim, None) }; + if !needs_execute { unsafe { vb_set_execute_callback(self.sim, None) }; } } @@ -393,13 +554,17 @@ impl Sim { let data = self.get_state(); data.step_from = None; data.breakpoints.clear(); + data.read_watchpoints.clear(); + data.write_watchpoints.clear(); + unsafe { vb_set_read_callback(self.sim, None) }; + unsafe { vb_set_write_callback(self.sim, None) }; unsafe { vb_set_execute_callback(self.sim, None) }; } pub fn stop_reason(&mut self) -> Option { let data = self.get_state(); let reason = data.stop_reason.take(); - if data.step_from.is_none() && data.breakpoints.is_empty() { + if !data.needs_execute_callback() { unsafe { vb_set_execute_callback(self.sim, None) }; } reason diff --git a/src/gdbserver.rs b/src/gdbserver.rs index ce49814..0c0393f 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -13,7 +13,9 @@ use tokio::{ sync::{mpsc, oneshot}, }; -use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId}; +use crate::emulator::{ + DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType, +}; mod registers; mod request; @@ -187,7 +189,7 @@ impl GdbConnection { } self.stop_reason = Some(reason); self.response() - .write_str(debug_stop_reason_string(self.stop_reason)) + .write_str(&debug_stop_reason_string(self.stop_reason)) } }; Some(res) @@ -251,7 +253,7 @@ impl GdbConnection { bail!("debug process was killed"); } else if req.match_str("?") { self.response() - .write_str(debug_stop_reason_string(self.stop_reason)) + .write_str(&debug_stop_reason_string(self.stop_reason)) } else if req.match_some_str(["c", "vCont;c:"]).is_some() { self.client .send_command(EmulatorCommand::DebugContinue(self.sim_id)); @@ -329,18 +331,68 @@ impl GdbConnection { } else { self.response() } - } else if req.match_str("Z0,") { - if let Some(address) = req.match_hex() { - self.client - .send_command(EmulatorCommand::AddBreakpoint(self.sim_id, address)); + } else if req.match_str("Z") { + let mut parse_request = || { + let type_ = req.match_hex::()?; + if !req.match_str(",") { + return None; + } + let address = req.match_hex()?; + if type_ == 0 || type_ == 1 { + return Some(EmulatorCommand::AddBreakpoint(self.sim_id, address)); + } + if !req.match_str(",") { + return None; + } + let length = req.match_hex()?; + let watch = match type_ { + 2 => VBWatchpointType::Write, + 3 => VBWatchpointType::Read, + 4 => VBWatchpointType::Access, + _ => return None, + }; + Some(EmulatorCommand::AddWatchpoint( + self.sim_id, + address, + length, + watch, + )) + }; + if let Some(command) = parse_request() { + self.client.send_command(command); self.response().write_str("OK") } else { self.response() } - } else if req.match_str("z0,") { - if let Some(address) = req.match_hex() { - self.client - .send_command(EmulatorCommand::RemoveBreakpoint(self.sim_id, address)); + } else if req.match_str("z") { + let mut parse_request = || { + let type_ = req.match_hex::()?; + if !req.match_str(",") { + return None; + } + let address = req.match_hex()?; + if type_ == 0 || type_ == 1 { + return Some(EmulatorCommand::RemoveBreakpoint(self.sim_id, address)); + } + if !req.match_str(",") { + return None; + } + let length = req.match_hex()?; + let watch = match type_ { + 2 => VBWatchpointType::Write, + 3 => VBWatchpointType::Read, + 4 => VBWatchpointType::Access, + _ => return None, + }; + Some(EmulatorCommand::RemoveWatchpoint( + self.sim_id, + address, + length, + watch, + )) + }; + if let Some(command) = parse_request() { + self.client.send_command(command); self.response().write_str("OK") } else { self.response() @@ -367,11 +419,38 @@ impl Drop for GdbConnection { } } -fn debug_stop_reason_string(reason: Option) -> &'static str { - match reason { - Some(DebugStopReason::Trace) => "T05;thread:p1.t1;threads:p1.t1;reason:trace;", - Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads:p1.t1;reason:breakpoint;", - Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;", - None => "T00;thread:p1.t1;threads:p1.t1;", +fn debug_stop_reason_string(reason: Option) -> String { + let mut result = String::new(); + result += if reason.is_some() { "T05;" } else { "T00;" }; + if let Some(DebugStopReason::Breakpoint) = reason { + result += "swbreak;"; } + if let Some(DebugStopReason::Watchpoint(watch, address)) = reason { + result += match watch { + VBWatchpointType::Write => "watch:", + VBWatchpointType::Read => "rwatch:", + VBWatchpointType::Access => "awatch:", + }; + result += &format!("{address:08x};"); + } + + result += "thread:p1.t1;threads:p1.t1;"; + + if let Some(reason) = reason { + result += "reason:"; + result += match reason { + DebugStopReason::Trace => "trace;", + DebugStopReason::Breakpoint => "breakpoint;", + DebugStopReason::Watchpoint(_, _) => "watchpoint;", + DebugStopReason::Trapped => "trap;", + }; + } + + if let Some(DebugStopReason::Watchpoint(_, address)) = reason { + result += "description:"; + result += &hex::encode(address.to_string()); + result += ";"; + } + + result }