Expand description
An ergonomic, featureful, and easy-to-integrate implementation of the GDB
Remote Serial Protocol
in Rust, with no-compromises #![no_std]
support.
§Feature flags
By default, both the std
and alloc
features are enabled.
When using gdbstub
in #![no_std]
contexts, make sure to set
default-features = false
.
alloc
- Implement
Connection
forBox<dyn Connection>
. - Log outgoing packets via
log::trace!
(uses a heap-allocated output buffer). - Provide built-in implementations for certain protocol features:
- Use a heap-allocated packet buffer in
GdbStub
(if none is provided viaGdbStubBuilder::with_packet_buffer
). - (Monitor Command) Use a heap-allocated output buffer in
ConsoleOutput
.
- Use a heap-allocated packet buffer in
- Implement
std
(impliesalloc
)- Implement
Connection
forTcpStream
andUnixStream
. - Implement
std::error::Error
forgdbstub::Error
. - Add a
TargetError::Io
variant to simplifystd::io::Error
handling from Target methods.
- Implement
paranoid_unsafe
- Please refer to the
unsafe
ingdbstub
section of the README.md for more details.
- Please refer to the
§Getting Started
This section provides a brief overview of the key traits and types used in
gdbstub
, and walks though the basic steps required to integrate gdbstub
into a project.
At a high level, there are only three things that are required to get up and
running with gdbstub
: a Connection
, a
Target
, and a event loop.
Note: I highly recommended referencing some of the examples listed in the project README when integrating
gdbstub
into a project for the first time.
In particular, the in-tree
armv4t
example contains basic implementations off almost all protocol extensions, making it an incredibly valuable reference when implementing protocol extensions.
§The Connection
Trait
First things first: gdbstub
needs some way to communicate with a GDB
client. To facilitate this communication, gdbstub
uses a custom
Connection
trait.
Connection
is automatically implemented for common std
types such as
TcpStream
and
UnixStream
.
If you’re using gdbstub
in a #![no_std]
environment, Connection
will
most likely need to be manually implemented on top of whatever in-order,
serial, byte-wise I/O your particular platform has available (e.g:
putchar/getchar over UART, using an embedded TCP stack, etc.).
One common way to start a remote debugging session is to simply wait for a GDB client to connect via TCP:
use std::io;
use std::net::{TcpListener, TcpStream};
fn wait_for_gdb_connection(port: u16) -> io::Result<TcpStream> {
let sockaddr = format!("localhost:{}", port);
eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);
let sock = TcpListener::bind(sockaddr)?;
let (stream, addr) = sock.accept()?;
// Blocks until a GDB client connects via TCP.
// i.e: Running `target remote localhost:<port>` from the GDB prompt.
eprintln!("Debugger connected from {}", addr);
Ok(stream) // `TcpStream` implements `gdbstub::Connection`
}
§The Target
Trait
The Target
trait describes how to control and modify a
system’s execution state during a GDB debugging session, and serves as the
primary bridge between gdbstub
’s generic GDB protocol implementation and a
specific target’s project/platform-specific code.
At a high level, the Target
trait is a collection of user-defined handler
methods that the GDB client can invoke via the GDB remote serial protocol.
For example, the Target
trait includes methods to read/write
registers/memory, start/stop execution, etc…
Target
is the most important trait in gdbstub
, and must be implemented
by anyone integrating gdbstub
into their project!
Please refer to the target
module documentation for in-depth
instructions on how to implement Target
for a particular
platform.
§The Event Loop
Once a Connection
has been established and the
Target
has been initialized, all that’s left is to
wire things up and decide what kind of event loop will be used to drive the
debugging session!
First things first, let’s get an instance of GdbStub
ready to run:
// Set-up a valid `Target`
let mut target = MyTarget::new()?; // implements `Target`
// Establish a `Connection`
let connection: TcpStream = wait_for_gdb_connection(9001);
// Create a new `gdbstub::GdbStub` using the established `Connection`.
let mut debugger = gdbstub::GdbStub::new(connection);
Cool, but how do you actually start the debugging session?
§GdbStub::run_blocking
: The quick and easy way to get up and running with gdbstub
If you’ve got an extra thread to spare, the quickest way to get up and
running with gdbstub
is by using the
GdbStub::run_blocking
API alongside the
BlockingEventLoop
trait.
If you are on a more resource constrained platform, and/or don’t wish to
dedicate an entire thread to gdbstub
, feel free to skip ahead to the
following
section.
A basic integration of gdbstub
into a project using the
GdbStub::run_blocking
API might look something like this:
use gdbstub::common::Signal;
use gdbstub::conn::{Connection, ConnectionExt}; // note the use of `ConnectionExt`
use gdbstub::stub::{run_blocking, DisconnectReason, GdbStub};
use gdbstub::stub::SingleThreadStopReason;
use gdbstub::target::Target;
enum MyGdbBlockingEventLoop {}
// The `run_blocking::BlockingEventLoop` groups together various callbacks
// the `GdbStub::run_blocking` event loop requires you to implement.
impl run_blocking::BlockingEventLoop for MyGdbBlockingEventLoop {
type Target = MyTarget;
type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
// or MultiThreadStopReason on multi threaded targets
type StopReason = SingleThreadStopReason<u32>;
// Invoked immediately after the target's `resume` method has been
// called. The implementation should block until either the target
// reports a stop reason, or if new data was sent over the connection.
fn wait_for_stop_reason(
target: &mut MyTarget,
conn: &mut Self::Connection,
) -> Result<
run_blocking::Event<SingleThreadStopReason<u32>>,
run_blocking::WaitForStopReasonError<
<Self::Target as Target>::Error,
<Self::Connection as Connection>::Error,
>,
> {
// the specific mechanism to "select" between incoming data and target
// events will depend on your project's architecture.
//
// some examples of how you might implement this method include: `epoll`,
// `select!` across multiple event channels, periodic polling, etc...
//
// in this example, lets assume the target has a magic method that handles
// this for us.
let event = match target.run_and_check_for_incoming_data(conn) {
MyTargetEvent::IncomingData => {
let byte = conn
.read() // method provided by the `ConnectionExt` trait
.map_err(run_blocking::WaitForStopReasonError::Connection)?;
run_blocking::Event::IncomingData(byte)
}
MyTargetEvent::StopReason(reason) => {
run_blocking::Event::TargetStopped(reason)
}
};
Ok(event)
}
// Invoked when the GDB client sends a Ctrl-C interrupt.
fn on_interrupt(
target: &mut MyTarget,
) -> Result<Option<SingleThreadStopReason<u32>>, <MyTarget as Target>::Error> {
// notify the target that a ctrl-c interrupt has occurred.
target.stop_in_response_to_ctrl_c_interrupt()?;
// a pretty typical stop reason in response to a Ctrl-C interrupt is to
// report a "Signal::SIGINT".
Ok(Some(SingleThreadStopReason::Signal(Signal::SIGINT).into()))
}
}
fn gdb_event_loop_thread(
debugger: GdbStub<MyTarget, Box<dyn ConnectionExt<Error = std::io::Error>>>,
mut target: MyTarget
) {
match debugger.run_blocking::<MyGdbBlockingEventLoop>(&mut target) {
Ok(disconnect_reason) => match disconnect_reason {
DisconnectReason::Disconnect => {
println!("Client disconnected")
}
DisconnectReason::TargetExited(code) => {
println!("Target exited with code {}", code)
}
DisconnectReason::TargetTerminated(sig) => {
println!("Target terminated with signal {}", sig)
}
DisconnectReason::Kill => println!("GDB 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)
}
}
}
}
§GdbStubStateMachine
: Driving gdbstub
in an async event loop / via interrupt handlers
GdbStub::run_blocking
requires that the target implement the
BlockingEventLoop
trait, which as the name implies, uses blocking IO
when handling certain events. Blocking the thread is a totally reasonable
approach in most implementations, as one can simply spin up a separate
thread to run the GDB stub (or in certain emulator implementations, run the
emulator as part of the wait_for_stop_reason
method).
Unfortunately, this blocking behavior can be a non-starter when integrating
gdbstub
in projects that don’t support / wish to avoid the traditional
thread-based execution model, such as projects using async/await
, or
bare-metal no_std
projects running on embedded hardware.
In these cases, gdbstub
provides access to the underlying
GdbStubStateMachine
API, which gives implementations full control over
the GDB stub’s “event loop”. This API requires implementations to “push”
data to the gdbstub
implementation whenever new data becomes available
(e.g: when a UART interrupt handler receives a byte, when the target hits a
breakpoint, etc…), as opposed to the GdbStub::run_blocking
API, which
“pulls” these events in a blocking manner.
See the GdbStubStateMachine
docs for more details on how to use this
API.
And with that lengthy introduction, I wish you the best of luck in your debugging adventures!
If you have any suggestions, feature requests, or run into any problems,
please start a discussion / open an issue over on the
gdbstub
GitHub repo.
Modules§
- Traits to encode architecture-specific target information.
- Common types and definitions used across
gdbstub
. - Traits to perform in-order, serial, byte-wise I/O.
- The core
GdbStub
type, used to drive a GDB debugging session for a particularTarget
over a givenConnection
. - The core
Target
trait, and all its various protocol extension traits.
Macros§
- Send formatted data to the GDB client console.
- Send formatted data to the GDB client console, with a newline appended.