[][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 implemented Targets.
    • 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 and remove_breakpoints methods.
    • 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.

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 of Target enables using Target's associated types.
/// `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 original Target trait by adding a new Target method that simply returns self 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, returning Some(self) to "enable" an extension, or None 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.
  • 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.

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 target extended-remote, such as spawning new processes and/or attaching to existing processes.

monitor_cmd

Create custom target-specific debugging commands accessible via GDB's monitor command!

section_offsets

Get section/segment relocation offsets from the target.