Skip to content

Commit 4e46b72

Browse files
authored
Rewrite GdbStub::run to use the state machine implementation (and more!) (#88)
This is a set of API changes I've been sitting on for many months now, and I think it's high-time that I pushed these changes up. The docs are still a mess, but I've opted to commit things as is to get hands-on feedback from folks to make sure things work, and then I'll tackle the documentation later. At a high level, this PR introduces the following API tweaks: #### Removed the `read` and `peek` methods from `Connection`, moving them into a separate `ConnectionExt` trait. The state machine API was explicitly designed to _circumvent_ the need for a blocking `read` API. Nonetheless, it is useful to have a reference implementation of a blocking single-byte read for various types (e.g: TcpStream) built-in to `gdbstub`, so I've split these implementations off into a separate `ConnectionExt` trait - which as the name implies, requires the type to also implement `Connection`. This new trait is used as part of the `GdbStub::run_blocking` API described below... #### `Target::resume` is now _non blocking_, and does not support immediately returning a `StopReason` The previous iteration of the API kept the ability to return a `StopReason` from `Target::resume` in order to maintain compatibility with existing blocking `Target::resume` implementations (such as the one in the `armv4t` example). With this new API, targets are now _forced_ to resume in a non-blocking manner, with the responsibility of "driving" the resumed target lifted up into the state machine event loop. This change also means that `Target::resume` no longer takes a `GdbInterrupt` callback, which has simplified a _ton_ of code within `gdbstub`! #### The bespoke blocking `GdbStub::run` implementation has been swapped out with a new `GdbStub::run_blocking` method - backed by the state machine API. Instead of maintaining two parallel "front-ends" to `GdbStub`, I've consolidated the two to both use the `GdbStubStateMachine` API under the hood. This new API is _looks_ more complex to the old `GdbStub::run` API, as it requires the user to provide an implementation of `gdbstub_run_blocking::BlockingEventLoop` for their particular `Target` + `Connection`, but in reality, all this new API has done is "lift" most of the code that used to live in `Target::resume` up into `gdbstub_run_blocking::BlockingEventLoop`.
1 parent c5489a0 commit 4e46b72

File tree

25 files changed

+844
-912
lines changed

25 files changed

+844
-912
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Why use `gdbstub`?
2828
- **`#![no_std]` Ready & Size Optimized**
2929
- `gdbstub` is a **`no_std` first** library, whereby all protocol features are required to be `no_std` compatible.
3030
- `gdbstub` does not require _any_ dynamic memory allocation, and can be configured to use fixed-size, pre-allocated buffers. This enables `gdbstub` to be used on even the most resource constrained, no-[`alloc`](https://doc.rust-lang.org/alloc/) platforms.
31-
- `gdbstub` is entirely **panic free** (when compiled in release mode, without the `paranoid_unsafe` cargo feature).
31+
- `gdbstub` is entirely **panic free** in most minimal configurations (when compiled in release mode, without the `paranoid_unsafe` cargo feature).
3232
- Validated by inspecting the asm output of the in-tree `example_no_std`.
3333
- `gdbstub` is transport-layer agnostic, and uses a basic [`Connection`](https://docs.rs/gdbstub/latest/gdbstub/trait.Connection.html) interface to communicate with the GDB server. As long as target has some method of performing in-order, serial, byte-wise I/O (e.g: putchar/getchar over UART), it's possible to run `gdbstub` on it!
3434
- "You don't pay for what you don't use": All code related to parsing/handling protocol extensions is guaranteed to be dead-code-eliminated from an optimized binary if left unimplemented! See the [Zero-overhead Protocol Extensions](#zero-overhead-protocol-extensions) section below for more details.

example_no_std/src/conn.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ impl TcpConnection {
5050
Ok(buf[0])
5151
}
5252
}
53+
54+
#[allow(dead_code)]
55+
pub fn peek(&mut self) -> Result<Option<u8>, &'static str> {
56+
let mut buf = [0];
57+
let ret = unsafe {
58+
libc::recv(
59+
self.fd,
60+
buf.as_mut_ptr() as *mut _,
61+
buf.len(),
62+
libc::MSG_PEEK,
63+
)
64+
};
65+
if ret == -1 || ret != 1 {
66+
Err("socket peek failed")
67+
} else {
68+
Ok(Some(buf[0]))
69+
}
70+
}
5371
}
5472

5573
impl Drop for TcpConnection {
@@ -74,23 +92,6 @@ impl Connection for TcpConnection {
7492
}
7593
}
7694

77-
fn peek(&mut self) -> Result<Option<u8>, &'static str> {
78-
let mut buf = [0];
79-
let ret = unsafe {
80-
libc::recv(
81-
self.fd,
82-
buf.as_mut_ptr() as *mut _,
83-
buf.len(),
84-
libc::MSG_PEEK,
85-
)
86-
};
87-
if ret == -1 || ret != 1 {
88-
Err("socket peek failed")
89-
} else {
90-
Ok(Some(buf[0]))
91-
}
92-
}
93-
9495
fn flush(&mut self) -> Result<(), &'static str> {
9596
// huh, apparently flushing isn't a "thing" for Tcp streams.
9697
// see https://doc.rust-lang.org/src/std/net/tcp.rs.html#592-609

