Skip to content

Commit

Permalink
breakpoints and stepping and running oh my!
Browse files Browse the repository at this point in the history
With this commit, enough of the protocol is implemented for the gdbstub
to be considered _useful_!

The one major feature that's broken (and that I've been trying to fix)
is watchpoints. GDB seems to recieve the watchpoint stop request, but
then immediately sends another vCont packet! Why?? I'll try to figure it
out.

...

The plan for the next two commits is two-fold:
1) fix watchpoints
2) re-evaluate and re-architect the API, especially Target::Usize

If I can get those two features working, I'd be super happy, since at
the very least, it would cover my own use-case.
  • Loading branch information
daniel5151 committed Dec 29, 2019
1 parent fc386af commit 7daf21b
Show file tree
Hide file tree
Showing 23 changed files with 687 additions and 218 deletions.
163 changes: 163 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# gdbstub

An implementation of the [GDB Remote Server Protocol](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html#Remote-Protocol) in Rust, primarily for use in _emulators_.

`gdbstub` tries to make as few assumptions as possible about your emulator's architecture, and aims to provide a "drop-in" way to add GDB support into an emulator, _without_ any large refactoring / ownership juggling (hopefully).

## Debugging Features

- Core Protocol
- [x] Step / Continue
- [x] Add / Remove Breakpoints
- [x] Read/Write memory
- [ ] Read/Write/Access Watchpoints (i.e: value breakpoints)
- implemented, but currently broken
- Extended Protocol
- [x] Automatic architecture detection (via target.xml)

There are also a few features which rely on `std`, which can be enabled by enabling the `std` feature:

- An `impl Connection` for some common std types (notably: TcpStream)
- Additional logging (outputs protocol responses via `trace!`)

## Future Plans

- Improve packet-parsing infrastructure
- Reconsider / Rework `Target::Usize`
- i.e: find a "nicer" way to make `gdbstub` generic across various target pointer sizes
- Will most-likely switch to using `&[u8]` instead of a generic type
- If only const-generics were stable, then I could use generic-sized arrays. One day...
- Improve multiprocess / multithread / multicore support?
- Re-architect internals to remove `alloc` dependency?
- Current approach has a clear separation between packet parsing and command execution, and requires intermediate allocations for parsed data.
- Require user to provide their own packet buffers

## Example

_Disclaimer:_ `gdbstub`'s API and architecture is still very much in flux, so expect things to change often and "destructively"

This snippet provides a _very brief_ overview of how to use `gdbstub`.

While I have a few projects which are already using gdbstub, none of them are open-source (at the moment). In the future, I'll try to find some time time to create a more robust (read: compiling) example.

```rust
use std::net::{TcpListener, TcpStream};

use gdbstub::{GdbStub, Access, AccessKind, Target, TargetState};

// <your pre-existing emulator>
struct MySystem { /* ... */ }

// `Target` is the fundamental trait of `gdbstub`, wrapping the multitude of different
// emulator implementations behind a single, generic interface which the GdbStub can
// query for information / drive forward in response to incoming GDB commands.
impl Target for MySystem {
// The target's pointer size
type Usize = u32;
// A user-defined error type (for passing-through any internal emulation errors)
type Error = ();

// 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> {
self.cpu.cycle()?;

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

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 a few other optional methods which can be implemented to enable
// some more advanced functionality (e.g: automatic arch detection).
// See the docs for details.
}


fn main() -> Result<(), Box<dyn std::error::Error>> {
// Your existing setup code...
let mut system = MySystem::new()?;
// ...

// e.g: using a TcpStream for the GDB connection
let sockaddr = format!("localhost:{}", 9001);
eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);
let sock = TcpListener::bind(sockaddr)?;
let (stream, addr) = sock.accept()?;
eprintln!("Debugger connected from {}", addr);

// At this point, it's possible to connect to the emulator using
// `gdb-multiarch -iex "target remote localhost:9001"`

// Hand the connection off to the GdbStub.
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(())
}
Err(gdbstub::Error::TargetError(e)) => Err(e),
Err(e) => return Err(e.into()),
};

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


```

## Using `gdbstub` on actual hardware

While the target use-case for `gdbstub` is emulation, the crate is `no_std` compatible (albeit with a dependency on `alloc`), which means it _should_ be possible to use in embedded contexts as well.

At the moment, this is not a supported use-case, and has not been tested. Please let me know if you've had any success using `gdbstub` on actual hardware!
57 changes: 21 additions & 36 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,26 @@ mod connection_impls;
mod error;
mod protocol;
mod stub;
mod support;

pub use error::Error;
pub use stub::GdbStub;

/// The set of operations that a GDB target needs to implement.
pub trait Target {
/// The target architecture's pointer size
type Usize: FromLEBytes;
/// The target architecture's pointer size. Should be one of the built-in
/// unsigned integer types (u8, u16, u32, u64, or u128)
type Usize: support::ToFromLEBytes
+ Clone
+ Copy
+ core::hash::Hash
+ core::fmt::Debug
+ Eq
+ Ord
+ PartialEq
+ PartialOrd
+ Sized;

/// A target-specific unrecoverable error, which will be propagated
/// through the GdbStub
type Error;
Expand Down Expand Up @@ -57,8 +69,14 @@ pub trait Target {
/// register's value.
fn read_registers(&mut self, push_reg: impl FnMut(&[u8]));

/// Read the target's current PC
fn read_pc(&mut self) -> Self::Usize;

/// Read bytes from the specified address range
fn read_addrs(&mut self, addr: core::ops::Range<Self::Usize>, val: impl FnMut(u8));

/// Write bytes to the specified address range
fn write_addrs(&mut self, get_addr_val: impl FnMut() -> Option<(Self::Usize, u8)>);
}

#[derive(Debug)]
Expand All @@ -74,7 +92,7 @@ pub struct Access<U> {
pub val: u8,
}

// TODO: explore if TargetState is really necissary...
// TODO: explore if TargetState is really necessary...
#[derive(Debug, PartialEq, Eq)]
pub enum TargetState {
Running,
Expand Down Expand Up @@ -112,36 +130,3 @@ pub trait Connection {
})
}
}

