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
ConnectionforBox<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
ConnectionforTcpStreamandUnixStream. - Implement
std::error::Errorforgdbstub::Error. - Add a
TargetError::Iovariant to simplifystd::io::Errorhandling from Target methods.
- Implement
paranoid_unsafe- Please refer to the
unsafeingdbstubsection of the README.md for more details.
- Please refer to the
core_error- Make
GdbStubErrorimplementcore::error::Errorinstead ofstd::error::Error.
- Make
§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
gdbstubinto a project for the first time.
In particular, the in-tree
armv4texample 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§
- arch
- Traits to encode architecture-specific target information.
- common
- Common types and definitions used across
gdbstub. - conn
- Traits to perform in-order, serial, byte-wise I/O.
- stub
- The core
GdbStubtype, used to drive a GDB debugging session for a particularTargetover a givenConnection. - target
- The core
Targettrait, and all its various protocol extension traits.