Skip to content

Commit 0949523

Browse files
Rework scheduler locking
This lays down some groundwork for v0.8, where the primary new feature will be multiprocess support, which will be similar to multithreaded support. As discussed on the multiprocess github support issue, there's a simpler way to support the scheduler locking behavior that we plan to use for multiprocess targets, and since we're breaking the API anyways with v0.8, it's a decent time to implement it for multithreaded targets as well.
1 parent 1bc505f commit 0949523

8 files changed

Lines changed: 119 additions & 112 deletions

File tree

docs/transition_guide.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ This document does _not_ discuss any new features that might have been added bet
66

77
> _Note:_ after reading through this doc, you may also find it helpful to refer to the in-tree `armv4t` and `armv4t_multicore` examples when transitioning between versions.
88
9+
## `0.7` -> `0.8`
10+
11+
#### Changes to multi-threaded resume behavior
12+
13+
Previously, if a thread had no `resume_set_action_XXX` methods called on it, the default was to assume `set_resume_action_continue` was called on it. Now, if a thread has no `set_resume_action_XXX` methods called on it, it should remain stopped.
14+
15+
This new behavior obsoletes the `MultiThreadSchedulerLocking` trait, which has been removed. If a stub is unable to handle executing a single thread and keeping all others locked, it should return an error in the `resume` method.
16+
917
## `0.6` -> `0.7`
1018

1119
`0.7` is a fairly minimal "cleanup" release, landing a collection of small breaking changes that collectively improve various ergonomic issues in `gdbstub`'s API.

