gdbstub Documentation

repository·master·Indexed 19 days ago

https://github.com/daniel5151/gdbstub

An ergonomic implementation of the GDB Remote Serial Protocol in Rust (v0.7.10). Designed for emulators, hypervisors, and embedded projects, it offers full #![no_std] support with zero-allocation options and transport-layer agnosticism. The library utilizes Inlineable Dyn Extension Traits (IDETs) for zero-overhead protocol extensions and provides a Target trait for integrating memory, register, and thread enumeration capabilities.

Tokens
37.3K
Snippets
110
Records
179
Agent score
65%

What's inside gdbstub

  1. Use community-contributed architecture implementations in gdbstub

    master

    The gdbstub_arch crate provides community-contributed implementations of the gdbstub::arch::Arch trait for various hardware architectures. If you are working with a supported architecture, you can use these implementations to integrate with gdbstub immediately.

    Note on missing architectures: If your target architecture is not provided in this crate, you can still use gdbstub. As long as GDB supports your target architecture, you can manually implement the gdbstub::arch::Arch trait for your specific hardware.

  2. How the new MultiThreadOps::resume lifecycle works in 0.5

    master

    In version 0.5, the resume API for multithreaded targets was refactored from a single method using an iterator to a multi-step lifecycle. This allows for better error handling and avoids the overhead of heavy iterator machinery.

    Targets must now maintain internal state (such as a HashMap<Tid, ResumeAction>) to track how each thread should behave when execution resumes.

    The Resume Lifecycle

    1. set_resume_action(tid, action): The caller notifies the target of the specific ResumeAction for a given Tid.
    2. set_resume_action_range_step(...) (Optional): If the target implements MultiThreadRangeStepping, this method can be used for optimized range-stepping.
    3. resume(): The caller triggers the target to resume execution. The target uses its stored actions to resume threads.
    4. clear_resume_actions(): After resume() returns a ThreadStopReason, the caller triggers this method to reset the internal mapping of Tids to ResumeActions.
  3. Handle stub::GdbStubError changes in 0.7

    master

    In 0.7, stub::GdbStubError is no longer an enum that can be matched on directly. It is now an opaque struct. To handle specific error types, you must use the provided inspection and conversion methods.

    If your code previously matched on concrete error variants, you should now use:

    • e.is_target_error() and e.into_target_error() to handle target-specific errors.
    • e.is_connection_error() and e.into_connection_error() to handle connection-specific errors.

    Note: If your logic relied on matching on error variants other than TargetError or ConnectionError, you should file an issue with the project.

    // ==== 0.6.x (Old way) ====
    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 (New way) ====
    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)
            }
        }
    }
  4. How Inlineable Dyn Extension Traits (IDETs) work

    master

    Instead of using compile-time Cargo feature flags to enable GDB protocol extensions, gdbstub uses Inlineable Dyn Extension Traits (IDETs).

    This technique provides:

    • Zero-overhead: Unused protocol features are guaranteed to be dead-code-eliminated in release builds. For example, if you do not implement a custom GDB monitor command handler, the code for parsing qRcmd packets will not be included in your binary.
    • Fine-grained control: You can toggle enabled protocol features at runtime/via traits without relying on clunky feature flags or unsafe code.
    • Type safety: Protocol invariants are enforced via Rust's type system.
  5. Ensure panic-free execution in embedded/no_std environments

    master

    To ensure gdbstub does not introduce additional panics into your project (which is critical for low-level debuggers where panics are difficult to debug), the following conditions must be met:

    1. Compile in release mode: Optimization is required for the compiler to omit certain panic checks.
    2. Disable paranoid_unsafe: LLVM often requires unsafe code to successfully omit certain panic checks.
    3. Use a panic-free Arch implementation: While gdbstub core aims to be panic-free, implementations found in gdbstub_arch are not guaranteed to be panic-free.

    Note: It is recommended to manually verify the generated assembly output using tools like example_no_std/dump_asm.sh to ensure panicking paths are optimized out.

  6. Migrate `resume` and single-step APIs from 0.5 to 0.6

    master

    The resume API has undergone major behavioral changes in 0.6:

    1. Optional Resumption: Support for resume is now optional. It has been extracted from {Single,Multi}ThreadBase into new {Single,Multi}ThreadResume IDETs.
    2. Optional Single-Stepping: Single-step support is now its own IDET, and enum ResumeAction has been removed.
    3. Non-blocking resume: The resume method no longer blocks waiting for a stop condition. It now performs 'bookkeeping' (recording the requested execution mode) and returns immediately. The responsibility of driving execution and handling interrupts has moved to higher levels of the gdbstub stack.

    Porting Example:

    // ==== 0.6.0 ==== //
    
    impl SingleThreadBase for Emu {
        #[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> { 
            if let Some(signal) = signal {
                self.handle_signal(signal)?;
            }
            self.set_execution_mode(ExecMode::Continue)?;
            Ok(())
        }
    
        #[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(())
        }
    }
    // ==== 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 its own 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(())
        }
    }
  7. Use Unix Domain Sockets (UDS) for debugging

    master

    On Unix-like systems, you can run the emulator using the --uds flag. This binds the GdbStub to a Unix Domain Socket at /tmp/armv4t_gdb instead of using loopback TCP, which can provide a snappier debugging experience. Note that this feature is only available on Unix-like operating systems.

    cargo run --example armv4t --features=std -- --uds
  8. Run the armv4t-multicore example

    master

    To run the dual-core ARMv4T emulator example, use cargo run with the armv4t example and the std feature enabled. This example demonstrates gdbstub's multi-process support using a contrived dual-core architecture.

    If debug symbols fail to load in GDB, ensure you have the arm-none-eabi toolchain installed, rebuild test.elf locally, and recompile the example.

    cargo run --example armv4t --features=std
  9. Migrate from gdbstub 0.4 to 0.5

    master

    Upgrading from version 0.4 to 0.5 involves three main breaking changes related to how breakpoints, register access, and thread resumption are handled. While the core logic remains similar, the organizational structure of the traits has changed.

    1. Consolidating Breakpoint IDETs

    Breakpoint operations have moved from the top-level Target trait into a consolidated Breakpoints IDET.

    • Old way: Implement sw_breakpoint and hw_watchpoint directly on Target.
    • New way: Implement breakpoints() on Target which returns BreakpointsOps. Then, implement the Breakpoints trait on your target type to provide sw_breakpoint() and hw_watchpoint() implementations.
    • Note: SwBreakpoint methods (like add_sw_breakpoint) now include a new kind parameter (e.g., arch::arm::ArmBreakpointKind).

    2. Moving Single-Register Access to a Separate Trait

    Single register access is no longer part of the core SingleThreadOps but is moved to the SingleRegisterAccess IDET.

    • Old way: Implement read_register and write_register directly on SingleThreadOps.
    • New way: Implement single_register_access() on SingleThreadOps to return SingleRegisterAccessOps. Then, implement the SingleRegisterAccess trait.
    • Note: The read_register and write_register methods now include a tid parameter (which can be ignored on single-threaded systems).

    3. Refactoring MultiThreadOps::resume API

    The single resume method that used an Actions iterator has been replaced by a lifecycle-based approach to improve error handling and performance. Targets are now responsible for maintaining internal state (e.g., a HashMap<Tid, ResumeAction>) to map thread IDs to their respective actions.

    The new lifecycle flow is:

    1. set_resume_action: Called prior to resume to notify the target how a specific Tid should be resumed.
    2. set_resume_action_range_step (Optional): Implement MultiThreadRangeStepping if the target supports optimized range-stepping.
    3. resume: Called to trigger execution. The target uses its internal state to determine how to resume each thread.
    4. clear_resume_actions: Called after resume returns a ThreadStopReason to reset the per-tid actions.
    // ==== 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> { ... }
    }
    
    // ==== 0.4.x (Register Access) ====
    
    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 (Register Access) ====
    
    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> { ... }
    }
  10. Configure gdbstub for no_std environments

    master

    gdbstub is a no_std first library and does not require dynamic memory allocation. To use it in a #![no_std] context (e.g., on a resource-constrained microcontroller), you must disable default features in your Cargo.toml to avoid dependencies on std and alloc.

    Use the following configuration:

    [dependencies]
    gdbstub = { version = "0.7.10", default-features = false }

    In these environments, you can configure gdbstub to use fixed-size, pre-allocated buffers via GdbStubBuilder::with_packet_buffer to avoid any requirement for an allocator.

  11. Explore gdbstub in-tree examples

    master

    The repository includes several 'toy' examples maintained in CI to demonstrate gdbstub usage and API capabilities. These are useful for learning how to implement protocol extensions or using the library in no_std environments.

    • armv4t (./examples/armv4t/): A simple ARMv4T system emulator. It implements nearly all available target::ext features, making it the primary resource for learning how to implement new protocol extensions.
    • armv4t_multicore (./examples/armv4t_multicore/): A dual-core version of the armv4t example that demonstrates the core of the multithread extensions API.
    • example_no_std (./example_no_std): An extremely minimal example for #![no_std] projects. It does not include an emulator but stubs all gdbstub functions. It is also used to track binary footprint and validate dead-code-elimination.