An implementation of the GDB Remote Serial Protocol in Rust.

gdbstub tries to make as few assumptions as possible about a project's architecture, and aims to provide a "drop-in" way to add GDB support, without requiring any large refactoring / ownership juggling. It is particularly useful in emulators, where it provides a powerful, non-intrusive way to debug code running within an emulated system.

Disclaimer: gdbstub is still in it's early stages of development! Expect breaking API changes between minor releases.

At the moment, gdbstub implements enough of the GDB Remote Serial Protocol to support step-through + breakpoint debugging of single-threaded code.

Core GDB Protocol Step + Continue Add + Remove Breakpoints Read/Write memory Read/Write registers Read/Write/Access Watchpoints (i.e: value breakpoints) (currently broken)

Extended GDB Protocol (optional) Automatic architecture detection



The GDB Remote Serial Protocol is surprisingly complex, supporting advanced features such as remote file I/O, spawning new processes, "rewinding" program execution, and much, much more. Thankfully, most of these features are completely optional, and getting a basic debugging session up-and-running only requires a small subset of commands to be implemented.

gdbstub is no_std by default, though it does have a dependency on alloc .

Additional functionality can be enabled by activating certain features.

std - (disabled by default) Implements Connection for std::net::TcpStream . Implements std::error::Error for gdbstub::Error . Outputs protocol responses via log::trace!

- (disabled by default)

Note: Please refer to the Real-World Examples for examples that can be compiled and run. The example below merely provides a high-level overview of what a gdbstub integration might look like.

Consider a project with the following structure:

ⓘ This example deliberately fails to compile

struct EmuError { } struct Emu { } impl Emu { fn step ( & mut self ) - > Result < (), EmuError > { } fn read_reg ( & self , idx : usize ) - > u32 { } fn r8 ( & mut self , addr : u32 ) - > u8 { } } fn main () - > Result < (), Box < dyn std :: error :: Error > > { let mut emu = Emu :: new (); loop { emu . step () ? ; } }

The Target trait is used to modify and control a system's execution state during a GDB debugging session. Since each project is different, it's up to the user to provide methods to read/write memory, step execution, etc...

ⓘ This example deliberately fails to compile

use gdbstub ::{ GdbStub , Access , AccessKind , Target , TargetState }; impl Target for Emu { type Usize = u32 ; type Error = EmuError ; fn step ( & mut self , mut log_mem_access : impl FnMut ( Access < u32 > ), ) - > Result < TargetState , Self :: Error > { self . step () ? ; for ( read_or_write , addr , val ) in self . mem . recent_accesses . drain (..) { log_mem_access ( Access { kind : if read_or_write { AccessKind :: Read } else { AccessKind :: Write }, addr , val }) } Ok ( TargetState :: Running ) } fn read_registers ( & mut self , mut push_reg : impl FnMut ( & [ u8 ])) { for i in 0 .. 13 { push_reg ( & self . cpu . reg_get ( i ). to_le_bytes ()); } push_reg ( & self . cpu . reg_get ( reg :: SP ). to_le_bytes ()); push_reg ( & self . cpu . reg_get ( reg :: LR ). to_le_bytes ()); push_reg ( & self . cpu . reg_get ( reg :: PC ). to_le_bytes ()); for _ in 0 .. 25 { push_reg ( & [ 0 , 0 , 0 , 0 ]); } push_reg ( & self . cpu . reg_get ( reg :: CPSR ). to_le_bytes ()); } fn write_registers ( & mut self , regs : & [ u8 ]) { } fn read_pc ( & mut self ) - > u32 { self . cpu . reg_get ( reg :: PC ) } fn read_addrs ( & mut self , addr : std :: ops :: Range < u32 > , mut push_byte : impl FnMut ( u8 )) { for addr in addr { push_byte ( self . mem . r8 ( addr )) } } fn write_addrs ( & mut self , mut get_addr_val : impl FnMut () - > Option < ( u32 , u8 ) > ) { while let Some (( addr , val )) = get_addr_val () { self . mem . w8 ( addr , val ); } } }

The GDB Remote Serial Protocol is transport agnostic, only requiring that the transport provides in-order, bytewise I/O (such as TCP, UDS, UART, etc...). This transport requirement is encoded in the Connection trait.

gdbstub includes a pre-defined implementation of Connection for std::net::TcpStream (assuming the std feature flag is enabled).

A common way to begin a remote debugging is connecting to a target via TCP:

use std :: net ::{ TcpListener , TcpStream }; fn wait_for_gdb_connection ( port : u16 ) - > std :: 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 () ? ; eprintln ! ( "Debugger connected from {}" , addr ); Ok ( stream ) }

All that's left is to create a new GdbStub , pass it your Connection and Target , and call run !

ⓘ This example deliberately fails to compile

fn main () - > Result < (), Box < dyn std :: error :: Error > > { let mut system = Emu :: new () ? ; let stream = wait_for_gdb_connection ( 9001 ); let debugger = GdbStub :: new ( stream ); let system_result = match debugger . run ( & mut system ) { Ok ( state ) = > { eprintln ! ( "Disconnected from GDB. Target state: {:?}" , state ); Ok (()) } Err ( gdbstub :: Error :: TargetError ( e )) = > Err ( e ), Err ( e ) = > return Err ( e . into ()), }; eprintln ! ( "{:?}" , system_result ); }

There are already several projects which are using gdbstub :

rustyboyadvance-ng - Nintendo GameBoy Advance emulator and debugger

microcorruption-emu - msp430 emulator for the microcorruption.com ctf

ts7200 - An emulator for the TS-7200, a somewhat bespoke embedded ARMv4t platform