[−][src]Module gdbstub::target::ext
Extensions to Target
which add support for various
subsets of the GDB Remote Serial Protocol.
On it's own, the Target
trait doesn't actually include
any methods to debug the target. Instead, Target
uses a collection of
"Inlineable Dyn Extension Traits" (IDETs) to optionally implement various
subsets of the GDB protocol. For more details on IDETs, scroll down to the
How Protocol Extensions Work - Inlineable Dyn Extension Traits
(IDETs)
section below.
As a starting point, consider implementing some of the extensions under
breakpoints
. For example, adding support for Software Breakpoints would
require implementing the
breakpoints::SwBreakpoint
extension, and
overriding the Target::sw_breakpoint
method to return Some(self)
.
Note: Missing Protocol Extensions
gdbstub
's development is guided by the needs of it's contributors, with
new features being added on an "as-needed" basis.
If there's a GDB feature you need that hasn't been implemented yet, (e.g: remote filesystem access, tracepoint support, etc...), consider opening an issue / filing a PR on Github!
Check out the GDB Remote Configuration Docs for a table of GDB commands + their corresponding Remote Serial Protocol packets.
Note: What's with all the <Self::Arch as Arch>::
syntax?
Many of the method signatures across the Target
extension traits include
some pretty gnarly type syntax.
If rust-lang/rust#38078
gets fixed, then types like <Self::Arch as Arch>::Foo
could be simplified
to just Self::Arch::Foo
. Until then, the much more explicit
fully qualified syntax
must be used instead.
When you come across this syntax, it's highly recommended to use the
concrete type instead. e.g: on a 32-bit target, instead of cluttering up
the implementation with <Self::Arch as Arch>::Usize
, just use u32
directly.
How Protocol Extensions Work - Inlineable Dyn Extension Traits (IDETs)
The GDB protocol is massive, and contains all sorts of optional
functionality. In previous versions of gdbstub
, the Target
trait would
directly have a method for every single protocol extension, resulting in
literally hundreds of associated methods!
This approach had numerous drawbacks:
- Implementations that did not implement all available protocol extensions
still had to "pay" for the unused packet parsing/handler code, resulting
in substantial code bloat, even on
no_std
platforms. - Required the
GdbStub
implementation to include runtime checks to deal with incorrectly implementedTarget
s.- No way to enforce "mutually-dependent" trait methods at compile-time.
- e.g: When implementing hardware breakpoint extensions, targets
must implement both the
add_breakpoint
andremove_breakpoints
methods.
- e.g: When implementing hardware breakpoint extensions, targets
must implement both the
- No way to enforce "mutually-exclusive" trait methods at compile-time.
- e.g: The
resume
method for single-threaded targets has a much simpler API than for multi-threaded targets, but it would be incorrect for a target to implement both.
- e.g: The
- No way to enforce "mutually-dependent" trait methods at compile-time.
Starting from version 0.4.0
, gdbstub
is taking a new approach to
implementing and enumerating available Target features, using a technique
called Inlineable Dyn Extension Traits.
Author's note: As far as I can tell, this isn't a very well-known trick, or at the very least, I've personally never encountered any library that uses this sort of API. As such, I've decided to be a bit cheeky and give it a name! At some point, I'm hoping to write a standalone blog post which further explores this technique, comparing it to other/existing approaches, and diving into details of the how the compiler optimizes this sort of code.
So, what are "Inlineable Dyn Extension Traits"? Well, let's break it down:
- Extension Traits - A common Rust convention to extend the functionality of a Trait, without modifying the original trait.
- Dyn - Alludes to the use of Dynamic Dispatch via Trait Objects.
- Inlineable - Alludes to the fact that this approach can be easily inlined, making it a truly zero-cost abstraction.
In a nutshell, Inlineable Dyn Extension Traits (or IDETs) are an abuse of the Rust trait system + modern compiler optimizations to emulate zero-cost, runtime-query-able optional trait methods!
Technical overview
The basic principles behind Inlineable Dyn Extension Traits are best explained though example:
Lets say we want to add an optional protocol extension described by an
OptExt
trait to the Target
trait. How would we do that using IDETs?
- (library) Define a
trait OptExt: Target { ... }
with all the optional methods:- Making
OptExt
a supertrait ofTarget
enables usingTarget
's associated types.
- Making
/// `foo` and `bar` are mutually-dependent methods. trait OptExt: Target { fn foo(&self); // can use associated types in method signature! fn bar(&mut self) -> Result<(), Self::Error>; }
- (library) "Tie" the
OptExt
extension trait to the originalTarget
trait by adding a newTarget
method that simply returnsself
cast to a&mut dyn OptExt
:
trait Target { // Optional extension fn ext_optfeat(&mut self) -> Option<OptExtOps<Self>> { // disabled by default None } // Mutually-exclusive extensions fn ext_a_or_b(&mut self) -> EitherOrExt<Self::Arch, Self::Error>; } // Using a typedef for readability type OptExtOps<T> = &'a mut dyn OptExt<Arch = <T as Target>::Arch, Error = <T as Target>::Error>; enum EitherOrExt<A, E> { OptExtA(&'a mut dyn OptExtA<Arch = A, Error = E>), OptExtB(&'a mut dyn OptExtB<Arch = A, Error = E>), }
- (user) Implements the
OptExt
extension for their target (just like a normal trait).
impl OptExt for Target { fn foo(&self) { ... } fn bar(&mut self) -> Result<(), Self::Error> { ... } }
- (user) Implements the base
Target
trait, returningSome(self)
to "enable" an extension, orNone
to leave it disabled.
impl Target for MyTarget { // Optional extension - Always enabled fn ext_optfeat(&mut self) -> Option<OptExtOps<Self>> { Some(self) // will not compile unless `MyTarget` also implements `OptExt` } // Mutually-exclusive extensions fn ext_a_or_b(&mut self) -> EitherOrExt<Self::Arch, Self::Error> { EitherOrExt::OptExtA(self) } }
If the user didn't implement OptExt
, but tried to return Some(self)
,
they'll get an error similar to:
error[E0277]: the trait bound `MyTarget: OptExt` is not satisfied
--> path/to/implementation.rs:44:14
|
44 | Some(self)
| ^^^^ the trait `OptExt` is not implemented for `MyTarget`
|
= note: required for the cast to the object type `dyn OptExt<Arch = ..., Error = ...>`
- (library) Can now query whether or not the extension is available, without having to actually invoke any method on the target!
// in a method that accepts `target: impl Target` match target.ext_optfeat() { Some(ops) => ops.cool_feature(), None => { /* do nothing */ } }
Moreover, if you take a look at the generated assembly (e.g: using
godbolt.org), you'll find that the compiler is able to efficiently inline
and devirtualize all the single-line ext_
methods, which in-turn allows
the dead-code-eliminator to work it's magic, and remove the unused branches
from the generated code! i.e: If a target didn't implement the OptExt
extension, then that match
statement would be converted into a noop!
Check out daniel5151/optional-trait-methods for some sample code that shows off the power of IDETs. It includes code snippets which can be pasted into godbolt.org directly to confirm the optimizations described above.
Optimizing compilers really are magic!
Summary: The Benefits of IDETs
IDETs solve the numerous issues and shortcomings that arise from the traditional single trait + "optional" methods approach:
- Compile-time enforcement of mutually-dependent methods
- By grouping mutually-dependent methods behind a single extension trait and marking them all as required methods, the Rust compiler is able to catch missing mutually-dependent methods at compile time, with no need for any runtime checks!
- Compile-time enforcement of mutually-exclusive methods
- By grouping mutually-exclusive methods behind two extension traits, and
wrapping those in an
enum
, the API is able to document mutually-exclusive functions at the type-level, in-turn enabling the library to omit any runtime checks! - Note: Strictly speaking, this isn't really compile time "enforcement", as there's nothing stopping an "adversarial" implementation from implementing both sets of methods, and then "flipping" between the two at runtime. Nonetheless, it serves as a good guardrail.
- By grouping mutually-exclusive methods behind two extension traits, and
wrapping those in an
- Enforce dead-code-elimination without
cargo
feature flags- This is a really awesome trick: by wrapping code in a
if target.ext_optfeat().is_some()
block, it's possible to specify arbitrary blocks of code to be feature-dependent! - This is used to great effect in
gdbstub
to optimize-out any packet parsing / handler code for unimplemented protocol extensions.
- This is a really awesome trick: by wrapping code in a
Modules
base | Base operations required to debug any target (read/write memory/registers, step/resume, etc...) |
breakpoints | Add/Remove various kinds of breakpoints. |
extended_mode | Enables Extended Mode
functionality when connecting using |
monitor_cmd | Create custom target-specific debugging commands accessible via GDB's
|
section_offsets | Get section/segment relocation offsets from the target. |