Skip to content

Commit 49ddf23

Browse files
author
Diego Asanza
committed
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]>
1 parent 5986099 commit 49ddf23

File tree

9 files changed

+927
-135
lines changed

9 files changed

+927
-135
lines changed

Cargo.lock

Lines changed: 206 additions & 135 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use zmu_cortex_m::Processor;
3939

4040
use zmu_cortex_m::system::simulation::simulate_trace;
4141
use zmu_cortex_m::system::simulation::{simulate, SimulationError};
42+
use zmu_cortex_m::gdb::server::GdbServer;
4243

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

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

127+
if gdb {
128+
let gdb = GdbServer::new(
129+
&flash_mem,
130+
semihost_func,
131+
if flash_start_address != 0 {
132+
Some(MemoryMapConfig::new(flash_start_address, 0, flash_size))
133+
} else {
134+
None
135+
},
136+
flash_size,
137+
);
138+
139+
let exit_code = gdb?.start().expect("GDB server failed");
140+
return Ok(exit_code);
141+
}
142+
125143
let statistics = if trace {
126144
debug!("Configuring tracing.");
127145

@@ -234,6 +252,7 @@ fn run(args: &ArgMatches) -> Result<u32> {
234252
run_matches.get_flag("trace"),
235253
trace_start,
236254
itm_output,
255+
run_matches.get_flag("gdb"),
237256
)?
238257
}
239258
Some((_, _)) => unreachable!(),
@@ -289,6 +308,13 @@ fn main() {
289308
.help("List of free arguments to pass to runtime as parameters")
290309
.index(2)
291310
.action(ArgAction::Append),
311+
)
312+
.arg(
313+
Arg::new("gdb")
314+
.action(ArgAction::SetTrue)
315+
.long("gdb")
316+
.help("Enable the gdb server")
317+
.num_args(0)
292318
),
293319
)
294320
.get_matches();

zmu_cortex_m/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ edition = "2021"
77
[dependencies]
88
byteorder = "1"
99
enum-set = "0.0.8"
10+
gdbstub = "0.7"
11+
gdbstub_arch = "0.3"
1012

1113

1214
[features]

zmu_cortex_m/src/gdb/conn.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use gdbstub::conn::{Connection, ConnectionExt};
2+
use std::{
3+
io::Read, net::{Shutdown, TcpListener, TcpStream}, str
4+
};
5+
6+
pub struct TcpConnection {
7+
stream: TcpStream,
8+
}
9+
10+
impl TcpConnection {
11+
pub fn new_localhost(port: u16) -> Result<TcpConnection, &'static str> {
12+
let listener = TcpListener::bind(("127.0.0.1", port)).unwrap();
13+
14+
for stream in listener.incoming() {
15+
let stream = stream.unwrap();
16+
stream.set_read_timeout(Some(std::time::Duration::from_millis(1)))
17+
.expect("set_read_timeout call failed");
18+
// stream.set_nonblocking(true).expect("set_nonblocking call failed");
19+
return Ok(TcpConnection { stream });
20+
};
21+
22+
Err("could not accept socket connection")
23+
}
24+
}
25+
26+
impl Drop for TcpConnection {
27+
fn drop(&mut self) {
28+
self.stream.shutdown(Shutdown::Both).expect("shutdown failed");
29+
}
30+
}
31+
32+
impl Connection for TcpConnection {
33+
type Error = &'static str;
34+
35+
fn write(&mut self, b: u8) -> Result<(), &'static str> {
36+
match self.stream.write(b) {
37+
Ok(_) => Ok(()),
38+
Err(_) => Err("socket write failed")
39+
}
40+
}
41+
42+
fn flush(&mut self) -> Result<(), &'static str> {
43+
match self.stream.flush() {
44+
Ok(_) => Ok(()),
45+
Err(_) => Err("socket flush failed")
46+
}
47+
}
48+
}
49+
50+
impl ConnectionExt for TcpConnection {
51+
52+
fn read(&mut self) -> std::result::Result<u8, Self::Error> {
53+
let mut buf: [u8; 1] = [0];
54+
loop {
55+
match self.stream.read_exact(&mut buf)
56+
{
57+
Ok(_) => break,
58+
Err(e) => match e.kind() {
59+
#[cfg(windows)]
60+
std::io::ErrorKind::TimedOut => continue,
61+
#[cfg(unix)]
62+
std::io::ErrorKind::WouldBlock => continue,
63+
_ => return Err("socket read failed")
64+
}
65+
}
66+
}
67+
Ok(buf[0])
68+
}
69+
70+
fn peek(&mut self) -> std::result::Result<Option<u8>, Self::Error> {
71+
let mut buf: [u8; 1] = [0];
72+
loop {
73+
match self.stream.peek(&mut buf)
74+
{
75+
Ok(_) => break,
76+
Err(e) => match e.kind() {
77+
#[cfg(windows)]
78+
std::io::ErrorKind::TimedOut => return Ok(None),
79+
#[cfg(unix)]
80+
std::io::ErrorKind::WouldBlock => return Ok(None),
81+
_ => {
82+
println!("peek error: {:?}", e);
83+
return Err("socket peek failed")
84+
}
85+
}
86+
}
87+
}
88+
Ok(Some(buf[0]))
89+
}
90+
}

