-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement basic gdb server functionality.
This commit introduces basic gdbserver support to the zmu ARM simulator, enabling remote debugging with a gdb client. The following features are implemented: * Breakpoints: Users can set, clear, and manage breakpoints during program execution. * Continue: Execution can be resumed from the current breakpoint or paused state. * Step Instruction: Users can step through program instructions for detailed debugging. This functionality significantly enhances the debugging capabilities of zmu, making it more versatile for developers. To start the gdbserver just call zmu with the --gdb flag: $ zmu.exe run --gdb binary.elf A gdb server will be open on localhost port 9001 Signed-off-by: Diego Asanza <[email protected]>
- Loading branch information
Diego Asanza
committed
Jan 4, 2025
1 parent
5986099
commit 49ddf23
Showing
9 changed files
with
927 additions
and
135 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
use gdbstub::conn::{Connection, ConnectionExt}; | ||
use std::{ | ||
io::Read, net::{Shutdown, TcpListener, TcpStream}, str | ||
}; | ||
|
||
pub struct TcpConnection { | ||
stream: TcpStream, | ||
} | ||
|
||
impl TcpConnection { | ||
pub fn new_localhost(port: u16) -> Result<TcpConnection, &'static str> { | ||
let listener = TcpListener::bind(("127.0.0.1", port)).unwrap(); | ||
|
||
for stream in listener.incoming() { | ||
let stream = stream.unwrap(); | ||
stream.set_read_timeout(Some(std::time::Duration::from_millis(1))) | ||
.expect("set_read_timeout call failed"); | ||
// stream.set_nonblocking(true).expect("set_nonblocking call failed"); | ||
return Ok(TcpConnection { stream }); | ||
}; | ||
|
||
Err("could not accept socket connection") | ||
} | ||
} | ||
|
||
impl Drop for TcpConnection { | ||
fn drop(&mut self) { | ||
self.stream.shutdown(Shutdown::Both).expect("shutdown failed"); | ||
} | ||
} | ||
|
||
impl Connection for TcpConnection { | ||
type Error = &'static str; | ||
|
||
fn write(&mut self, b: u8) -> Result<(), &'static str> { | ||
match self.stream.write(b) { | ||
Ok(_) => Ok(()), | ||
Err(_) => Err("socket write failed") | ||
} | ||
} | ||
|
||
fn flush(&mut self) -> Result<(), &'static str> { | ||
match self.stream.flush() { | ||
Ok(_) => Ok(()), | ||
Err(_) => Err("socket flush failed") | ||
} | ||
} | ||
} | ||
|
||
impl ConnectionExt for TcpConnection { | ||
|
||
fn read(&mut self) -> std::result::Result<u8, Self::Error> { | ||
let mut buf: [u8; 1] = [0]; | ||
loop { | ||
match self.stream.read_exact(&mut buf) | ||
{ | ||
Ok(_) => break, | ||
Err(e) => match e.kind() { | ||
#[cfg(windows)] | ||
std::io::ErrorKind::TimedOut => continue, | ||
#[cfg(unix)] | ||
std::io::ErrorKind::WouldBlock => continue, | ||
_ => return Err("socket read failed") | ||
} | ||
} | ||
} | ||
Ok(buf[0]) | ||
} | ||
|
||
fn peek(&mut self) -> std::result::Result<Option<u8>, Self::Error> { | ||
let mut buf: [u8; 1] = [0]; | ||
loop { | ||
match self.stream.peek(&mut buf) | ||
{ | ||
Ok(_) => break, | ||
Err(e) => match e.kind() { | ||
#[cfg(windows)] | ||
std::io::ErrorKind::TimedOut => return Ok(None), | ||
#[cfg(unix)] | ||
std::io::ErrorKind::WouldBlock => return Ok(None), | ||
_ => { | ||
println!("peek error: {:?}", e); | ||
return Err("socket peek failed") | ||
} | ||
} | ||
} | ||
} | ||
Ok(Some(buf[0])) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
//! | ||
//! Gdb Module | ||
//! | ||
pub mod server; | ||
mod conn; | ||
mod simulation; | ||
mod target; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
//! | ||
//! Flash Memory simulation | ||
//! | ||
//! | ||
use gdbstub::stub::run_blocking; | ||
use gdbstub::stub::SingleThreadStopReason; | ||
use gdbstub::conn::Connection; | ||
use gdbstub::conn::ConnectionExt; | ||
use gdbstub::target::Target; | ||
use gdbstub::common::Signal; | ||
use gdbstub::stub::GdbStub; | ||
use gdbstub::stub::DisconnectReason; | ||
|
||
use crate::{MemoryMapConfig}; | ||
use crate::gdb::conn; | ||
use conn::TcpConnection; | ||
use crate::gdb::simulation::SimulationEvent; | ||
use crate::gdb::simulation::SimulationRunEvent; | ||
|
||
use crate::gdb::target::ZmuTarget; | ||
|
||
use crate::semihosting::SemihostingCommand; | ||
use crate::semihosting::SemihostingResponse; | ||
|
||
/// | ||
/// The gdb Server | ||
/// | ||
pub struct GdbServer { | ||
// number: i32, | ||
target: ZmuTarget, | ||
} | ||
|
||
/// | ||
impl GdbServer { | ||
|
||
/// | ||
pub fn new( | ||
code: &[u8], | ||
semihost_func: Box<dyn FnMut(&SemihostingCommand) -> SemihostingResponse + 'static>, | ||
map: Option<MemoryMapConfig>, | ||
flash_size: usize, | ||
) -> Result<GdbServer, &'static str> { | ||
|
||
let target = ZmuTarget::new(code, semihost_func, map, flash_size); | ||
|
||
Ok(GdbServer {target}) | ||
} | ||
|
||
/// | ||
pub fn start(&mut self) -> Result<u32, &'static str> { | ||
println!("Starting GDB server"); | ||
let mut exit_code = 0; | ||
let conn = match conn::TcpConnection::new_localhost(9001) { | ||
Ok(conn) => conn, | ||
Err(e) => return Err(e), | ||
}; | ||
|
||
let gdb = GdbStub::new(conn); | ||
|
||
match gdb.run_blocking::<EventLoop>(&mut self.target) { | ||
Ok(disconnect_reason) => match disconnect_reason { | ||
DisconnectReason::Disconnect => { | ||
println!("GDB client has disconnected. Running to completion..."); | ||
loop { | ||
match self.target.step() { | ||
SimulationEvent::Halted => break, | ||
SimulationEvent::Finalized(code) => { | ||
exit_code = code; | ||
break; | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
DisconnectReason::TargetExited(code) => { | ||
println!("\nTarget exited with code {}!", code) | ||
} | ||
DisconnectReason::TargetTerminated(sig) => { | ||
println!("\nTarget terminated with signal {}!", sig) | ||
} | ||
DisconnectReason::Kill => println!("\nGDB sent a kill command!"), | ||
}, | ||
Err(e) => { | ||
if e.is_target_error() { | ||
println!( | ||
"target encountered a fatal error: {}", | ||
e.into_target_error().unwrap() | ||
) | ||
} else if e.is_connection_error() { | ||
let (e, kind) = e.into_connection_error().unwrap(); | ||
println!("connection error: {:?} - {}", kind, e,) | ||
} else { | ||
println!("gdbstub encountered a fatal error: {}", e) | ||
} | ||
} | ||
} | ||
Ok(exit_code) | ||
} | ||
} | ||
|
||
|
||
enum EventLoop {} | ||
|
||
impl run_blocking::BlockingEventLoop for EventLoop { | ||
type Target = ZmuTarget; | ||
type Connection = TcpConnection; | ||
type StopReason = SingleThreadStopReason<u32>; | ||
|
||
#[allow(clippy::type_complexity)] | ||
fn wait_for_stop_reason( | ||
target: &mut ZmuTarget, | ||
conn: &mut Self::Connection, | ||
) -> Result< | ||
run_blocking::Event<SingleThreadStopReason<u32>>, | ||
run_blocking::WaitForStopReasonError< | ||
<Self::Target as Target>::Error, | ||
<Self::Connection as Connection>::Error, | ||
>, | ||
> { | ||
|
||
let poll_incoming_data = || { | ||
// gdbstub takes ownership of the underlying connection, so the `borrow_conn` | ||
// method is used to borrow the underlying connection back from the stub to | ||
// check for incoming data. | ||
conn.peek().map(|b| b.is_some()).unwrap_or(true) | ||
}; | ||
|
||
match target.run(poll_incoming_data) { | ||
SimulationRunEvent::IncomingData => { | ||
let byte = conn | ||
.read() | ||
.map_err(run_blocking::WaitForStopReasonError::Connection)?; | ||
Ok(run_blocking::Event::IncomingData(byte)) | ||
} | ||
SimulationRunEvent::Event(event) => { | ||
use gdbstub::target::ext::breakpoints::WatchKind; | ||
|
||
// translate emulator stop reason into GDB stop reason | ||
let stop_reason = match event { | ||
SimulationEvent::DoneStep => SingleThreadStopReason::DoneStep, | ||
SimulationEvent::Halted => SingleThreadStopReason::Terminated(Signal::SIGSTOP), | ||
SimulationEvent::Break => SingleThreadStopReason::SwBreak(()), | ||
SimulationEvent::WatchWrite(addr) => SingleThreadStopReason::Watch { | ||
tid: (), | ||
kind: WatchKind::Write, | ||
addr, | ||
}, | ||
SimulationEvent::WatchRead(addr) => SingleThreadStopReason::Watch { | ||
tid: (), | ||
kind: WatchKind::Read, | ||
addr, | ||
}, | ||
SimulationEvent::Finalized(exit_code) => SingleThreadStopReason::Exited(exit_code as u8), | ||
}; | ||
|
||
Ok(run_blocking::Event::TargetStopped(stop_reason)) | ||
} | ||
} | ||
} | ||
|
||
fn on_interrupt( | ||
_target: &mut ZmuTarget, | ||
) -> Result<Option<SingleThreadStopReason<u32>>, <ZmuTarget as Target>::Error> { | ||
// Because this emulator runs as part of the GDB stub loop, there isn't any | ||
// special action that needs to be taken to interrupt the underlying target. It | ||
// is implicitly paused whenever the stub isn't within the | ||
// `wait_for_stop_reason` callback. | ||
Ok(Some(SingleThreadStopReason::Signal(Signal::SIGINT))) | ||
} | ||
} |
Oops, something went wrong.