From c50ab1ecd8370fbebc92069aae901d049b9f7d5a Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Fri, 21 Feb 2020 14:04:39 +0200 Subject: [PATCH] feat: Add basic gdbserver feature Initially I began implementing the gdb protocol on my own, but I then found a nice work-in-progress crate on https://github.com/daniel5151/gdbstub that suited my needs. Former-commit-id: f77557cbbd8652c2ed05ac439efc1956d8e99729 --- Cargo.toml | 3 + README.md | 2 + src/core/arm7tdmi/cpu.rs | 15 +++-- src/core/gba.rs | 14 ++-- src/gdb.rs | 142 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ src/plat/sdl2/cli.yml | 7 +- src/plat/sdl2/main.rs | 40 +++++++++++ 8 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 src/gdb.rs diff --git a/Cargo.toml b/Cargo.toml index 79bd991..5771469 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ arrayvec = "0.5.1" rustyline = {version = "6.0.0", optional = true} nom = {version = "5.0.0", optional = true} +gdbstub = {git = "https://github.com/daniel5151/gdbstub.git", optional = true, features = ["std"], rev = "9686df6d74c4bf45cbc5746273b82640e8852b6d"} + [[bin]] name = "rba-sdl2" @@ -51,6 +53,7 @@ path = "src/plat/minifb/main.rs" [features] debugger = ["nom", "rustyline"] +gdb = ["gdbstub"] [profile.dev] opt-level = 0 diff --git a/README.md b/README.md index 295cff7..8a1b60e 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ Special key bindings | Key | Function | |-------------- |-------------------- | | Space (hold) | Disable 60fps cap | +| F1 | Custom debugger (requires --features debugger) | +| F2 | Spawn gdbserver (experimetnal, requires --features gdb) | | F5 | Save snapshot file | | F9 | Load snapshot file | diff --git a/src/core/arm7tdmi/cpu.rs b/src/core/arm7tdmi/cpu.rs index 5394f0a..43b4b2b 100644 --- a/src/core/arm7tdmi/cpu.rs +++ b/src/core/arm7tdmi/cpu.rs @@ -8,9 +8,10 @@ use std::fmt; pub use super::exception::Exception; use super::CpuAction; +#[cfg(feature = "debugger")] +use super::DecodedInstruction; use super::{ - arm::*, psr::RegPSR, thumb::ThumbInstruction, Addr, CpuMode, CpuState, DecodedInstruction, - InstructionDecoder, + arm::*, psr::RegPSR, thumb::ThumbInstruction, Addr, CpuMode, CpuState, InstructionDecoder, }; use crate::core::bus::Bus; @@ -34,6 +35,8 @@ pub struct Core { pub(super) bs_carry_out: bool, pipeline: [u32; 2], + + #[cfg(feature = "debugger")] pub last_executed: Option, pub cycles: usize, @@ -296,8 +299,8 @@ impl Core { #[cfg(feature = "debugger")] { self.gpr_previous = self.get_registers(); + self.last_executed = Some(DecodedInstruction::Arm(decoded_arm)); } - self.last_executed = Some(DecodedInstruction::Arm(decoded_arm)); let result = self.exec_arm(sb, decoded_arm); match result { CpuAction::AdvancePC => self.advance_arm(), @@ -310,8 +313,8 @@ impl Core { #[cfg(feature = "debugger")] { self.gpr_previous = self.get_registers(); + self.last_executed = Some(DecodedInstruction::Thumb(decoded_thumb)); } - self.last_executed = Some(DecodedInstruction::Thumb(decoded_thumb)); let result = self.exec_thumb(sb, decoded_thumb); match result { CpuAction::AdvancePC => self.advance_thumb(), @@ -320,7 +323,7 @@ impl Core { } #[inline] - pub(super) fn reload_pipeline16(&mut self, sb: &mut SysBus) { + pub fn reload_pipeline16(&mut self, sb: &mut SysBus) { self.pipeline[0] = sb.read_16(self.pc) as u32; self.N_cycle16(sb, self.pc); self.advance_thumb(); @@ -330,7 +333,7 @@ impl Core { } #[inline] - pub(super) fn reload_pipeline32(&mut self, sb: &mut SysBus) { + pub fn reload_pipeline32(&mut self, sb: &mut SysBus) { self.pipeline[0] = sb.read_32(self.pc); self.N_cycle16(sb, self.pc); self.advance_arm(); diff --git a/src/core/gba.rs b/src/core/gba.rs index e65c364..97cf2d7 100644 --- a/src/core/gba.rs +++ b/src/core/gba.rs @@ -19,11 +19,11 @@ pub struct GameBoyAdvance { pub sysbus: Box, pub cpu: arm7tdmi::Core, - video_device: Rc>, - audio_device: Rc>, - input_device: Rc>, + pub video_device: Rc>, + pub audio_device: Rc>, + pub input_device: Rc>, - cycles_to_next_event: usize, + pub cycles_to_next_event: usize, } #[derive(Serialize, Deserialize)] @@ -120,8 +120,8 @@ impl GameBoyAdvance { self.sysbus.io.gpu.skip_bios(); } - fn step_cpu(&mut self, io: &mut IoDevices) -> usize { - if io.intc.irq_pending() && self.cpu.last_executed.is_some() { + pub fn step_cpu(&mut self, io: &mut IoDevices) -> usize { + if io.intc.irq_pending() { self.cpu.irq(&mut self.sysbus); io.haltcnt = HaltState::Running; } @@ -131,7 +131,7 @@ impl GameBoyAdvance { } pub fn step(&mut self) { - // // I hate myself for doing this, but rust left me no choice. + // I hate myself for doing this, but rust left me no choice. let io = unsafe { let ptr = &mut *self.sysbus as *mut SysBus; &mut (*ptr).io as &mut IoDevices diff --git a/src/gdb.rs b/src/gdb.rs new file mode 100644 index 0000000..27fdb8a --- /dev/null +++ b/src/gdb.rs @@ -0,0 +1,142 @@ +use super::core::arm7tdmi::CpuState; +use super::core::interrupt::*; +use super::core::iodev::IoDevices; +use super::core::sysbus::SysBus; +use super::core::Bus; +use super::core::GameBoyAdvance; + +use byteorder::{LittleEndian, ReadBytesExt}; +use gdbstub::{Access, GdbStub, Target, TargetState}; + +use std::fmt; +use std::io::Cursor; +use std::net::{TcpListener, TcpStream, ToSocketAddrs}; + +pub type GdbServer = GdbStub; + +pub fn spawn_gdb_server( + addr: A, +) -> Result> { + info!("spawning gdbserver, listening on {}", addr); + + let sock = TcpListener::bind(addr)?; + let (stream, addr) = sock.accept()?; + info!("got connection from {}", addr); + + Ok(GdbServer::new(stream)) +} + +impl Target for GameBoyAdvance { + type Usize = u32; + type Error = (); + + fn step( + &mut self, + mut _log_mem_access: impl FnMut(Access), + ) -> Result { + static mut S_TOTAL_CYCLES: usize = 0; + + let io = unsafe { + let ptr = &mut *self.sysbus as *mut SysBus; + &mut (*ptr).io as &mut IoDevices + }; + + // clear any pending DMAs + let mut irqs = IrqBitmask(0); + while io.dmac.is_active() { + io.dmac.perform_work(&mut self.sysbus, &mut irqs); + } + io.intc.request_irqs(irqs); + + // run the CPU, ignore haltcnt + let cycles = self.step_cpu(io); + io.timers.update(cycles, &mut self.sysbus, &mut irqs); + + unsafe { + S_TOTAL_CYCLES += cycles; + } + + if self.cycles_to_next_event <= unsafe { S_TOTAL_CYCLES } { + let mut cycles_to_next_event = std::usize::MAX; + io.gpu.step( + unsafe { S_TOTAL_CYCLES }, + &mut self.sysbus, + &mut irqs, + &mut cycles_to_next_event, + &self.video_device, + ); + io.sound.update( + unsafe { S_TOTAL_CYCLES }, + &mut cycles_to_next_event, + &self.audio_device, + ); + self.cycles_to_next_event = cycles_to_next_event; + + unsafe { + S_TOTAL_CYCLES = 0; + }; + } else { + self.cycles_to_next_event -= unsafe { S_TOTAL_CYCLES }; + } + + io.intc.request_irqs(irqs); + + Ok(TargetState::Running) + } + + fn read_pc(&mut self) -> u32 { + self.cpu.get_next_pc() + } + + // read the specified memory addresses from the target + fn read_addrs(&mut self, addr: std::ops::Range, mut push_byte: impl FnMut(u8)) { + for addr in addr { + push_byte(self.sysbus.read_8(addr)) + } + } + + // write data to the specified memory addresses + fn write_addrs(&mut self, mut get_addr_val: impl FnMut() -> Option<(u32, u8)>) { + while let Some((addr, val)) = get_addr_val() { + self.sysbus.write_8(addr, val); + } + } + + fn read_registers(&mut self, mut push_reg: impl FnMut(&[u8])) { + // general purpose registers + for i in 0..15 { + push_reg(&self.cpu.get_reg(i).to_le_bytes()); + } + push_reg(&self.cpu.get_next_pc().to_le_bytes()); + // Floating point registers, unused + for _ in 0..25 { + push_reg(&[0, 0, 0, 0]); + } + push_reg(&self.cpu.cpsr.get().to_le_bytes()); + } + + fn write_registers(&mut self, regs: &[u8]) { + let mut rdr = Cursor::new(regs); + for i in 0..15 { + self.cpu.set_reg(i, rdr.read_u32::().unwrap()); + } + let new_pc = rdr.read_u32::().unwrap(); + self.cpu.set_reg(15, new_pc); + + self.cpu.cpsr.set(rdr.read_u32::().unwrap()); + + match self.cpu.cpsr.state() { + CpuState::ARM => self.cpu.reload_pipeline32(&mut self.sysbus), + CpuState::THUMB => self.cpu.reload_pipeline16(&mut self.sysbus), + }; + } + + fn target_description_xml() -> Option<&'static str> { + Some( + r#" + + armv4t + "#, + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index f35be30..95a887f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ pub mod util; pub mod core; pub mod disass; +#[cfg(feature = "gdb")] +pub mod gdb; + #[cfg(feature = "debugger")] pub mod debugger; @@ -56,6 +59,7 @@ pub trait InputInterface { pub mod prelude { pub use super::core::arm7tdmi; pub use super::core::cartridge::GamepakBuilder; + pub use super::core::Bus; pub use super::core::{GBAError, GBAResult, GameBoyAdvance}; #[cfg(feature = "debugger")] pub use super::debugger::Debugger; diff --git a/src/plat/sdl2/cli.yml b/src/plat/sdl2/cli.yml index 565e53e..0756d27 100644 --- a/src/plat/sdl2/cli.yml +++ b/src/plat/sdl2/cli.yml @@ -29,7 +29,12 @@ args: help: Skip running bios and start from the ROM instead - debug: long: debug - help: Start with the debugger attached + help: Use the custom debugger + - with_gdbserver: + long: with-gdbserver + help: Start with experimental gdbserver + conflicts_with: + - debug - script_file: long: script-file short: f diff --git a/src/plat/sdl2/main.rs b/src/plat/sdl2/main.rs index 31939d5..9d5d1e2 100644 --- a/src/plat/sdl2/main.rs +++ b/src/plat/sdl2/main.rs @@ -27,6 +27,9 @@ extern crate log; use flexi_logger; use flexi_logger::*; +#[cfg(feature = "gdb")] +use gdbstub; + mod audio; mod input; mod video; @@ -36,6 +39,8 @@ use input::create_input; use video::{create_video_interface, SCREEN_HEIGHT, SCREEN_WIDTH}; use rustboyadvance_ng::core::cartridge::BackupType; +#[cfg(feature = "gdb")] +use rustboyadvance_ng::gdb::spawn_gdb_server; use rustboyadvance_ng::prelude::*; use rustboyadvance_ng::util::FpsCounter; @@ -60,6 +65,29 @@ fn wait_for_rom(event_pump: &mut EventPump) -> String { } } +fn spawn_and_run_gdb_server(gba: &mut GameBoyAdvance) -> Result<(), Box> { + #[cfg(feature = "gdb")] + { + let mut gdb = spawn_gdb_server(format!("localhost:{}", 1337))?; + let result = match gdb.run(gba) { + Ok(state) => { + info!("Disconnected from GDB. Target state: {:?}", state); + Ok(()) + } + Err(gdbstub::Error::TargetError(e)) => Err(e), + Err(e) => return Err(e.into()), + }; + + info!("Debugger session ended, result={:?}", result); + } + #[cfg(not(feature = "gdb"))] + { + error!("Please compile me with 'gdb' feature") + } + + Ok(()) +} + fn main() -> Result<(), Box> { fs::create_dir_all(LOG_DIR).expect(&format!("could not create log directory ({})", LOG_DIR)); flexi_logger::Logger::with_env_or_str("info") @@ -78,6 +106,7 @@ fn main() -> Result<(), Box> { let skip_bios = matches.occurrences_of("skip_bios") != 0; let debug = matches.occurrences_of("debug") != 0; + let with_gdbserver = matches.occurrences_of("with_gdbserver") != 0; info!("Initializing SDL2 context"); let sdl_context = sdl2::init().expect("failed to initialize sdl2"); @@ -161,6 +190,10 @@ fn main() -> Result<(), Box> { } } + if with_gdbserver { + spawn_and_run_gdb_server(&mut gba)?; + } + let mut fps_counter = FpsCounter::default(); let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60); 'running: loop { @@ -192,6 +225,13 @@ fn main() -> Result<(), Box> { info!("ending debugger..."); break; } + #[cfg(feature = "gdb")] + Event::KeyUp { + keycode: Some(Keycode::F2), + .. + } => { + spawn_and_run_gdb_server(&mut gba)?; + } Event::KeyUp { keycode: Some(Keycode::F5), ..