This document provides a brief overview of breaking changes between major gdbstub
releases, along with tips/tricks/suggestions on how to migrate between gdbstub
releases.
This document does not discuss any new features that might have been added between releases. For a comprehensive overview of what's been added to gdbstub
(as opposed to what's changed), check out the CHANGELOG.md
.
Note: after reading through this doc, you may also find it helpful to refer to the in-tree
armv4t
andarmv4t_multicore
examples when transitioning between versions.
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.
The breaking changes introduced in 0.7
are generally trivial to fix, and porting from 0.6
to 0.7
shouldn't take more than ~10 minutes, at most.
stub::GdbStubError
is now an opaque struct
with a handful of methods to extract user-defined context.
Please file an issue if your code required matching on concrete error variants aside from TargetError
and ConnectionError
!.
In contrast with the old version - which was an enum
that directly exposed all error internals to the user - this new type will enable future versions of gdbstub
to fearlessly improve error infrastructure without requiring semver breaking changes. See #112 for more.
Assuming you stuck to the example error handling described in the gdbstub
getting started guide, adapting to the new type should be quite straightforward.
// ==== 0.6.x ==== //
match gdb.run_blocking::<EmuGdbEventLoop>(&mut emu) {
Ok(disconnect_reason) => { ... },
Err(GdbStubError::TargetError(e)) => {
println!("target encountered a fatal error: {}", e)
}
Err(e) => {
println!("gdbstub encountered a fatal error: {}", e)
}
}
// ==== 0.7.0 ==== //
match gdb.run_blocking::<EmuGdbEventLoop>(&mut emu) {
Ok(disconnect_reason) => { ... },
Err(e) => {
if e.is_target_error() {
println!(
"target encountered a fatal error: {}",
e.into_target_error().unwrap()
)
} else if e.is_connection_error() {
let (e, kind) = e.into_connection_error().unwrap();
println!("connection error: {:?} - {}", kind, e,)
} else {
println!("gdbstub encountered a fatal error: {}", e)
}
}
}
read_addrs
now returns a usize
instead of a ()
, allowing implementations to report cases where only a subset of memory could be read.
In the past, the only way to handle these cases was by returning a TargetError
. This provides an alternative mechanism, which may or may not be more appropriate for your particular use-case.
When upgrading, the Rust compiler will emit a clear error message pointing out the updated function signature. The fix should be trivial.
See #132 for more discussion on why this API was removed.
This change only affects you if you're maintaining a custom Arch
implementation (vs. using a community-maintained one via gdbstub_arch
).
The fix here is to simply remove the Arch::single_step_behavior
impl.
That's it! It's that easy.
0.6
introduces a large number of breaking changes to the public APIs, and will require quite a bit more more "hands on" porting than previous gdbstub
upgrades.
The following guide is a best-effort attempt to document all the changes, but there are some parts that may be missing / incomplete.
Many types have been renamed, and many import paths have changed in 0.6
.
Exhaustively listing them would be nearly impossible, but suffice it to say, you will need to tweak your imports.
Note: If you haven't implemented
Connection
yourself (i.e: you are using one of the built-inConnection
impls onTcpStream
/UnixStream
), you can skip this section.
The blocking read
method and non-blocking peek
methods have been removed from the base Connection
API, and have been moved to a new ConnectionExt
type.
For more context around this change, please refer to Moving from GdbStub::run
to GdbStub::run_blocking
.
Porting a 0.5
Connection
to 0.6
is incredibly straightforward - you simply split your existing implementation in two:
// ==== 0.5.x ==== //
impl Connection for MyConnection {
type Error = MyError;
fn write(&mut self, byte: u8) -> Result<(), Self::Error> { .. }
fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { .. }
fn read(&mut self) -> Result<u8, Self::Error> { .. }
fn peek(&mut self) -> Result<Option<u8>, Self::Error> { .. }
fn flush(&mut self) -> Result<(), Self::Error> { .. }
fn on_session_start(&mut self) -> Result<(), Self::Error> { .. }
}
// ==== 0.6.0 ==== //
impl Connection for MyConnection {
type Error = MyError;
fn write(&mut self, byte: u8) -> Result<(), Self::Error> { .. }
fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { .. }
fn flush(&mut self) -> Result<(), Self::Error> { .. }
fn on_session_start(&mut self) -> Result<(), Self::Error> { .. }
}
impl ConnectionExt for MyConnection {
type Error = MyError;
fn read(&mut self) -> Result<u8, Self::Error> { .. }
fn peek(&mut self) -> Result<Option<u8>, Self::Error> { .. }
}
Note: If you haven't implemented
Arch
yourself (i.e: you are any of theArch
impls fromgdbstub_arch
), you can skip this section.
The Arch
API has had one breaking changes: The RegId::from_raw_id
method's "register size" return value has been changed from usize
to Option<NonZeroUsize>
.
If the register size is Some
, gdbstub
will include a runtime check to ensures that the target implementation does not send back more bytes than the register allows when responding to single-register read requests.
If the register size is None
, gdbstub
will omit this runtime check, and trust that the target's implementation of read_register
is correct.
Porting advice: If your Arch
implementation targets a specific architecture, it is highly recommended that you simply wrap your existing size value with Some
. This API change was made to support dynamic Arch
implementations, whereby the behavior of the Arch
varies on the runtime state of the program (e.g: in multi-system emulators), and there is not "fixed" register size per id.
All IDET methods have been prefixed with supports_
, to make it easier to tell at-a-glance which methods are actual handler methods, and which are simply IDET plumbing.
As such, when porting target code from 0.5
to 0.6
, before you dive into any functional changes, you should take a moment to find and rename any methods that have had their name changed.
In prior versions of gdbstub
, signals were encoded as raw u8
values. This wasn't very user-friendly, as it meant users had to manually locate the signal-to-integer mapping table themselves when working with signals in code.
0.6
introduces a new enum Signal
which encodes this information within gdbstub
itself.
This new Signal
type has replaced u8
in any places that a u8
was used to represent a signal, such as in StopReason::Signal
, or as part of the various resume
APIs.
Porting advice: The Rust compiler should catch any type errors due to this change, making it easy to swap out any instances of u8
with the new Signal
type.
The watchpoint API has been updated to include a new length
parameter, specifying what range of memory addresses the watchpoint should encompass.
In an effort to unify the implementations of various new qXfer
-backed protocol extensions, the existing TargetXmlOverride
has been changed from returning a &str
value to using a std::io::Read
-style "write the data into a &mut [u8]
buffer" API.
Porting a 0.5
TargetDescriptionXmlOverride
to 0.6
is straightforward, though a bit boilerplate-y.
// ==== 0.5.x ==== //
impl target::ext::target_description_xml_override::TargetDescriptionXmlOverride for Emu {
fn target_description_xml(&self) -> &str {
r#"<target version="1.0"><!-- custom override string --><architecture>armv4t</architecture></target>"#
}
}
// ==== 0.6.0 ==== //
pub fn copy_to_buf(data: &[u8], buf: &mut [u8]) -> usize {
let len = data.len();
let buf = &mut buf[..len];
buf.copy_from_slice(data);
len
}
pub fn copy_range_to_buf(data: &[u8], offset: u64, length: usize, buf: &mut [u8]) -> usize {
let offset = match usize::try_from(offset) {
Ok(v) => v,
Err(_) => return 0,
};
let len = data.len();
let data = &data[len.min(offset)..len.min(offset + length)];
copy_to_buf(data, buf)
}
impl target::ext::target_description_xml_override::TargetDescriptionXmlOverride for Emu {
fn target_description_xml(
&self,
offset: u64,
length: usize,
buf: &mut [u8],
) -> TargetResult<usize, Self> {
let xml = r#"<target version="1.0"><!-- custom override string --><architecture>armv4t</architecture></target>"#
.trim()
.as_bytes();
Ok(copy_range_to_buf(xml, offset, length, buf))
}
}
0.6
includes three fairly major behavioral changes to the resume
method:
There are quite a few use cases where it might make sense to debug a target that does not support resumption, e.g: a post-mortem debugging session, or when debugging crash dumps. In these cases, past version of gdbstub
would force the user to nonetheless implement "stub" methods for resuming these targets, along with forcing users to pay the "cost" of including all the handler code related to resumption (of which there is quite a bit.)
In 0.6
, all resume-related functionality has been extracted out of {Single,Multi}ThreadBase
, and split into new {Singe,Multi}ThreadResume
IDETs.
The GDB protocol only requires that targets implement support for continuing execution - support for instruction-level single-step execution is totally optional.
Note: this isn't actually true in practice, thanks to a bug in the mainline GDB client... See the docs for
Target::use_optional_single_step
for details...
To model this behavior, 0.6
has split single-step support into its own IDET, in a manner similar to how optimized range step support was handled in 0.5
.
In doing so, the enum ResumeAction
type could be removed entirely, as single-step resume was to be handled in its own method.
In past versions of gdbstub
, the resume
API would block the thread waiting for the target to hit some kind of stop condition. In this model, checking for pending GDB interrupts was quite unergonomic, requiring that the thread periodically wake up and check whether an interrupt has arrived via the GdbInterrupt
type.
gdbstub
0.6
introduces a new paradigm of driving target execution, predicated on the idea that the target's resume
method does not block, instead yielding execution immediately, and deferring the responsibility of "selecting" between incoming stop events and GDB interrupts to higher levels of the gdbstub
"stack".
In practice, this means that much of the logic that used to live in the resume
implementation will now move into upper-levels of the gdbstub
API, with the resume
API serving more of a "bookkeeping" purpose, recording what kind of resumption mode the GDB client has requested from the target, while not actually resuming the target itself.
For more context around this change, please refer to Moving from GdbStub::run
to GdbStub::run_blocking
.
Much of the code contained within methods such as block_until_stop_reason_or_interrupt
will be lifted into upper layers of the gdbstub
API, leaving behind just a small bit of code in the target's resume
method to perform "bookkeeping" regarding how the GDB client requested the target to be resumed.
// ==== 0.5.x ==== //
impl SingleThreadOps for Emu {
fn resume(
&mut self,
action: ResumeAction,
gdb_interrupt: GdbInterrupt<'_>,
) -> Result<StopReason<u32>, Self::Error> {
match action {
ResumeAction::Step => self.do_single_step(),
ResumeAction::Continue => self.block_until_stop_reason_or_interrupt(action, || gdb_interrupt.pending()),
_ => self.handle_resume_with_signal(action),
}
}
}
// ==== 0.6.0 ==== //
impl SingleThreadBase for Emu {
// resume has been split into a separate IDET
#[inline(always)]
fn support_resume(
&mut self
) -> Option<SingleThreadResumeOps<Self>> {
Some(self)
}
}
impl SingleThreadResume for Emu {
fn resume(
&mut self,
signal: Option<Signal>,
) -> Result<(), Self::Error> { // <-- no longer returns a stop reason!
if let Some(signal) = signal {
self.handle_signal(signal)?;
}
// upper layers of the `gdbstub` API will be responsible for "driving"
// target execution - `resume` simply performs book keeping on _how_ the
// target should be resumed.
self.set_execution_mode(ExecMode::Continue)?;
Ok(())
}
// single-step support has been split into a separate IDET
#[inline(always)]
fn support_single_step(
&mut self
) -> Option<SingleThreadSingleStepOps<'_, Self>> {
Some(self)
}
}
impl SingleThreadSingleStep for Emu {
fn step(&mut self, signal: Option<Signal>) -> Result<(), Self::Error> {
if let Some(signal) = signal {
self.handle_signal(signal)?;
}
self.set_execution_mode(ExecMode::Step)?;
Ok(())
}
}
With the introduction of the new state-machine API, the responsibility of reading incoming has been lifted out of gdbstub
itself, and is now something implementations are responsible for . The alternative approach would've been to have Connection
include multiple different read
-like methods for various kinds of paradigms - such as async
/await
, epoll
, etc...
TODO. In the meantime, I would suggest looking at rustdoc for details on how to use
GdbStub::run_blocking
...
While the overall structure of the API has remained the same, 0.5.0
does introduce a few breaking API changes that require some attention. That being said, it should not be a difficult migration, and updating to 0.5.0
from 0.4
shouldn't take more than 10 mins of refactoring.
The various breakpoint IDETs that were previously directly implemented on the top-level Target
trait have now been consolidated under a single Breakpoints
IDET. This is purely an organizational change, and will not require rewriting any existing {add, remove}_{sw_break,hw_break,watch}point
implementations.
Porting from 0.4
to 0.5
should be as simple as:
// ==== 0.4.x ==== //
impl Target for Emu {
fn sw_breakpoint(&mut self) -> Option<target::ext::breakpoints::SwBreakpointOps<Self>> {
Some(self)
}
fn hw_watchpoint(&mut self) -> Option<target::ext::breakpoints::HwWatchpointOps<Self>> {
Some(self)
}
}
impl target::ext::breakpoints::SwBreakpoint for Emu {
fn add_sw_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> { ... }
fn remove_sw_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> { ... }
}
impl target::ext::breakpoints::HwWatchpoint for Emu {
fn add_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> { ... }
fn remove_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> { ... }
}
// ==== 0.5.0 ==== //
impl Target for Emu {
// (New Method) //
fn breakpoints(&mut self) -> Option<target::ext::breakpoints::BreakpointsOps<Self>> {
Some(self)
}
}
impl target::ext::breakpoints::Breakpoints for Emu {
fn sw_breakpoint(&mut self) -> Option<target::ext::breakpoints::SwBreakpointOps<Self>> {
Some(self)
}
fn hw_watchpoint(&mut self) -> Option<target::ext::breakpoints::HwWatchpointOps<Self>> {
Some(self)
}
}
// (Almost Unchanged) //
impl target::ext::breakpoints::SwBreakpoint for Emu {
// /-- New `kind` parameter
// \/
fn add_sw_breakpoint(&mut self, addr: u32, _kind: arch::arm::ArmBreakpointKind) -> TargetResult<bool, Self> { ... }
fn remove_sw_breakpoint(&mut self, addr: u32, _kind: arch::arm::ArmBreakpointKind) -> TargetResult<bool, Self> { ... }
}
// (Unchanged) //
impl target::ext::breakpoints::HwWatchpoint for Emu {
fn add_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> { ... }
fn remove_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> { ... }
}
Single-register access methods ({read,write}_register
) are now a separate SingleRegisterAccess
trait
Single register access is not a required part of the GDB protocol, and as such, has been moved out into its own IDET. This is a purely organizational change, and will not require rewriting any existing {read,write}_register
implementations.
Porting from 0.4
to 0.5
should be as simple as:
// ==== 0.4.x ==== //
impl SingleThreadOps for Emu {
fn read_register(&mut self, reg_id: arch::arm::reg::id::ArmCoreRegId, dst: &mut [u8]) -> TargetResult<(), Self> { ... }
fn write_register(&mut self, reg_id: arch::arm::reg::id::ArmCoreRegId, val: &[u8]) -> TargetResult<(), Self> { ... }
}
// ==== 0.5.0 ==== //
impl SingleThreadOps for Emu {
// (New Method) //
fn single_register_access(&mut self) -> Option<target::ext::base::SingleRegisterAccessOps<(), Self>> {
Some(self)
}
}
impl target::ext::base::SingleRegisterAccess<()> for Emu {
// /-- New `tid` parameter (ignored on single-threaded systems)
// \/
fn read_register(&mut self, _tid: (), reg_id: arch::arm::reg::id::ArmCoreRegId, dst: &mut [u8]) -> TargetResult<(), Self> { ... }
fn write_register(&mut self, _tid: (), reg_id: arch::arm::reg::id::ArmCoreRegId, val: &[u8]) -> TargetResult<(), Self> { ... }
}
In 0.4
, resuming a multithreaded target was done using an Actions
iterator passed to a single resume
method. In hindsight, this approach had a couple issues:
- It was impossible to statically enforce the property that the
Actions
iterator was guaranteed to return at least one element, often forcing users to manuallyunwrap
- The iterator machinery was quite heavy, and did not optimize very effectively
- Handling malformed packets encountered during iteration was tricky, as the user-facing API exposed an infallible iterator, thereby complicating the internal error handling
- Adding new kinds of
ResumeAction
(e.g: range stepping) required a breaking change, and forced users to change theirresume
method implementation regardless whether or not their target ended up using said action.
In 0.5
, the API has been refactored to address some of these issues, and the single resume
method has now been split into multiple "lifecycle" methods:
resume
- As before, when
resume
is called the target should resume execution. - But how does the target know how each thread should be resumed? That's where the next method comes in...
- As before, when
set_resume_action
- This method is called prior to
resume
, and notifies the target how a particularTid
should be resumed.
- This method is called prior to
- (optionally)
set_resume_action_range_step
- If the target supports optimized range-stepping, it can opt to implement the newly added
MultiThreadRangeStepping
IDET which includes this method. - Targets that aren't interested in optimized range-stepping can skip this method!
- If the target supports optimized range-stepping, it can opt to implement the newly added
clear_resume_actions
- After the target returns a
ThreadStopReason
fromresume
, this method will be called to reset the previously set per-tid
resume actions.
- After the target returns a
NOTE: This change does mean that targets are now responsible for maintaining some internal state that maps Tid
s to ResumeAction
s. Thankfully, this isn't difficult at all, and can as simple as maintaining a HashMap<Tid, ResumeAction>
.
Please refer to the in-tree armv4t_multicore
example for an example of how this new resume
flow works.