Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/debugger/window.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ PageUp/PageDown by whole pages. Four buttons sit above the dump:

- **Find** searches CPU-visible memory for the `$` box's hex byte pattern
(`4E75`, or spaced pairs like `C0 FF EE`), starting past the previous
hit and wrapping the 24-bit space once. The view jumps to the match.
hit and wrapping the decoded memory map once. The sweep covers every RAM
bank the machine decodes -- chip, slow, Zorro II, and on a 32-bit CPU the
motherboard (`$04000000`), CPU-slot (`$08000000`), and Zorro III banks --
plus the Kickstart and extended-ROM windows. The view jumps to the match.
- **Save...** writes the `$` box's `ADDR LEN` (hex) region to a file via
a save dialog -- the GUI counterpart of `COPPERLINE_DBG_RAMDUMP`.
- **Writer?** reports the last instruction that wrote the word at the
Expand Down
100 changes: 87 additions & 13 deletions src/amigaos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ pub fn task_state_name(state: u8) -> &'static str {
}

/// Heuristic: even and in an address range where exec structures can
/// live -- chip+Z2 RAM, slow RAM, or Zorro III space (OS 3.2+ SetPatch
/// moves ExecBase to fast RAM, which may be a Z3 board).
fn plausible_ptr(addr: u32) -> bool {
/// live -- chip+Z2 RAM, slow RAM, the big-box motherboard and CPU-slot
/// fast-RAM windows, or Zorro III space (OS 3.2+ SetPatch moves ExecBase
/// to fast RAM, which on a big-box machine is well above the 24-bit space).
pub(crate) fn plausible_ptr(addr: u32) -> bool {
addr & 1 == 0
&& ((0x100..0x00A0_0000).contains(&addr) // chip + Z2 fast
|| (0x00C0_0000..0x00D8_0000).contains(&addr) // slow
Expand All @@ -110,6 +111,14 @@ fn plausible_ptr(addr: u32) -> bool {
|| (0x1000_0000..0x8000_0000).contains(&addr)) // Zorro III
}

/// The same test for a BPTR (an address shifted right by two, as AmigaDOS
/// stores pointers). The pointed-at address must be longword aligned, so
/// the shift is exact; BPTRs past a quarter of the address space cannot
/// name a real address at all and are rejected before the shift.
fn plausible_bptr(bptr: u32) -> bool {
bptr < 0x4000_0000 && plausible_ptr(bptr << 2)
}

/// Read-only view of guest memory, built from the debugger's peek
/// primitives. `peek8`/`peek32` must be side-effect-free.
pub struct OsMemory<'a> {
Expand Down Expand Up @@ -240,14 +249,14 @@ impl OsMemory<'_> {
return None;
}
let cli = (self.peek32)(task.wrapping_add(PR_CLI));
if cli != 0 && cli < 0x0040_0000 {
if plausible_bptr(cli) {
let module = (self.peek32)((cli << 2).wrapping_add(CLI_MODULE));
if module != 0 {
return Some(module);
}
}
let array = (self.peek32)(task.wrapping_add(PR_SEGLIST));
if array == 0 || array >= 0x0040_0000 {
if !plausible_bptr(array) {
return None;
}
let count = (self.peek32)(array << 2);
Expand All @@ -263,11 +272,8 @@ impl OsMemory<'_> {
/// size longword; the payload follows the next pointer.
pub fn walk_seglist(&self, mut bptr: u32) -> Vec<Segment> {
let mut out = Vec::new();
while bptr != 0 && bptr < 0x0040_0000 && out.len() < 64 {
while plausible_bptr(bptr) && out.len() < 64 {
let addr = bptr << 2;
if !(4..0x0100_0000).contains(&addr) {
break;
}
let size = (self.peek32)(addr.wrapping_sub(4));
let next = (self.peek32)(addr);
out.push(Segment {
Expand Down Expand Up @@ -295,7 +301,7 @@ impl OsMemory<'_> {
/// Read a BSTR (BPTR to a length-prefixed string): printable ASCII,
/// bounded by `cap`, empty for implausible pointers.
pub fn read_bstr(&self, bptr: u32, cap: usize) -> String {
if bptr == 0 || bptr >= 0x0040_0000 {
if !plausible_bptr(bptr) {
return String::new();
}
let addr = bptr << 2;
Expand All @@ -317,7 +323,7 @@ impl OsMemory<'_> {
pub fn process_command_name(&self, task: u32) -> String {
if (self.peek8)(task.wrapping_add(LN_TYPE)) == NT_PROCESS {
let cli = (self.peek32)(task.wrapping_add(PR_CLI));
if cli != 0 && cli < 0x0040_0000 {
if plausible_bptr(cli) {
let name_bptr = (self.peek32)((cli << 2).wrapping_add(CLI_COMMAND_NAME));
// A full AmigaDOS path fits in a 255-byte BSTR.
let name = self.read_bstr(name_bptr, 255);
Expand Down Expand Up @@ -356,8 +362,10 @@ pub fn command_basename(path: &str) -> &str {
}

/// A loaded hunk must sit in RAM exec could have allocated and carry a
/// believable size (the loader's size longword is bounded by the 16MB
/// chip/Z2 space a seglist can live in).
/// believable size. The 16 MiB size ceiling is a sanity bound on the
/// loader's size longword, not an address-space limit: the hunk itself may
/// sit anywhere `plausible_ptr` accepts, including the 32-bit fast-RAM
/// windows of a big-box machine.
fn segment_plausible(seg: &Segment) -> bool {
plausible_ptr(seg.start) && seg.size < 0x0100_0000
}
Expand Down Expand Up @@ -890,4 +898,70 @@ mod tests {
let err = os(&peek8, &peek32).exec_base().unwrap_err();
assert!(err.contains("ChkBase"), "{err}");
}

/// Exec structures live wherever exec allocated them. On a big-box
/// machine that is the Ramsey motherboard bank ($04000000-$07FFFFFF),
/// the CPU-slot accelerator bank ($08000000-$0FFFFFFF), or a Zorro III
/// board -- all beyond the 24-bit space the small-box map fits in.
#[test]
fn plausible_pointers_cover_the_32_bit_ram_windows() {
for addr in [
0x0000_1000, // chip
0x0020_0000, // Zorro II fast
0x00C0_0000, // slow
0x0400_0000, // motherboard (Ramsey), bottom of the window
0x07FF_FFFE, // motherboard, top of the window
0x0800_0000, // CPU slot, bottom
0x0FFF_FFFE, // CPU slot, top
0x1000_0000, // Zorro III, bottom
0x4000_0000, // Zorro III
] {
assert!(plausible_ptr(addr), "${addr:08X} should be plausible");
assert!(
plausible_bptr(addr >> 2),
"BPTR ${:08X} should be plausible",
addr >> 2
);
}
// Still rejected: odd addresses, the zero page, the custom-register
// and ROM spaces, and anything past the Zorro III window.
for addr in [
0x0000_0001,
0x0000_0080,
0x00DF_F000,
0x00F8_0000,
0x8000_0000u32,
] {
assert!(!plausible_ptr(addr), "${addr:08X} should be rejected");
}
}

/// A seglist walked from a hunk in motherboard RAM: the BPTR bound
/// must follow the same address windows, not a 16 MiB ceiling.
#[test]
fn walks_a_seglist_loaded_above_the_24_bit_space() {
let hunk = 0x0450_0000u32;
let mut mem = FakeMem::new();
mem.put32(hunk - 4, 0x100); // loader size longword
mem.put32(hunk, 0); // end of list
let peek8 = |a: u32| mem.peek8(a);
let peek32 = |a: u32| mem.peek32(a);
let segs = os(&peek8, &peek32).walk_seglist(hunk >> 2);
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].start, hunk + 4);
assert!(segment_plausible(&segs[0]));
}

/// The same for a BSTR: cli_CommandName points into whatever RAM the
/// shell was loaded into.
#[test]
fn reads_a_bstr_above_the_24_bit_space() {
let bstr = 0x0900_0000u32;
let mut mem = FakeMem::new();
mem.put8(bstr, 3);
mem.put_str(bstr + 1, "Dir");
let peek8 = |a: u32| mem.peek8(a);
let peek32 = |a: u32| mem.peek32(a);
assert_eq!(os(&peek8, &peek32).read_bstr(bstr >> 2, 255), "Dir");
}
}
31 changes: 31 additions & 0 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5477,6 +5477,37 @@ impl Bus {
regions
}

/// The regions a debugger pattern search should sweep: every writable
/// RAM bank plus the Kickstart and extended-ROM windows, in ascending
/// address order. Sweeping the decoded map rather than a fixed
/// address span is what lets a search reach RAM above the 24-bit
/// space (motherboard, CPU-slot, and Zorro III banks) without walking
/// the gigabytes of undecoded address space between them.
///
/// Chip RAM is offered as the whole $000000-$1FFFFF select window
/// rather than the fitted bank: Agnus decodes fewer address bits than
/// the window on the smaller parts, so the bank repeats inside it and
/// a search of CPU-visible memory sees every image.
pub fn searchable_regions(&self) -> Vec<(u32, u32)> {
let mut regions = self.writable_ram_regions();
for (base, len) in regions.iter_mut() {
if *base == crate::memory::CHIP_RAM_BASE as u32 {
*len = crate::memory::CHIP_WINDOW_SIZE as u32;
}
}
if !self.mem.rom.is_empty() {
regions.push((crate::memory::ROM_BASE as u32, self.mem.rom.len() as u32));
}
if !self.mem.extended_rom.is_empty() {
regions.push((
self.mem.extended_rom_base as u32,
self.mem.extended_rom.len() as u32,
));
}
regions.sort_by_key(|(base, _)| *base);
regions
}

/// Read a 16-bit big-endian word from whichever RAM/ROM region maps
/// `addr` (chip, fast, slow, motherboard, accelerator, or ROM), for the
/// debugger's memory dumps. Returns 0 for unmapped addresses.
Expand Down
35 changes: 35 additions & 0 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10897,3 +10897,38 @@ fn motherboard_ram_reaches_the_debugger_helpers() {
.writable_ram_regions()
.contains(&(base, 1024 * 1024_u32)));
}

/// A debugger pattern search sweeps the decoded map, so it must offer the
/// RAM banks that sit past the 24-bit space -- the Ramsey motherboard
/// bank, the CPU-slot accelerator bank, and Zorro III boards -- alongside
/// the chip window and the ROMs. Chip RAM is offered as the whole
/// $000000-$1FFFFF select window so the Agnus image repeats are searched.
#[test]
fn searchable_regions_cover_the_32_bit_ram_banks() {
let mut bus = empty_bus();
bus.mem.fit_mb_ram(4 * 1024 * 1024);
bus.mem.fit_accel_ram(8 * 1024 * 1024);
bus.mem
.zorro
.add_board_configured_at(
crate::zorro::BoardSpec::z3_ram(16 * 1024 * 1024),
0x1000_0000,
)
.expect("Z3 RAM board");

let regions = bus.searchable_regions();
assert!(regions.contains(&(0, crate::memory::CHIP_WINDOW_SIZE as u32)));
assert!(regions.contains(&(0x07C0_0000, 4 * 1024 * 1024)));
assert!(regions.contains(&(crate::memory::ACCEL_RAM_BASE as u32, 8 * 1024 * 1024)));
assert!(
regions
.iter()
.any(|(base, len)| *base >= 0x1000_0000 && *len == 16 * 1024 * 1024),
"Zorro III RAM board missing: {regions:08X?}"
);
assert!(regions.contains(&(crate::memory::ROM_BASE as u32, 512 * 1024)));
assert!(
regions.windows(2).all(|w| w[0].0 <= w[1].0),
"regions must be in ascending address order: {regions:08X?}"
);
}
5 changes: 3 additions & 2 deletions src/control/headless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,8 @@ impl Session {
// Target already met (or met exactly at the last quantum)?
match target {
Some(RunTarget::Pc(pc))
if self.emu.machine.pc() & 0x00FF_FFFF == pc & 0x00FF_FFFF =>
if self.emu.machine.pc() & self.emu.machine.ui_addr_mask()
== pc & self.emu.machine.ui_addr_mask() =>
{
return finish("target", format!("pc ${pc:06X}"), extra_ids, false);
}
Expand All @@ -638,7 +639,7 @@ impl Session {
// debug_step_for_gdb keeps reverse-debug captures
// happening at frame crossings. The hit itself is
// seen by the checks at the top of the loop.
let mask = 0x00FF_FFFF;
let mask = self.emu.machine.ui_addr_mask();
for _ in 0..PC_POLL_CHUNK {
self.emu.debug_step_for_gdb(&mut cpu_idle)?;
if self.emu.machine.pc() & mask == pc & mask
Expand Down
9 changes: 6 additions & 3 deletions src/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1674,11 +1674,11 @@ impl M68kMachine {
| u32::from(self.bus.bus.peek_word_any(addr.wrapping_add(2)))
};
let execbase = peek32(4);
if execbase == 0 || execbase & 1 != 0 || execbase >= 0x0100_0000 {
if !crate::amigaos::plausible_ptr(execbase) {
return None;
}
let task = peek32(execbase.wrapping_add(0x114));
(task != 0 && task & 1 == 0 && task < 0x0100_0000).then_some(task)
crate::amigaos::plausible_ptr(task).then_some(task)
}

/// The name a task node points at, uppercased for matching.
Expand All @@ -1688,7 +1688,10 @@ impl M68kMachine {
| u32::from(self.bus.bus.peek_word_any(addr.wrapping_add(2)))
};
let name_ptr = peek32(task.wrapping_add(10));
if name_ptr == 0 || name_ptr >= 0x0100_0000 {
// Names may live in ROM or at odd addresses (the ROM devices' node
// names are Kickstart strings), so no plausible_ptr bound here;
// unmapped pointers read as zero bytes and end the string at once.
if name_ptr == 0 {
return String::new();
}
let mut name = String::new();
Expand Down
17 changes: 10 additions & 7 deletions src/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,16 +978,19 @@ impl Emulator {
end_pos: u64,
full: bool,
) -> Result<Option<(u64, String)>> {
const PC_MASK: u32 = 0x00FF_FFFF;
let is_bp = |pc: u32| breakpoints.contains(&(pc & PC_MASK));
// A0-A23 on the 24-bit models, the full 32 bits on 020+, so a
// breakpoint in RAM above the 24-bit space is not aliased onto a
// different PC during replay.
let pc_mask = self.machine.ui_addr_mask();
let is_bp = |pc: u32| breakpoints.contains(&(pc & pc_mask));
// Drain any stop left over from an earlier replay segment.
let _ = self.machine.take_ui_debug_stop();
self.tt_apply_due_input(self.retired_instructions);
let mut best: Option<(u64, String)> = None;
if self.retired_instructions < end_pos && is_bp(self.machine.pc()) {
best = Some((
self.retired_instructions,
format!("Breakpoint at ${:06X}", self.machine.pc() & PC_MASK),
format!("Breakpoint at ${:06X}", self.machine.pc() & pc_mask),
));
}
let mut cpu_idle = false;
Expand All @@ -1006,7 +1009,7 @@ impl Emulator {
if self.retired_instructions < end_pos && is_bp(self.machine.pc()) {
best = Some((
self.retired_instructions,
format!("Breakpoint at ${:06X}", self.machine.pc() & PC_MASK),
format!("Breakpoint at ${:06X}", self.machine.pc() & pc_mask),
));
}
if self.retired_instructions == before && !cpu_idle {
Expand Down Expand Up @@ -1294,11 +1297,11 @@ impl Emulator {
/// Run until the CPU reaches `target_pc` (masked to the bus width), up
/// to `max_instructions`. Returns true when the target was hit.
pub fn debug_run_to_pc(&mut self, target_pc: u32, max_instructions: usize) -> Result<bool> {
const PC_MASK: u32 = 0x00FF_FFFF;
let target = target_pc & PC_MASK;
let pc_mask = self.machine.ui_addr_mask();
let target = target_pc & pc_mask;
for _ in 0..max_instructions {
self.debug_step_one_with_idle()?;
if self.machine.pc() & PC_MASK == target {
if self.machine.pc() & pc_mask == target {
return Ok(true);
}
// A breakpoint/watch hit on the way to the target ends the
Expand Down
8 changes: 5 additions & 3 deletions src/video/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,16 +403,18 @@ impl DebuggerPanel {
.collect()
}

/// Region spec for Save region: "ADDR LEN", both hex, length bounded
/// to the 24-bit bus.
/// Region spec for Save region: "ADDR LEN", both hex. The address is
/// taken as written -- a dump can start anywhere the CPU decodes,
/// including the motherboard, CPU-slot, and Zorro III RAM above the
/// 24-bit space -- and only the length is capped, at 16 MiB per dump.
pub fn region_spec(&self) -> Option<(u32, u32)> {
let mut tokens = self.entry.split_whitespace();
let addr = parse_hex_u32(tokens.next()?)?;
let len = parse_hex_u32(tokens.next()?)?;
if tokens.next().is_some() || len == 0 || len > 0x0100_0000 {
return None;
}
Some((addr & 0x00FF_FFFF, len))
Some((addr, len))
Comment thread
LinuxJedi marked this conversation as resolved.
}

pub fn push_entry_char(&mut self, ch: char) {
Expand Down
Loading
Loading