/// A simple trait that enables a type to be constructed from a slice of little
/// endian bytes. It is automatically implemented for u8 through u128.
pub trait FromLEBytes: Sized {
/// Create [Self] from an array of little-endian order bytes.
/// Returns None if byte array is too short.
/// The array can be longer than required (truncating the result).
fn from_le_bytes(bytes: &[u8]) -> Option<Self>;
}

impl FromLEBytes for u8 {
fn from_le_bytes(buf: &[u8]) -> Option<Self> {
buf.get(0).copied()
}
}

macro_rules! impl_FromLEBytes {
($($type:ty),*) => {$(
impl FromLEBytes for $type {
fn from_le_bytes(buf: &[u8]) -> Option<Self> {
if buf.len() < core::mem::size_of::<Self>() {
return None;
}

let mut b = [0; core::mem::size_of::<Self>()];
b.copy_from_slice(&buf[..core::mem::size_of::<Self>()]);
Some(Self::from_le_bytes(b))
}
})*
};
}

impl_FromLEBytes! { u16, u32, u64, u128 }
15 changes: 13 additions & 2 deletions src/protocol/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// TODO: figure out how to make it accept exprs _and_ blocks
// TODO: use a trie structure for more efficient longest-prefix matching
macro_rules! prefix_match {
(
match $val:expr => [$name:ident|$rest:ident] {
Expand Down Expand Up @@ -32,7 +33,7 @@ macro_rules! commands {
#[derive(PartialEq, Eq, Debug)]
pub enum Command<'a> {
$($command($command<$($lifetime)?>),)*
Unknown,
Unknown(&'a str),
}

impl<'a> Command<'a> {
Expand All @@ -49,7 +50,7 @@ macro_rules! commands {
.map_err(|_| CommandParseError::MalformedCommand(name))?;
Command::$command(cmd)
})*
_ => { Command::Unknown }
_ => { Command::Unknown(body) }
}
};

Expand All @@ -61,6 +62,7 @@ macro_rules! commands {
}

/// Command parse error
// TODO: add more granular errors
#[derive(Debug)]
pub enum CommandParseError<'a> {
Empty,
Expand All @@ -70,14 +72,23 @@ pub enum CommandParseError<'a> {

commands! {
"?" => question_mark::QuestionMark,
"c" => _c::c,
"D" => _D::D,
"g" => _g::g,
"H" => _H::H,
"m" => _m::m,
"M" => _M::M,
"qAttached" => _qAttached::qAttached,
"qC" => _qC::qC,
"qfThreadInfo" => _qfThreadInfo::qfThreadInfo,
"qsThreadInfo" => _qsThreadInfo::qsThreadInfo,
"qSupported" => _qSupported::qSupported<'a>,
"qXfer:features:read" => _qXfer_features_read::qXferFeaturesRead<'a>,
"s" => _s::s,
"z" => _z::z,
"Z" => _Z::Z,

// Order Matters (because of prefix matching)
"vCont?" => vCont_question_mark::vContQuestionMark,
"vCont" => _vCont::vCont,
}
18 changes: 0 additions & 18 deletions src/protocol/commands/_D.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
/// 'D'
/// 'D;pid'
///
/// The first form of the packet is used to detach GDB from the remote system.
/// It is sent to the remote target before GDB disconnects via the detach
/// command.
///
/// The second form, including a process ID, is used when multiprocess protocol
/// extensions are enabled (see multiprocess extensions), to detach only a
/// specific process. The pid is specified as a big-endian hex string.
///
/// Reply:
///
/// 'OK'
/// for success
///
/// 'E NN'
/// for an error
#[derive(PartialEq, Eq, Debug)]
pub struct D {
pub pid: Option<isize>,
Expand Down
17 changes: 1 addition & 16 deletions src/protocol/commands/_H.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
/// 'H op thread-id'
///
/// Set thread for subsequent operations ('m', 'M', 'g', 'G', et.al.). Depending
/// on the operation to be performed, op should be 'c' for step and continue
/// operations (note that this is deprecated, supporting the 'vCont' command is
/// a better option), and 'g' for other operations. The thread designator
/// thread-id has the format and interpretation described in thread-id syntax.
///
/// Reply:
///
/// 'OK'
/// for success
///
/// 'E NN'
/// for an error
#[derive(PartialEq, Eq, Debug)]
pub struct H {
pub kind: char, // TODO: make this an enum
pub id: isize,
pub id: isize, // FIXME: 'H' has invlaid thread-id syntax
}

impl H {
Expand Down
32 changes: 32 additions & 0 deletions src/protocol/commands/_M.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use alloc::vec::Vec;

#[derive(PartialEq, Eq, Debug)]
pub struct M {
// FIXME: 'M' packet's addr should correspond to Target::USize
pub addr: u64,
pub len: usize,
pub val: Vec<u8>,
}

impl M {
pub fn parse(body: &str) -> Result<Self, ()> {
let mut body = body.split(|c| c == ',' || c == ':');
let addr = u64::from_str_radix(body.next().ok_or(())?, 16).map_err(drop)?;
let len = usize::from_str_radix(body.next().ok_or(())?, 16).map_err(drop)?;
let val = body.next().ok_or(())?;

if val.len() % 2 != 0 || !val.is_ascii() {
return Err(());
}

let val = val
.as_bytes()
.chunks_exact(2)
.map(|c| unsafe { core::str::from_utf8_unchecked(c) })
.map(|c| u8::from_str_radix(c, 16))
.collect::<Result<Vec<_>, _>>()
.map_err(drop)?;

Ok(M { addr, len, val })
}
}
Loading

0 comments on commit 7daf21b

Please sign in to comment.