[][src]Crate gdbstub

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.

Debugging Features

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.

Feature flags

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

Additional functionality can be enabled by activating certain features.

Example

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 {
    /// tick the system a single instruction
    fn step(&mut self) -> Result<(), EmuError> { /* ... */ }
    /// read a register's value
    fn read_reg(&self, idx: usize) -> u32 { /* ... */  }
    /// read a byte from a given address
    fn r8(&mut self, addr: u32) -> u8 { /* ... */ }
    // ... etc ...
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut emu = Emu::new();
    loop {
        emu.step()?;
    }
}

The Target trait

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 {
    // The target's pointer size.
    type Usize = u32;
    // Project-specific error type.
    type Error = EmuError;

    // Run the system for a single "step", using the provided callback to log
    // any memory accesses which may have occurred
    fn step(
        &mut self,
        mut log_mem_access: impl FnMut(Access<u32>),
    ) -> Result<TargetState, Self::Error> {
        // run the system
        self.step()?; // <-- can use `?` to propagate project-specific errors!

        // log any memory accesses which might have occurred
        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)
    }

    // Read-out the CPU's register values in the order specified in the arch's
    // `target.xml` file.
    // e.g: for ARM: binutils-gdb/blob/master/gdb/features/arm/arm-core.xml
    fn read_registers(&mut self, mut push_reg: impl FnMut(&[u8])) {
        // general purpose registers
        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());
        // Floating point registers, unused
        for _ in 0..25 {
            push_reg(&[0, 0, 0, 0]);
        }
        push_reg(&self.cpu.reg_get(reg::CPSR).to_le_bytes());
    }

    // Write to the CPU's register values in the order specified in the arch's
    // `target.xml` file.
    fn write_registers(&mut self, regs: &[u8]) {
        /* ... similar to read_registers ... */
    }

    fn read_pc(&mut self) -> u32 {
        self.cpu.reg_get(reg::PC)
    }

    // read the specified memory addresses from the target
    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))
        }
    }

    // write data to the specified memory addresses
    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);
        }
    }

    // there are several other methods whose default implementations can be
    // overridden to enable certain advanced GDB features
    // (e.g: automatic arch detection).
    //
    // See the docs for details.
}

The Connection trait

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()?;

    // 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)
}

Creating the GdbStub

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>> {
    // Pre-existing setup code
    let mut system = Emu::new()?;
    // ... etc ...

    // Establish a `Connection`
    let stream = wait_for_gdb_connection(9001);

    // Create a new `GdbStub` using the established `Connection`.
    let debugger = GdbStub::new(stream);

    // Instead of taking ownership of the system, GdbStub takes a &mut, yielding
    // ownership once the debugging session is closed, or an error occurs.
    let system_result = match debugger.run(&mut system) {
        Ok(state) => {
            eprintln!("Disconnected from GDB. Target state: {:?}", state);
            Ok(())
        }
        // handle any target-specific errors
        Err(gdbstub::Error::TargetError(e)) => Err(e),
        // connection / gdbstub internal errors
        Err(e) => return Err(e.into()),
    };

    eprintln!("{:?}", system_result);
}

Real-World Examples

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

If you happen to use gdbstub in one of your own projects, feel free to open a PR to add it to this list!

Structs

Access

Describes a memory access.

GdbStub

Facilitates the remote debugging of a Target using the GDB Remote Serial Protocol over a given Connection.

Enums

AccessKind

The kind of memory access being performed

Error

Errors which may occur during a GDB debugging session.

TargetState

The underlying system's execution state.

Traits

Connection

A trait for reading / writing bytes across some transport layer.

Target

Describes a target system which can be debugged using GdbStub.