Skip to content

Commit

Permalink
Implement basic gdb server functionality.
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 9 changed files with 927 additions and 135 deletions.
341 changes: 206 additions & 135 deletions Cargo.lock

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use zmu_cortex_m::Processor;

use zmu_cortex_m::system::simulation::simulate_trace;
use zmu_cortex_m::system::simulation::{simulate, SimulationError};
use zmu_cortex_m::gdb::server::GdbServer;

mod errors {
// Create the Error, ErrorKind, ResultExt, and Result types
Expand All @@ -59,6 +60,7 @@ fn run_bin(
trace: bool,
option_trace_start: Option<u64>,
itm_file: Option<Box<dyn io::Write + 'static>>,
gdb: bool,
) -> Result<u32> {
let res = Object::parse(buffer).unwrap();

Expand Down Expand Up @@ -122,6 +124,22 @@ fn run_bin(
let trace_start = option_trace_start.unwrap_or(0);
let semihost_func = Box::new(get_semihost_func(Instant::now()));

if gdb {
let gdb = GdbServer::new(
&flash_mem,
semihost_func,
if flash_start_address != 0 {
Some(MemoryMapConfig::new(flash_start_address, 0, flash_size))
} else {
None
},
flash_size,
);

let exit_code = gdb?.start().expect("GDB server failed");
return Ok(exit_code);
}

let statistics = if trace {
debug!("Configuring tracing.");

Expand Down Expand Up @@ -234,6 +252,7 @@ fn run(args: &ArgMatches) -> Result<u32> {
run_matches.get_flag("trace"),
trace_start,
itm_output,
run_matches.get_flag("gdb"),
)?
}
Some((_, _)) => unreachable!(),
Expand Down Expand Up @@ -289,6 +308,13 @@ fn main() {
.help("List of free arguments to pass to runtime as parameters")
.index(2)
.action(ArgAction::Append),
)
.arg(
Arg::new("gdb")
.action(ArgAction::SetTrue)
.long("gdb")
.help("Enable the gdb server")
.num_args(0)
),
)
.get_matches();
Expand Down
2 changes: 2 additions & 0 deletions zmu_cortex_m/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ edition = "2021"
[dependencies]
byteorder = "1"
enum-set = "0.0.8"
gdbstub = "0.7"
gdbstub_arch = "0.3"


[features]
Expand Down
90 changes: 90 additions & 0 deletions zmu_cortex_m/src/gdb/conn.rs
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]))
}
}
8 changes: 8 additions & 0 deletions zmu_cortex_m/src/gdb/mod.rs
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;
171 changes: 171 additions & 0 deletions zmu_cortex_m/src/gdb/server.rs
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)))
}
}
Loading

0 comments on commit 49ddf23

Please sign in to comment.