diff options
Diffstat (limited to 'src/stub/state_machine.rs')
-rw-r--r-- | src/stub/state_machine.rs | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/src/stub/state_machine.rs b/src/stub/state_machine.rs new file mode 100644 index 0000000..766d343 --- /dev/null +++ b/src/stub/state_machine.rs @@ -0,0 +1,370 @@ +//! Low-level state-machine interface that underpins [`GdbStub`]. +// +// TODO: write some proper documentation + examples of how to interface with +// this API. +//! +//! # Hey, what gives? Where are all the docs!? +//! +//! Yep, sorry about that! +//! +//! `gdbstub` 0.6 turned out ot be a pretty massive release, and documenting +//! everything has proven to be a somewhat gargantuan task that's kept delaying +//! the release data further and further back... +//! +//! To avoid blocking the release any further, I've decided to leave this bit of +//! the API sparsely documented. +//! +//! If you're interested in using this API directly (e.g: to integrate `gdbstub` +//! into a `no_std` project, or to use `gdbstub` in a non-blocking manner +//! alongside `async/await` / a project specific event loop), your best bet +//! would be to review the following bits of code to get a feel for the API: +//! +//! - The implementation of [`GdbStub::run_blocking`] +//! - Implementations of [`BlockingEventLoop`] used alongside +//! `GdbStub::run_blocking` (e.g: the in-tree `armv4t` / `armv4t_multicore` +//! examples) +//! - Real-world projects using the API +//! - The best example of this (at the time of writing) is the code at +//! [`vmware-labs/node-replicated-kernel`](https://github.com/vmware-labs/node-replicated-kernel/blob/4326704aaf3c0052e614dcde2a788a8483224394/kernel/src/arch/x86_64/gdb/mod.rs#L106) +//! +//! If you have any questions, feel free to open a discussion thread over at the +//! [`gdbstub` GitHub repo](https://github.com/daniel5151/gdbstub/). +//! +//! [`BlockingEventLoop`]: super::run_blocking::BlockingEventLoop +//! [`GdbStub::run_blocking`]: super::GdbStub::run_blocking + +use managed::ManagedSlice; + +use crate::arch::Arch; +use crate::conn::Connection; +use crate::protocol::recv_packet::RecvPacketStateMachine; +use crate::protocol::{Packet, ResponseWriter}; +use crate::stub::error::GdbStubError as Error; +use crate::stub::stop_reason::IntoStopReason; +use crate::target::Target; + +use super::core_impl::{FinishExecStatus, GdbStubImpl, State}; +use super::{DisconnectReason, GdbStub}; + +/// State-machine interface to `GdbStub`. +/// +/// See the [module level documentation](self) for more details. +pub enum GdbStubStateMachine<'a, T, C> +where + T: Target, + C: Connection, +{ + /// The target is completely stopped, and the GDB stub is waiting for + /// additional input. + Idle(GdbStubStateMachineInner<'a, state::Idle<T>, T, C>), + /// The target is currently running, and the GDB client is waiting for + /// the target to report a stop reason. + /// + /// Note that the client may still send packets to the target + /// (e.g: to trigger a Ctrl-C interrupt). + Running(GdbStubStateMachineInner<'a, state::Running, T, C>), + /// The GDB client has sent a Ctrl-C interrupt to the target. + CtrlCInterrupt(GdbStubStateMachineInner<'a, state::CtrlCInterrupt, T, C>), + /// The GDB client has disconnected. + Disconnected(GdbStubStateMachineInner<'a, state::Disconnected, T, C>), +} + +/// State machine typestates. +/// +/// The types in this module are used to parameterize instances of +/// [`GdbStubStateMachineInner`], thereby enforcing that certain API methods +/// can only be called while the stub is in a certain state. +// As an internal implementation detail, they _also_ carry state-specific +// payloads, which are used when transitioning between states. +pub mod state { + use super::*; + + use crate::stub::stop_reason::MultiThreadStopReason; + + // used internally when logging state transitions + pub(crate) const MODULE_PATH: &str = concat!(module_path!(), "::"); + + /// Typestate corresponding to the "Idle" state. + #[non_exhaustive] + pub struct Idle<T: Target> { + pub(crate) deferred_ctrlc_stop_reason: + Option<MultiThreadStopReason<<<T as Target>::Arch as Arch>::Usize>>, + } + + /// Typestate corresponding to the "Running" state. + #[non_exhaustive] + pub struct Running {} + + /// Typestate corresponding to the "CtrlCInterrupt" state. + #[non_exhaustive] + pub struct CtrlCInterrupt { + pub(crate) from_idle: bool, + } + + /// Typestate corresponding to the "Disconnected" state. + #[non_exhaustive] + pub struct Disconnected { + pub(crate) reason: DisconnectReason, + } +} + +/// Internal helper macro to convert between a particular inner state into +/// its corresponding `GdbStubStateMachine` variant. +macro_rules! impl_from_inner { + ($state:ident $($tt:tt)*) => { + impl<'a, T, C> From<GdbStubStateMachineInner<'a, state::$state $($tt)*, T, C>> + for GdbStubStateMachine<'a, T, C> + where + T: Target, + C: Connection, + { + fn from(inner: GdbStubStateMachineInner<'a, state::$state $($tt)*, T, C>) -> Self { + GdbStubStateMachine::$state(inner) + } + } + }; + } + +impl_from_inner!(Idle<T>); +impl_from_inner!(Running); +impl_from_inner!(CtrlCInterrupt); +impl_from_inner!(Disconnected); + +/// Internal helper trait to cut down on boilerplate required to transition +/// between states. +trait Transition<'a, T, C> +where + T: Target, + C: Connection, +{ + /// Transition between different state machine states + fn transition<S2>(self, state: S2) -> GdbStubStateMachineInner<'a, S2, T, C>; +} + +impl<'a, S1, T, C> Transition<'a, T, C> for GdbStubStateMachineInner<'a, S1, T, C> +where + T: Target, + C: Connection, +{ + #[inline(always)] + fn transition<S2>(self, state: S2) -> GdbStubStateMachineInner<'a, S2, T, C> { + if log::log_enabled!(log::Level::Trace) { + let s1 = core::any::type_name::<S1>(); + let s2 = core::any::type_name::<S2>(); + log::trace!( + "transition: {:?} --> {:?}", + s1.strip_prefix(state::MODULE_PATH).unwrap_or(s1), + s2.strip_prefix(state::MODULE_PATH).unwrap_or(s2) + ); + } + GdbStubStateMachineInner { i: self.i, state } + } +} + +// split off `GdbStubStateMachineInner`'s non state-dependant data into separate +// struct for code bloat optimization (i.e: `transition` will generate better +// code when the struct is cleaved this way). +struct GdbStubStateMachineReallyInner<'a, T: Target, C: Connection> { + conn: C, + packet_buffer: ManagedSlice<'a, u8>, + recv_packet: RecvPacketStateMachine, + inner: GdbStubImpl<T, C>, +} + +/// Core state machine implementation that is parameterized by various +/// [states](state). Can be converted back into the appropriate +/// [`GdbStubStateMachine`] variant via [`Into::into`]. +pub struct GdbStubStateMachineInner<'a, S, T: Target, C: Connection> { + i: GdbStubStateMachineReallyInner<'a, T, C>, + state: S, +} + +/// Methods which can be called regardless of the current state. +impl<'a, S, T: Target, C: Connection> GdbStubStateMachineInner<'a, S, T, C> { + /// Return a mutable reference to the underlying connection. + pub fn borrow_conn(&mut self) -> &mut C { + &mut self.i.conn + } +} + +/// Methods which can only be called from the [`GdbStubStateMachine::Idle`] +/// state. +impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::Idle<T>, T, C> { + /// Internal entrypoint into the state machine. + pub(crate) fn from_plain_gdbstub( + stub: GdbStub<'a, T, C>, + ) -> GdbStubStateMachineInner<'a, state::Idle<T>, T, C> { + GdbStubStateMachineInner { + i: GdbStubStateMachineReallyInner { + conn: stub.conn, + packet_buffer: stub.packet_buffer, + recv_packet: RecvPacketStateMachine::new(), + inner: stub.inner, + }, + state: state::Idle { + deferred_ctrlc_stop_reason: None, + }, + } + } + + /// Pass a byte to the GDB stub. + pub fn incoming_data( + mut self, + target: &mut T, + byte: u8, + ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> { + let packet_buffer = match self.i.recv_packet.pump(&mut self.i.packet_buffer, byte)? { + Some(buf) => buf, + None => return Ok(self.into()), + }; + + let packet = Packet::from_buf(target, packet_buffer).map_err(Error::PacketParse)?; + let state = self + .i + .inner + .handle_packet(target, &mut self.i.conn, packet)?; + Ok(match state { + State::Pump => self.into(), + State::Disconnect(reason) => self.transition(state::Disconnected { reason }).into(), + State::DeferredStopReason => { + match self.state.deferred_ctrlc_stop_reason { + // if we were interrupted while idle, immediately report the deferred stop + // reason after transitioning into the running state + Some(reason) => { + return self + .transition(state::Running {}) + .report_stop(target, reason) + } + // otherwise, just transition into the running state as usual + None => self.transition(state::Running {}).into(), + } + } + State::CtrlCInterrupt => self + .transition(state::CtrlCInterrupt { from_idle: true }) + .into(), + }) + } +} + +/// Methods which can only be called from the +/// [`GdbStubStateMachine::Running`] state. +impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::Running, T, C> { + /// Report a target stop reason back to GDB. + pub fn report_stop( + mut self, + target: &mut T, + reason: impl IntoStopReason<T>, + ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> { + let mut res = ResponseWriter::new(&mut self.i.conn, target.use_rle()); + let event = self.i.inner.finish_exec(&mut res, target, reason.into())?; + res.flush()?; + + Ok(match event { + FinishExecStatus::Handled => self + .transition(state::Idle { + deferred_ctrlc_stop_reason: None, + }) + .into(), + FinishExecStatus::Disconnect(reason) => { + self.transition(state::Disconnected { reason }).into() + } + }) + } + + /// Pass a byte to the GDB stub. + /// + /// NOTE: unlike the `incoming_data` method in the `state::Idle` state, + /// this method does not perform any state transitions, and will + /// return a `GdbStubStateMachineInner` in the `state::Running` state. + pub fn incoming_data( + mut self, + target: &mut T, + byte: u8, + ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> { + let packet_buffer = match self.i.recv_packet.pump(&mut self.i.packet_buffer, byte)? { + Some(buf) => buf, + None => return Ok(self.into()), + }; + + let packet = Packet::from_buf(target, packet_buffer).map_err(Error::PacketParse)?; + let state = self + .i + .inner + .handle_packet(target, &mut self.i.conn, packet)?; + Ok(match state { + State::Pump => self.transition(state::Running {}).into(), + State::Disconnect(reason) => self.transition(state::Disconnected { reason }).into(), + State::DeferredStopReason => self.transition(state::Running {}).into(), + State::CtrlCInterrupt => self + .transition(state::CtrlCInterrupt { from_idle: false }) + .into(), + }) + } +} + +/// Methods which can only be called from the +/// [`GdbStubStateMachine::CtrlCInterrupt`] state. +impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::CtrlCInterrupt, T, C> { + /// Acknowledge the Ctrl-C interrupt. + /// + /// Passing `None` as a stop reason will return the state machine to + /// whatever state it was in pre-interruption, without immediately returning + /// a stop reason. + /// + /// Depending on how the target is implemented, it may or may not make sense + /// to immediately return a stop reason as part of handling the Ctrl-C + /// interrupt. e.g: in some cases, it may be better to send the target a + /// signal upon receiving a Ctrl-C interrupt _without_ immediately sending a + /// stop reason, and instead deferring the stop reason to some later point + /// in the target's execution. + /// + /// Some notes on handling Ctrl-C interrupts: + /// + /// - Stubs are not required to recognize these interrupt mechanisms, and + /// the precise meaning associated with receipt of the interrupt is + /// implementation defined. + /// - If the target supports debugging of multiple threads and/or processes, + /// it should attempt to interrupt all currently-executing threads and + /// processes. + /// - If the stub is successful at interrupting the running program, it + /// should send one of the stop reply packets (see Stop Reply Packets) to + /// GDB as a result of successfully stopping the program + pub fn interrupt_handled( + self, + target: &mut T, + stop_reason: Option<impl IntoStopReason<T>>, + ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> { + if self.state.from_idle { + // target is stopped - we cannot report the stop reason yet + Ok(self + .transition(state::Idle { + deferred_ctrlc_stop_reason: stop_reason.map(Into::into), + }) + .into()) + } else { + // target is running - we can immediately report the stop reason + let gdb = self.transition(state::Running {}); + match stop_reason { + Some(reason) => gdb.report_stop(target, reason), + None => Ok(gdb.into()), + } + } + } +} + +/// Methods which can only be called from the +/// [`GdbStubStateMachine::Disconnected`] state. +impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::Disconnected, T, C> { + /// Inspect why the GDB client disconnected. + pub fn get_reason(&self) -> DisconnectReason { + self.state.reason + } + + /// Reuse the existing state machine instance, reentering the idle loop. + pub fn return_to_idle(self) -> GdbStubStateMachine<'a, T, C> { + self.transition(state::Idle { + deferred_ctrlc_stop_reason: None, + }) + .into() + } +} |