Skip to content

Commit

Permalink
add state machine based packet parser frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel5151 committed May 25, 2021
1 parent 2d745df commit 297e065
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 101 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

An ergonomic and easy-to-integrate implementation of the [GDB Remote Serial Protocol](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html#Remote-Protocol) in Rust, with full `#![no_std]` support.

`gdbstub` makes it easy to integrate powerful guest debugging support to your emulator/hypervisor/debugger/embedded project. By implementing just a few basic methods of the [`gdbstub::Target`](https://docs.rs/gdbstub/latest/gdbstub/target/ext/base/singlethread/trait.SingleThreadOps.html) trait, you can have a rich GDB debugging session up and running in no time!
`gdbstub` makes it easy to integrate powerful guest debugging support to your emulator/hypervisor/debugger/embedded project. By implementing just a few basic methods of the [`gdbstub::Target`](https://docs.rs/gdbstub/latest/gdbstub/target/ext/base/singlethread/trait.SingleThreadOps.html) trait, you can have a rich GDB debugging session up and running in no time!

**If you're looking for a quick snippet of example code to see what a typical `gdbstub` integration might look like, check out [examples/armv4t/gdb/mod.rs](https://github.com/daniel5151/gdbstub/blob/master/examples/armv4t/gdb/mod.rs)**

Expand Down
6 changes: 6 additions & 0 deletions example_no_std/dump_asm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -e

cargo rustc --release -- --emit asm -C "llvm-args=-x86-asm-syntax=intel"
cat ./target/release/deps/gdbstub_nostd-*.s | c++filt > asm.s
sed -i -E '/\.(cfi_def_cfa_offset|cfi_offset|cfi_startproc|cfi_endproc|size)/d' asm.s
39 changes: 24 additions & 15 deletions example_no_std/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

extern crate libc;

use gdbstub::{DisconnectReason, GdbStubBuilder, GdbStubError};
use gdbstub::{Connection, DisconnectReason, GdbStubBuilder, GdbStubError};

mod conn;
mod gdb;
Expand Down Expand Up @@ -31,27 +31,36 @@ fn rust_main() -> Result<(), i32> {
};

let mut buf = [0; 4096];
let mut gdb = GdbStubBuilder::new(conn)
let gdb = GdbStubBuilder::new(conn)
.with_packet_buffer(&mut buf)
.build()
.map_err(|_| 1)?;

print_str("Starting GDB session...");

match gdb.run(&mut target) {
Ok(disconnect_reason) => match disconnect_reason {
DisconnectReason::Disconnect => print_str("GDB Disconnected"),
DisconnectReason::TargetExited(_) => print_str("Target exited"),
DisconnectReason::TargetTerminated(_) => print_str("Target halted"),
DisconnectReason::Kill => print_str("GDB sent a kill command"),
},
Err(GdbStubError::TargetError(_e)) => {
print_str("Target raised a fatal error");
}
Err(_e) => {
print_str("gdbstub internal error");
let mut gdb = gdb.run_state_machine().map_err(|_| 1)?;

loop {
let byte = gdb.borrow_conn().read().map_err(|_| 1)?;
match gdb.pump(&mut target, byte) {
Ok(None) => {}
Ok(Some(disconnect_reason)) => match disconnect_reason {
DisconnectReason::Disconnect => {
print_str("GDB Disconnected");
break;
}
DisconnectReason::TargetExited(_) => print_str("Target exited"),
DisconnectReason::TargetTerminated(_) => print_str("Target halted"),
DisconnectReason::Kill => print_str("GDB sent a kill command"),
},
Err(GdbStubError::TargetError(_e)) => {
print_str("Target raised a fatal error");
}
Err(_e) => {
print_str("gdbstub internal error");
}
}
};
}

Ok(())
}
Expand Down
213 changes: 128 additions & 85 deletions src/gdbstub_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use managed::ManagedSlice;

use crate::common::*;
use crate::connection::Connection;
use crate::protocol::recv_packet::{RecvPacketBlocking, RecvPacketStateMachine};
use crate::protocol::{commands::Command, Packet, ResponseWriter, SpecificIdKind};
use crate::target::Target;
use crate::util::managed_vec::ManagedVec;
use crate::SINGLE_THREAD_TID;

mod builder;
Expand Down Expand Up @@ -48,10 +48,12 @@ impl<'a, T: Target, C: Connection> GdbStub<'a, T, C> {

/// Create a new `GdbStub` using the provided connection.
///
/// For fine-grained control over various `GdbStub` options, use the
/// [`builder()`](GdbStub::builder) method instead.
/// _Note:_ `new` is only available when the `alloc` feature is enabled, as
/// it will use a dynamically allocated `Vec` as a packet buffer.
///
/// _Note:_ `new` is only available when the `alloc` feature is enabled.
/// For fine-grained control over various `GdbStub` options, including the
/// ability to specify a fixed-size buffer, use the [`GdbStub::builder`]
/// method instead.
#[cfg(feature = "alloc")]
pub fn new(conn: C) -> GdbStub<'a, T, C> {
GdbStubBuilder::new(conn).build().unwrap()
Expand All @@ -65,6 +67,61 @@ impl<'a, T: Target, C: Connection> GdbStub<'a, T, C> {
self.state
.run(target, &mut self.conn, &mut self.packet_buffer)
}

/// Starts a GDB remote debugging session, and convert this instance of
/// `GdbStub` into a [`GdbStubStateMachine`].
///
/// Note: This method will invoke `Connection::on_session_start`, and
/// as such may return a connection error.
pub fn run_state_machine(
mut self,
) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> {
self.conn
.on_session_start()
.map_err(Error::ConnectionRead)?;

Ok(GdbStubStateMachine {
conn: self.conn,
packet_buffer: self.packet_buffer,
recv_packet: RecvPacketStateMachine::new(),
state: self.state,
})
}
}

/// A variant of [`GdbStub`] which parses incoming packets using an asynchronous
/// state machine.
///
/// TODO: more docs
pub struct GdbStubStateMachine<'a, T: Target, C: Connection> {
conn: C,
packet_buffer: ManagedSlice<'a, u8>,
recv_packet: RecvPacketStateMachine,
state: GdbStubImpl<T, C>,
}

impl<'a, T: Target, C: Connection> GdbStubStateMachine<'a, T, C> {
/// Pass a byte to the `gdbstub` packet parser.
///
/// Returns a `Some(DisconnectReason)` if the GDB client
pub fn pump(
&mut self,
target: &mut T,
byte: u8,
) -> Result<Option<DisconnectReason>, Error<T::Error, C::Error>> {
let packet_buffer = match self.recv_packet.pump(&mut self.packet_buffer, byte)? {
Some(buf) => buf,
None => return Ok(None),
};

let packet = Packet::from_buf(target, packet_buffer).map_err(Error::PacketParse)?;
self.state.handle_packet(target, &mut self.conn, packet)
}

/// Return a mutable reference to the underlying connection.
pub fn borrow_conn(&mut self) -> &mut C {
&mut self.conn
}
}

struct GdbStubImpl<T: Target, C: Connection> {
Expand Down Expand Up @@ -111,96 +168,82 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
conn.on_session_start().map_err(Error::ConnectionRead)?;

loop {
match Self::recv_packet(conn, target, packet_buffer)? {
Packet::Ack => {}
Packet::Nack => return Err(Error::ClientSentNack),
Packet::Interrupt => {
debug!("<-- interrupt packet");
let mut res = ResponseWriter::new(conn);
res.write_str("S05")?;
res.flush()?;
}
Packet::Command(command) => {
// Acknowledge the command
if !self.no_ack_mode {
conn.write(b'+').map_err(Error::ConnectionRead)?;
}

let mut res = ResponseWriter::new(conn);
let disconnect = match self.handle_command(&mut res, target, command) {
Ok(HandlerStatus::Handled) => None,
Ok(HandlerStatus::NeedsOk) => {
res.write_str("OK")?;
None
}
Ok(HandlerStatus::Disconnect(reason)) => Some(reason),
// HACK: handling this "dummy" error is required as part of the
// `TargetResultExt::handle_error()` machinery.
Err(Error::NonFatalError(code)) => {
res.write_str("E")?;
res.write_num(code)?;
None
}
Err(Error::TargetError(e)) => {
// unlike all other errors which are "unrecoverable" in the sense that
// the GDB session cannot continue, there's still a chance that a target
// might want to keep the debugging session alive to do a "post-mortem"
// analysis. As such, we simply report a standard TRAP stop reason.
let mut res = ResponseWriter::new(conn);
res.write_str("S05")?;
res.flush()?;
return Err(Error::TargetError(e));
}
Err(e) => return Err(e),
};

// HACK: this could be more elegant...
if disconnect != Some(DisconnectReason::Kill) {
res.flush()?;
}

if let Some(disconnect_reason) = disconnect {
return Ok(disconnect_reason);
}
}
use crate::protocol::recv_packet::RecvPacketError;
let packet_buffer = match RecvPacketBlocking::new().recv(packet_buffer, || conn.read())
{
Err(RecvPacketError::Capacity) => return Err(Error::PacketBufferOverflow),
Err(RecvPacketError::Connection(e)) => return Err(Error::ConnectionWrite(e)),
Ok(buf) => buf,
};

let packet = Packet::from_buf(target, packet_buffer).map_err(Error::PacketParse)?;
if let Some(disconnect_reason) = self.handle_packet(target, conn, packet)? {
return Ok(disconnect_reason);
}
}
}

fn recv_packet<'a>(
conn: &mut C,
fn handle_packet(
&mut self,
target: &mut T,
pkt_buf: &'a mut ManagedSlice<u8>,
) -> Result<Packet<'a>, Error<T::Error, C::Error>> {
let header_byte = conn.read().map_err(Error::ConnectionRead)?;

// Wrap the buf in a `ManagedVec` to keep the code readable.
let mut buf = ManagedVec::new(pkt_buf);

buf.clear();
buf.push(header_byte)?;
if header_byte == b'$' {
// read the packet body
loop {
let c = conn.read().map_err(Error::ConnectionRead)?;
buf.push(c)?;
if c == b'#' {
break;
}
conn: &mut C,
packet: Packet<'_>,
) -> Result<Option<DisconnectReason>, Error<T::Error, C::Error>> {
match packet {
Packet::Ack => {}
Packet::Nack => return Err(Error::ClientSentNack),
Packet::Interrupt => {
debug!("<-- interrupt packet");
let mut res = ResponseWriter::new(conn);
res.write_str("S05")?;
res.flush()?;
}
// read the checksum as well
buf.push(conn.read().map_err(Error::ConnectionRead)?)?;
buf.push(conn.read().map_err(Error::ConnectionRead)?)?;
}
Packet::Command(command) => {
// Acknowledge the command
if !self.no_ack_mode {
conn.write(b'+').map_err(Error::ConnectionWrite)?;
}

let mut res = ResponseWriter::new(conn);
let disconnect_reason = match self.handle_command(&mut res, target, command) {
Ok(HandlerStatus::Handled) => None,
Ok(HandlerStatus::NeedsOk) => {
res.write_str("OK")?;
None
}
Ok(HandlerStatus::Disconnect(reason)) => Some(reason),
// HACK: handling this "dummy" error is required as part of the
// `TargetResultExt::handle_error()` machinery.
Err(Error::NonFatalError(code)) => {
res.write_str("E")?;
res.write_num(code)?;
None
}
Err(Error::TargetError(e)) => {
// unlike all other errors which are "unrecoverable" in the sense that
// the GDB session cannot continue, there's still a chance that a target
// might want to keep the debugging session alive to do a "post-mortem"
// analysis. As such, we simply report a standard TRAP stop reason.
let mut res = ResponseWriter::new(conn);
res.write_str("S05")?;
res.flush()?;
return Err(Error::TargetError(e));
}
Err(e) => return Err(e),
};

trace!(
"<-- {}",
core::str::from_utf8(buf.as_slice()).unwrap_or("<invalid packet>")
);
// every response needs to be flushed, _except_ for the response to a kill
// packet, but ONLY when extended mode is NOT implemented.
let is_kill = matches!(disconnect_reason, Some(DisconnectReason::Kill));
if !(target.extended_mode().is_none() && is_kill) {
res.flush()?;
}

drop(buf);
return Ok(disconnect_reason);
}
};

Packet::from_buf(target, pkt_buf.as_mut()).map_err(Error::PacketParse)
Ok(None)
}

fn handle_command(
Expand Down
1 change: 1 addition & 0 deletions src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod packet;
mod response_writer;

pub(crate) mod commands;
pub(crate) mod recv_packet;

pub(crate) use common::thread_id::{IdKind, SpecificIdKind, SpecificThreadId};
pub(crate) use packet::Packet;
Expand Down
Loading

0 comments on commit 297e065

Please sign in to comment.