zmu_cortex_m/src/gdb/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//!
2+
//! Gdb Module
3+
//!
4+
5+
pub mod server;
6+
mod conn;
7+
mod simulation;
8+
mod target;

zmu_cortex_m/src/gdb/server.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//!
2+
//! Flash Memory simulation
3+
//!
4+
//!
5+
6+
use gdbstub::stub::run_blocking;
7+
use gdbstub::stub::SingleThreadStopReason;
8+
use gdbstub::conn::Connection;
9+
use gdbstub::conn::ConnectionExt;
10+
use gdbstub::target::Target;
11+
use gdbstub::common::Signal;
12+
use gdbstub::stub::GdbStub;
13+
use gdbstub::stub::DisconnectReason;
14+
15+
use crate::{MemoryMapConfig};
16+
use crate::gdb::conn;
17+
use conn::TcpConnection;
18+
use crate::gdb::simulation::SimulationEvent;
19+
use crate::gdb::simulation::SimulationRunEvent;
20+
21+
use crate::gdb::target::ZmuTarget;
22+
23+
use crate::semihosting::SemihostingCommand;
24+
use crate::semihosting::SemihostingResponse;
25+
26+
///
27+
/// The gdb Server
28+
///
29+
pub struct GdbServer {
30+
// number: i32,
31+
target: ZmuTarget,
32+
}
33+
34+
///
35+
impl GdbServer {
36+
37+
///
38+
pub fn new(
39+
code: &[u8],
40+
semihost_func: Box<dyn FnMut(&SemihostingCommand) -> SemihostingResponse + 'static>,
41+
map: Option<MemoryMapConfig>,
42+
flash_size: usize,
43+
) -> Result<GdbServer, &'static str> {
44+
45+
let target = ZmuTarget::new(code, semihost_func, map, flash_size);
46+
47+
Ok(GdbServer {target})
48+
}
49+
50+
///
51+
pub fn start(&mut self) -> Result<u32, &'static str> {
52+
println!("Starting GDB server");
53+
let mut exit_code = 0;
54+
let conn = match conn::TcpConnection::new_localhost(9001) {
55+
Ok(conn) => conn,
56+
Err(e) => return Err(e),
57+
};
58+
59+
let gdb = GdbStub::new(conn);
60+
61+
match gdb.run_blocking::<EventLoop>(&mut self.target) {
62+
Ok(disconnect_reason) => match disconnect_reason {
63+
DisconnectReason::Disconnect => {
64+
println!("GDB client has disconnected. Running to completion...");
65+
loop {
66+
match self.target.step() {
67+
SimulationEvent::Halted => break,
68+
SimulationEvent::Finalized(code) => {
69+
exit_code = code;
70+
break;
71+
}
72+
_ => {}
73+
}
74+
}
75+
}
76+
DisconnectReason::TargetExited(code) => {
77+
println!("\nTarget exited with code {}!", code)
78+
}
79+
DisconnectReason::TargetTerminated(sig) => {
80+
println!("\nTarget terminated with signal {}!", sig)
81+
}
82+
DisconnectReason::Kill => println!("\nGDB sent a kill command!"),
83+
},
84+
Err(e) => {
85+
if e.is_target_error() {
86+
println!(
87+
"target encountered a fatal error: {}",
88+
e.into_target_error().unwrap()
89+
)
90+
} else if e.is_connection_error() {
91+
let (e, kind) = e.into_connection_error().unwrap();
92+
println!("connection error: {:?} - {}", kind, e,)
93+
} else {
94+
println!("gdbstub encountered a fatal error: {}", e)
95+
}
96+
}
97+
}
98+
Ok(exit_code)
99+
}
100+
}
101+
102+
103+
enum EventLoop {}
104+
105+
impl run_blocking::BlockingEventLoop for EventLoop {
106+
type Target = ZmuTarget;
107+
type Connection = TcpConnection;
108+
type StopReason = SingleThreadStopReason<u32>;
109+
110+
#[allow(clippy::type_complexity)]
111+
fn wait_for_stop_reason(
112+
target: &mut ZmuTarget,
113+
conn: &mut Self::Connection,
114+
) -> Result<
115+
run_blocking::Event<SingleThreadStopReason<u32>>,
116+
run_blocking::WaitForStopReasonError<
117+
<Self::Target as Target>::Error,
118+
<Self::Connection as Connection>::Error,
119+
>,
120+
> {
121+
122+
let poll_incoming_data = || {
123+
// gdbstub takes ownership of the underlying connection, so the `borrow_conn`
124+
// method is used to borrow the underlying connection back from the stub to
125+
// check for incoming data.
126+
conn.peek().map(|b| b.is_some()).unwrap_or(true)
127+
};
128+
129+
match target.run(poll_incoming_data) {
130+
SimulationRunEvent::IncomingData => {
131+
let byte = conn
132+
.read()
133+
.map_err(run_blocking::WaitForStopReasonError::Connection)?;
134+
Ok(run_blocking::Event::IncomingData(byte))
135+
}
136+
SimulationRunEvent::Event(event) => {
137+
use gdbstub::target::ext::breakpoints::WatchKind;
138+
139+
// translate emulator stop reason into GDB stop reason
140+
let stop_reason = match event {
141+
SimulationEvent::DoneStep => SingleThreadStopReason::DoneStep,
142+
SimulationEvent::Halted => SingleThreadStopReason::Terminated(Signal::SIGSTOP),
143+
SimulationEvent::Break => SingleThreadStopReason::SwBreak(()),
144+
SimulationEvent::WatchWrite(addr) => SingleThreadStopReason::Watch {
145+
tid: (),
146+
kind: WatchKind::Write,
147+
addr,
148+
},
149+
SimulationEvent::WatchRead(addr) => SingleThreadStopReason::Watch {
150+
tid: (),
151+
kind: WatchKind::Read,
152+
addr,
153+
},
154+
SimulationEvent::Finalized(exit_code) => SingleThreadStopReason::Exited(exit_code as u8),
155+
};
156+
157+
Ok(run_blocking::Event::TargetStopped(stop_reason))
158+
}
159+
}
160+
}
161+
162+
fn on_interrupt(
163+
_target: &mut ZmuTarget,
164+
) -> Result<Option<SingleThreadStopReason<u32>>, <ZmuTarget as Target>::Error> {
165+
// Because this emulator runs as part of the GDB stub loop, there isn't any
166+
// special action that needs to be taken to interrupt the underlying target. It
167+
// is implicitly paused whenever the stub isn't within the
168+
// `wait_for_stop_reason` callback.
169+
Ok(Some(SingleThreadStopReason::Signal(Signal::SIGINT)))
170+
}
171+
}

0 commit comments

Comments
 (0)