example_no_std/src/gdb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ impl MultiThreadResume for DummyTarget {
149149
#[inline(never)]
150150
fn set_resume_action_continue(
151151
&mut self,
152-
_tid: Tid,
152+
_tid: Option<Tid>,
153153
_signal: Option<Signal>,
154154
) -> Result<(), Self::Error> {
155155
print_str("> set_resume_action_continue");

examples/armv4t_multicore/emu.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@ pub enum Event {
3636
WatchRead(u32),
3737
}
3838

39-
#[derive(PartialEq)]
39+
#[derive(Debug, PartialEq)]
4040
pub enum ExecMode {
4141
Step,
4242
Continue,
43-
Stop,
4443
}
4544

4645
/// incredibly barebones armv4t-based emulator
@@ -49,6 +48,7 @@ pub struct Emu {
4948
pub(crate) cop: Cpu,
5049
pub(crate) mem: ExampleMem,
5150

51+
// If a CpuId is not in this, it is presumed "stopped".
5252
pub(crate) exec_mode: HashMap<CpuId, ExecMode>,
5353

5454
pub(crate) watchpoints: Vec<u32>,
@@ -189,7 +189,7 @@ impl Emu {
189189
let mut evt = None;
190190

191191
for id in [CpuId::Cpu, CpuId::Cop].iter().copied() {
192-
if matches!(self.exec_mode.get(&id), Some(ExecMode::Stop)) {
192+
if !self.exec_mode.contains_key(&id) {
193193
continue;
194194
}
195195

@@ -204,11 +204,18 @@ impl Emu {
204204
}
205205

206206
pub fn run(&mut self, mut poll_incoming_data: impl FnMut() -> bool) -> RunEvent {
207+
if self.exec_mode.is_empty() {
208+
// This should never happen (gdbstub will always ensure at least one
209+
// `set_resume_action_XXX` method is called), but in case it does, we explicitly
210+
// log it and return the closest event that represents this.
211+
eprintln!("Running while all threads are stopped; this should never happen! Treating as 0 steps");
212+
return RunEvent::Event(Event::DoneStep, CpuId::Cpu);
213+
}
214+
207215
// The underlying armv4t_multicore emulator cycles all cores in lock-step.
208216
//
209217
// Inside `self.step()`, we iterate through all cores and only invoke
210-
// `step_core` if that core's `ExecMode` is not `Stop`.
211-
218+
// `step_core` if that core has an `ExecMode`.
212219
let should_single_step = self.exec_mode.values().any(|mode| mode == &ExecMode::Step);
213220

214221
match should_single_step {

examples/armv4t_multicore/gdb.rs

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ impl MultiThreadResume for Emu {
160160
}
161161

162162
fn clear_resume_actions(&mut self) -> Result<(), Self::Error> {
163+
// We're in all-stop mode (since `gdbstub` doesn't support non-stop yet), so
164+
// the fact that we're processing commands from the GDB client means that all
165+
// threads have already stopped, which is represented by the thread being
166+
// absent from the `exec_mode` map.
163167
self.exec_mode.clear();
164168
Ok(())
165169
}
@@ -173,25 +177,27 @@ impl MultiThreadResume for Emu {
173177

174178
fn set_resume_action_continue(
175179
&mut self,
176-
tid: Tid,
180+
tid: Option<Tid>,
177181
signal: Option<Signal>,
178182
) -> Result<(), Self::Error> {
179183
if signal.is_some() {
180184
return Err("no support for continuing with signal");
181185
}
182186

183-
self.exec_mode
184-
.insert(tid_to_cpuid(tid)?, ExecMode::Continue);
187+
match tid {
188+
None => {
189+
for id in [CpuId::Cpu, CpuId::Cop] {
190+
self.exec_mode.entry(id).or_insert(ExecMode::Continue);
191+
}
192+
}
193+
Some(tid) => {
194+
self.exec_mode
195+
.insert(tid_to_cpuid(tid)?, ExecMode::Continue);
196+
}
197+
}
185198

186199
Ok(())
187200
}
188-
189-
#[inline(always)]
190-
fn support_scheduler_locking(
191-
&mut self,
192-
) -> Option<target::ext::base::multithread::MultiThreadSchedulerLockingOps<'_, Self>> {
193-
Some(self)
194-
}
195201
}
196202

197203
impl target::ext::base::multithread::MultiThreadSingleStep for Emu {
@@ -301,15 +307,6 @@ impl target::ext::thread_extra_info::ThreadExtraInfo for Emu {
301307
}
302308
}
303309

304-
impl target::ext::base::multithread::MultiThreadSchedulerLocking for Emu {
305-
fn set_resume_action_scheduler_lock(&mut self) -> Result<(), Self::Error> {
306-
for id in [CpuId::Cpu, CpuId::Cop] {
307-
self.exec_mode.entry(id).or_insert(ExecMode::Stop);
308-
}
309-
Ok(())
310-
}
311-
}
312-
313310
/// Copy all bytes of `data` to `buf`.
314311
/// Return the size of data copied.
315312
pub fn copy_to_buf(data: &[u8], buf: &mut [u8]) -> usize {

src/protocol/commands/_vCont.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ impl<'a> ParseCommand<'a> for vCont<'a> {
2424
fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
2525
let body = buf.into_body();
2626
match body as &[u8] {
27+
// From https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#vCont-packet:
28+
// "Specifying no actions is an error."
29+
b"" => None,
2730
b"?" => Some(vCont::Query),
28-
_ => Some(vCont::Actions(Actions::new_from_buf(body))),
31+
[b';', rest @ ..] => Some(vCont::Actions(Actions::new_from_buf(rest))),
32+
// Anything that doesn't start with a semicolon is malformed
33+
_ => None
2934
}
3035
}
3136
}
@@ -52,7 +57,7 @@ impl<'a> Actions<'a> {
5257
Actions::FixedCont(tid)
5358
}
5459

55-
pub fn iter(&self) -> impl Iterator<Item = Option<VContAction<'a>>> + '_ {
60+
pub fn iter(&self) -> impl DoubleEndedIterator<Item = Option<VContAction<'a>>> + '_ {
5661
match self {
5762
Actions::Buf(x) => EitherIter::A(x.iter()),
5863
Actions::FixedStep(x) => EitherIter::B(core::iter::once(Some(VContAction {
@@ -67,12 +72,16 @@ impl<'a> Actions<'a> {
6772
}
6873
}
6974

75+
// This does not include the leading semicolon of the first action.
7076
#[derive(Debug)]
7177
pub struct ActionsBuf<'a>(&'a [u8]);
7278

7379
impl<'a> ActionsBuf<'a> {
74-
fn iter(&self) -> impl Iterator<Item = Option<VContAction<'a>>> + '_ {
75-
self.0.split(|b| *b == b';').skip(1).map(|act| {
80+
fn iter(&self) -> impl DoubleEndedIterator<Item = Option<VContAction<'a>>> + '_ {
81+
// `ActionsBuf` doesn't include the leading semicolon in the first
82+
// action, so we don't need to worry about the first element of the
83+
// split being empty.
84+
self.0.split(|b| *b == b';').map(|act| {
7685
let mut s = act.split(|b| *b == b':');
7786
let kind = s.next()?;
7887
let thread = match s.next() {
@@ -109,7 +118,7 @@ impl<'a> ActionsBuf<'a> {
109118
//
110119
// As a workaround for these weird GDB clients, `gdbstub`
111120
// takes the pragmatic approach of treating this request as
112-
// though it the client requested _all_ threads to be
121+
// though the client requested _all_ threads to be
113122
// resumed.
114123
//
115124
// If this turns out to be wrong... `gdbstub` can explore a
@@ -194,3 +203,17 @@ where
194203
}
195204
}
196205
}
206+
207+
impl<A, B, T> DoubleEndedIterator for EitherIter<A, B>
208+
where
209+
A: DoubleEndedIterator<Item = T>,
210+
B: DoubleEndedIterator<Item = T>,
211+
{
212+
#[inline(always)]
213+
fn next_back(&mut self) -> Option<T> {
214+
match self {
215+
EitherIter::A(a) => a.next_back(),
216+
EitherIter::B(b) => b.next_back(),
217+
}
218+
}
219+
}

src/stub/core_impl/resume.rs

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
8989
) -> Result<(), Error<T::Error, C::Error>> {
9090
use crate::protocol::commands::_vCont::VContKind;
9191

92+
// In the single-threaded scenario, we don't reverse the actions like we
93+
// do in the multi-threaded: there are only two scenarios we concern
94+
// ourselves with: 1 action, or 2 actions where the second is a
95+
// continue action (sometimes GDB sends a packet of the form
96+
// `vCont;s:foo;c`, even in single-threaded scenarios). We ignore the
97+
// continue action, since there aren't any other threads to continue.
9298
let mut actions = actions.iter();
9399
let first_action = actions
94100
.next()
@@ -166,14 +172,16 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
166172
) -> Result<(), Error<T::Error, C::Error>> {
167173
ops.clear_resume_actions().map_err(Error::TargetError)?;
168174

169-
// Track whether the packet contains a wildcard/default continue action
170-
// (e.g., `c` or `c:-1`).
175+
// NOTE: We iterate through these actions in reverse order, which corresponds to
176+
// a right-to-left ordering of the actions specified in the vCont packet. This
177+
// is intentionally the opposite of the left-to-right order specified by
178+
// the vCont packet documentation.
171179
//
172-
// Presence of this action implies "Scheduler Locking" is OFF.
173-
// Absence implies "Scheduler Locking" is ON.
174-
let mut has_wildcard_continue = false;
175-
176-
for action in actions.iter() {
180+
// This is to simplify target implementations: each `set_resume_action_XXX`
181+
// callback can overwrite the current state, instead of having to keep track of
182+
// each thread specified by previous actions and making sure they don't get
183+
// overwritten.
184+
for action in actions.iter().rev() {
177185
use crate::protocol::commands::_vCont::VContKind;
178186

179187
let action = action.ok_or(Error::PacketParse(
@@ -187,17 +195,15 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
187195
_ => None,
188196
};
189197

190-
match action.thread.map(|thread| thread.tid) {
191-
// An action with no thread-id matches all threads
192-
None | Some(SpecificIdKind::All) => {
193-
// Target API contract specifies that the default
194-
// resume action for all threads is continue.
195-
has_wildcard_continue = true;
196-
}
197-
Some(SpecificIdKind::WithId(tid)) => ops
198-
.set_resume_action_continue(tid, signal)
199-
.map_err(Error::TargetError)?,
200-
}
198+
let tid = match action.thread.map(|thread| thread.tid) {
199+
// An action with no thread-id matches all threads, which is passed to
200+
// `set_resume_action_continue` as `None`.
201+
None | Some(SpecificIdKind::All) => None,
202+
Some(SpecificIdKind::WithId(tid)) => Some(tid),
203+
};
204+
205+
ops.set_resume_action_continue(tid, signal)
206+
.map_err(Error::TargetError)?;
201207
}
202208
VContKind::Step | VContKind::StepWithSig(_)
203209
if ops.support_single_step().is_some() =>
@@ -259,15 +265,6 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
259265
}
260266
}
261267

262-
if !has_wildcard_continue {
263-
let Some(locking_ops) = ops.support_scheduler_locking() else {
264-
return Err(Error::MissingMultiThreadSchedulerLocking);
265-
};
266-
locking_ops
267-
.set_resume_action_scheduler_lock()
268-
.map_err(Error::TargetError)?;
269-
}
270-
271268
ops.resume().map_err(Error::TargetError)
272269
}
273270

src/stub/error.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ pub(crate) enum InternalError<T, C> {
4343
MissingCurrentActivePidImpl,
4444
TracepointFeatureUnimplemented(u8),
4545
TracepointUnsupportedSourceEnumeration,
46-
MissingMultiThreadSchedulerLocking,
4746
MissingToRawId,
4847

4948
// Internal - A non-fatal error occurred (with errno-style error code)
@@ -149,7 +148,6 @@ where
149148
MissingCurrentActivePidImpl => write!(f, "GDB client attempted to attach to a new process, but the target has not implemented support for `ExtendedMode::support_current_active_pid`"),
150149
TracepointFeatureUnimplemented(feat) => write!(f, "GDB client sent us a tracepoint packet using feature {}, but `gdbstub` doesn't implement it. If this is something you require, please file an issue at https://github.com/daniel5151/gdbstub/issues", *feat as char),
151150
TracepointUnsupportedSourceEnumeration => write!(f, "The target doesn't support the gdbstub TracepointSource extension, but attempted to transition to enumerating tracepoint sources"),
152-
MissingMultiThreadSchedulerLocking => write!(f, "GDB requested Scheduler Locking, but the Target does not implement the `MultiThreadSchedulerLocking` IDET"),
153151
MissingToRawId => write!(f, "A RegId was used with an API that requires raw register IDs to be available (e.g. `report_stop_with_regs`) but returned `None` from `to_raw_id()`"),
154152

155153
NonFatalError(_) => write!(f, "Internal non-fatal error. You should never see this! Please file an issue if you do!"),

0 commit comments

Comments
 (0)