example_no_std/src/gdb.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use gdbstub::common::Tid;
22
use gdbstub::target;
3-
use gdbstub::target::ext::base::multithread::{
4-
GdbInterrupt, MultiThreadOps, ResumeAction, ThreadStopReason,
5-
};
3+
use gdbstub::target::ext::base::multithread::{MultiThreadOps, ResumeAction};
64
use gdbstub::target::{Target, TargetResult};
75

86
use crate::print_str::print_str;
@@ -42,13 +40,9 @@ impl Target for DummyTarget {
4240

4341
impl MultiThreadOps for DummyTarget {
4442
#[inline(never)]
45-
fn resume(
46-
&mut self,
47-
_default_resume_action: ResumeAction,
48-
_check_gdb_interrupt: GdbInterrupt<'_>,
49-
) -> Result<Option<ThreadStopReason<u32>>, Self::Error> {
43+
fn resume(&mut self) -> Result<(), Self::Error> {
5044
print_str("> resume");
51-
Ok(Some(ThreadStopReason::DoneStep))
45+
Ok(())
5246
}
5347

5448
#[inline(never)]

example_no_std/src/main.rs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
extern crate libc;
55

66
use gdbstub::state_machine::GdbStubStateMachine;
7+
use gdbstub::target::ext::base::multithread::ThreadStopReason;
78
use gdbstub::{DisconnectReason, GdbStubBuilder, GdbStubError};
89

910
mod conn;
@@ -18,8 +19,6 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
1819
}
1920

2021
fn rust_main() -> Result<(), i32> {
21-
// pretty_env_logger::init();
22-
2322
let mut target = gdb::DummyTarget::new();
2423

2524
let conn = match conn::TcpConnection::new_localhost(9001) {
@@ -41,34 +40,39 @@ fn rust_main() -> Result<(), i32> {
4140

4241
let mut gdb = gdb.run_state_machine().map_err(|_| 1)?;
4342

44-
loop {
43+
let res = loop {
4544
gdb = match gdb {
4645
GdbStubStateMachine::Pump(mut gdb) => {
4746
let byte = gdb.borrow_conn().read().map_err(|_| 1)?;
4847
match gdb.pump(&mut target, byte) {
49-
Ok((_, Some(disconnect_reason))) => {
50-
match disconnect_reason {
51-
DisconnectReason::Disconnect => print_str("GDB Disconnected"),
52-
DisconnectReason::TargetExited(_) => print_str("Target exited"),
53-
DisconnectReason::TargetTerminated(_) => print_str("Target halted"),
54-
DisconnectReason::Kill => print_str("GDB sent a kill command"),
55-
}
56-
break;
57-
}
48+
Ok((_, Some(disconnect_reason))) => break Ok(disconnect_reason),
49+
Ok((gdb, None)) => gdb,
50+
Err(e) => break Err(e),
51+
}
52+
}
53+
54+
GdbStubStateMachine::DeferredStopReason(gdb) => {
55+
match gdb.deferred_stop_reason(&mut target, ThreadStopReason::DoneStep) {
56+
Ok((_, Some(disconnect_reason))) => break Ok(disconnect_reason),
5857
Ok((gdb, None)) => gdb,
59-
Err(GdbStubError::TargetError(_e)) => {
60-
print_str("Target raised a fatal error");
61-
break;
62-
}
63-
Err(_e) => {
64-
print_str("gdbstub internal error");
65-
break;
66-
}
58+
Err(e) => break Err(e),
6759
}
6860
}
61+
}
62+
};
6963

70-
// example_no_std stubs out resume, so this will never happen
71-
GdbStubStateMachine::DeferredStopReason(_) => return Err(-1),
64+
match res {
65+
Ok(disconnect_reason) => match disconnect_reason {
66+
DisconnectReason::Disconnect => print_str("GDB Disconnected"),
67+
DisconnectReason::TargetExited(_) => print_str("Target exited"),
68+
DisconnectReason::TargetTerminated(_) => print_str("Target halted"),
69+
DisconnectReason::Kill => print_str("GDB sent a kill command"),
70+
},
71+
Err(GdbStubError::TargetError(_e)) => {
72+
print_str("Target raised a fatal error");
73+
}
74+
Err(_e) => {
75+
print_str("gdbstub internal error");
7276
}
7377
}
7478

examples/armv4t/emu.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@ const HLE_RETURN_ADDR: u32 = 0x12345678;
77

88
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
99
pub enum Event {
10+
DoneStep,
1011
Halted,
1112
Break,
1213
WatchWrite(u32),
1314
WatchRead(u32),
1415
}
1516

17+
pub enum ExecMode {
18+
Step,
19+
Continue,
20+
RangeStep(u32, u32),
21+
}
22+
1623
/// incredibly barebones armv4t-based emulator
1724
pub struct Emu {
1825
start_addr: u32,
1926

2027
// example custom register. only read/written to from the GDB client
2128
pub(crate) custom_reg: u32,
2229

30+
pub(crate) exec_mode: ExecMode,
31+
2332
pub(crate) cpu: Cpu,
2433
pub(crate) mem: ExampleMem,
2534

@@ -68,6 +77,8 @@ impl Emu {
6877

6978
custom_reg: 0x12345678,
7079

80+
exec_mode: ExecMode::Continue,
81+
7182
cpu,
7283
mem,
7384

@@ -84,6 +95,7 @@ impl Emu {
8495
self.cpu.reg_set(Mode::User, reg::CPSR, 0x10);
8596
}
8697

98+
/// single-step the interpreter
8799
pub fn step(&mut self) -> Option<Event> {
88100
let mut hit_watchpoint = None;
89101

@@ -114,4 +126,57 @@ impl Emu {
114126

115127
None
116128
}
129+
130+
/// run the emulator in accordance with the currently set `ExecutionMode`.
131+
///
132+
/// since the emulator runs in the same thread as the GDB loop, the emulator
133+
/// will use the provided callback to poll the connection for incoming data
134+
/// every 1024 steps.
135+
pub fn run(&mut self, mut poll_incoming_data: impl FnMut() -> bool) -> RunEvent {
136+
match self.exec_mode {
137+
ExecMode::Step => RunEvent::Event(self.step().unwrap_or(Event::DoneStep)),
138+
ExecMode::Continue => {
139+
let mut cycles = 0;
140+
loop {
141+
if cycles % 1024 == 0 {
142+
// poll for incoming data
143+
if poll_incoming_data() {
144+
break RunEvent::IncomingData;
145+
}
146+
}
147+
cycles += 1;
148+
149+
if let Some(event) = self.step() {
150+
break RunEvent::Event(event);
151+
};
152+
}
153+
}
154+
// just continue, but with an extra PC check
155+
ExecMode::RangeStep(start, end) => {
156+
let mut cycles = 0;
157+
loop {
158+
if cycles % 1024 == 0 {
159+
// poll for incoming data
160+
if poll_incoming_data() {
161+
break RunEvent::IncomingData;
162+
}
163+
}
164+
cycles += 1;
165+
166+
if let Some(event) = self.step() {
167+
break RunEvent::Event(event);
168+
};
169+
170+
if !(start..end).contains(&self.cpu.reg_get(self.cpu.mode(), reg::PC)) {
171+
break RunEvent::Event(Event::DoneStep);
172+
}
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
pub enum RunEvent {
180+
IncomingData,
181+
Event(Event),
117182
}

0 commit comments

Comments
 (0)