Debugging

Source debugging in Patina is implemented as a self-hosted or "kernel" debugger that executes within the exception handlers in the system. These exception handlers will inspect the state of the system through memory, registers, and exception frames to present the debugger application with a snapshot of the system at the moment it took the exception. The communication between the exception handlers and the software debugger is implemented using the GDB Remote Protocol which is supported by a number of debugger applications. For instruction on configuring and using the debugger, see the debugging dev page.

The Patina debugger is a software debugger which, in contrast to a hardware or JTAG debugger, is implemented entirely within the patina software stack. This has many advantages such as being more flexible, accessible, and available but also means that it will have inherent limitations on its ability to debug all scenarios. Like with all debugging tools, it is a powerful tool but may not be the correct choice for all scenarios.

Below is a simplified diagram of the sequence of a debugger interaction, starting with the exception, continuing with the debugger operations, and ending with resuming from the exception.

---
config:
  layout: elk
  look: handDrawn
displayMode: compact
---
sequenceDiagram
  Box blue Patina Platform
  participant Patina
  participant ExceptionHandler as Patina Exception Handler
  participant Debugger
  participant GdbStub as Gdb Stub
  end
  participant DebuggerApp as Debugger Application

  Patina->>ExceptionHandler: Exception occurs (e.g. breakpoint)
  ExceptionHandler->>Debugger: Invoke debugger callback
  Debugger->>GdbStub: Notify break
  GdbStub->>DebuggerApp: Notify break
  DebuggerApp->>GdbStub: Read state (e.g. memory/registers)
  GdbStub->>Debugger: Read state
  Debugger->>GdbStub: Send result
  GdbStub->>DebuggerApp: Return result
  DebuggerApp->>GdbStub: Continue command
  GdbStub->>Debugger: Continue command
  Debugger->>ExceptionHandler: Return
  ExceptionHandler->>Patina: Return from exception

Prerequisites

The debugger relies on exception table information being preserved in order to get an accurate stack trace. Two flags are needed to tell the toolchain to preserve this information, one for Rust, one for C code.

Rust:

This needs to be set in the environment variables for the build.

RUSTFLAGS=-Cforce-unwind-tables

C:

This needs to be set in the platform DSC's build options section:

*_*_*_GENFW_FLAGS = --keepexceptiontable

Structures

The debugger consists of two high-level structures: the debugger struct itself and the transport. The debugger implements the debugging logic and exception handling while the transport handles the physical communication to the debugger application.

Debugger Struct

The debugger is primary implemented through a 'static struct, PatinaDebugger, which is instantiated and configured by the platform code, but will be initialized by the core. This allows the platform to setup the appropriate transport and configurations prior to Patina. However, as Patina will control the exception handlers framework, the debugger cannot be initialized until that is available during early initialization. This struct must be 'static as it will be registered as an exception handler which cannot assume any ownership or lifetimes as they will occur independent of the previous executing thread.

Because of the need for a 'static lifetime, global access, and generic implementation based on the transport, the debugger will be set to a static variable. Access to debugger functions is done through static routines that will internally access the globally installed debugger, if it exists. This is a contrasting design to other Patina services because of the unique integration of the debugger in core initialization.

Debug Transport

For the self-hosted debugger to communicate with the debugging software, such as Windbg, there needs to be a physical line of communication between the system under debug and the host machine. This transport should implement the SerialIO trait to provide a simple mechanism to read or write to the bus. This transport may be the same transport used by the logging console or it may be a dedicated UART or other serial connection. Most devices should be able to use a standard UART implementation from the SDK as the transport, providing the correct interface configurations.

Phases of the Debugger

The debugger has two primary phases of operation: initialization where it prepares the debugging infrastructure, and exception handling where it will inspect system state and communicate with the debugger application.

Initialization

Initialization of the debugger consists of two major operations: allocating resources and configuration exception handlers.

A number of buffers are required for the debugger to function such as the GDB and monitor buffers used for parsing input and preparing responses. These buffers are pre-allocated because memory should not be allocated while actively broken into the debugger as the state of the system is unknown should be minimally altered by the presence of the debugger. This ensures a consistent view of the system under debug as well as prevent debugger instability with reliance on systems in an unknown state.

The exception handlers are where the debugger is "broken-in" and is actively inspecting system state and communicating with the debugger application. These are configured during initialization so whenever an exception is taken, such as a breakpoint or an access violation, it will cause the debugger to be invoked.

Initial Breakpoint

If enabled, at the end of initialization the debugger will invoke a hard-coded breakpoint instruction, causing the CPU to take an exception invoking the debugger exception handlers. This is referred to as the Initial Breakpoint. The initial breakpoint is intended to give the developer time to connect to the system as early as possible in order to setup breakpoints or otherwise interact with the debugger before any further execution takes place.

Support is planned to allow the initial breakpoint to have a timeout such that if a connection is not established within a configured time, the system will continue execution. This will allow scenarios where the debugger is enabled but only used occasionally.

Exception Handling

All actual debugging occurs within the exception handlers installed during initialization. This will rely on the Patina InterruptManager to capture the executing context during the initial exception, storing register state in a struct, before calling out to the registered debugger exception handler. At this point, the debugger will send a message over the transport to notify the debugger application that a break has occurred. From this point forward, the application will dictate all operations that occur. The following are the primary operations requested by the application.

  • Querying system/architecture information
  • Reading/writing registers
  • Reading/writing memory
  • Executing monitor commands
  • Setting/clearing breakpoints
  • Continuing or stepping

The self-hosted debugger will perform these operations but does not have the greater context that the application does. e.g. the self-hosted debugger may be asked to set memory address to a certain value, but only the application knows the significance of the memory address - be it a variable, stack, or other structure. The self-hosted debugger just performs rudimentary inspected/alteration of system state and is not responsible for understanding why these things are done.

Operations

While broken-in the Patina debugger is responsible for communicating with the application and performing various operations to support debugging scenarios. This communication and the resulting operations are detailed in this section.

GDB Stub

The GDB Remote Protocol implementation for a debugger is often referred to as the GDB Stub. For the Patina debugger, the gdbstub crate is used. This crate handles all of the protocol packet interpretation and creation and calls out to a target structure provided by the Patina debugger to handle the debugging operations such as reading/writing registers. Additionally a custom gdbstub_arch is used to align to the UEFI interrupt context structures.

The GDB protocol was selected because it is a robust communication standard that is open and supported by many debug applications, including Windbg, GDB, LLDB, etc. The GDB protocol is ascii based and so is not as performant as binary protocols such as those used by Windbg natively. Other protocols may be supported in the future to accommodate this and other shortcomings.

Register Access

During the exception, the Patina exception handler will capture the register state into a stack variable and provide this to the Patina debugger exception handler routines. This captured state is what will be presented to the application as the current state of the system, providing a snapshot of the registers at the point of the exception. Any change to this structure will be restored faithfully by the Patina exception handler code when the exception stack unwinds so that any change to these registers while in the debugger will take affect when returning from the exception.

Notably, this does not include most system registers or MSRs. Any alteration to these registers will take immediate effect (and may impact the debugger operation).

Memory Access

Unlike register state, memory inspected by the debugger is the actual memory. For this reason, the debugger should attempt to be minimally invasive or reliant on other stateful services in the core as it could cause torn state or inconsistent debugging results. The debugger currently requires that all memory access be mapped, but it will temporarily fix-up write access to memory as needed.

Important

Alteration to memory used by the debugger while the debugger is broken-in may cause unexpected behavior.

Breakpoints

There are several types of breakpoints that are configurable on the system.

Software Breakpoints - These are the most common type of dynamic breakpoints configured by the debugger and are the default for generic breakpoint instructions sent from the application. Software breakpoints are breakpoint instructions that the debugger will inject into the instruction stream, directly replacing the original instruction. For example, on x64 the debugger will replace the instruction with an int 3 instruction for the address of the breakpoint. When broken in, the application will typically temporarily remove the breakpoint so that this behavior is transparent to the user. Resuming from a software breakpoint must be done with care as a simple continue would either take the exception again or leave the instruction stream unaltered preventing the breakpoint from functioning in the future. So the the application will typically step beyond the broken instruction before setting the breakpoint again.

Break Instructions - These are permanent and compile time breakpoint instructions in the underlying code. In Patina this will typically be achieved by calling patina_debugger::breakpoint(). When resuming from a breakpoint, the debugger will inspect the exception address to see if it contains a hardcoded breakpoint, and if so it will increment the program counter to skip the instruction. Otherwise the system would never be able to make progress from a breakpoint instruction.

Data Breakpoints - Also known as watchpoints, these are the only supported form of hardware breakpoint, where debug registers are configured to cause an exception on access to a specific address. The hardware is responsible for creating the exception in these cases. These are used to capture reads or write to specific memory.

Module Breakpoints - These are simply break instruction, but can be conceptually considered their own entity. Module breaks are configured to cause the debugger to break in when a specific module is loaded. This is achieved through a callout from the core each time a new module is loaded. This is useful for developers who want to debug a specific module as it gives them a chance to set breakpoints prior to the module being executed.

Monitor Commands

Monitor commands are implementation interpreted commands in the GDB remote protocol through the qRcmd packet. These locally interpreted commands are useful for operations that do not have standard or well-supported remote protocol functions but require local code execution on the system. Some examples for this could be:

  • Altering debugger behavior such as mapping checks.
  • Querying the state of the debugger.
  • Querying arbitrary system registers.
  • Configuring module breakpoints.
  • Environment specific extensions.

The debugger allows for external code to register for monitor callbacks to allows for Patina or components to add their own monitor commands through the add_monitor_command routine. As these commands are executed from the exception handler, special care should be taken to avoid memory allocations or other global state alterations. It is possible to have commands that alter global state or allocate memory, but your mileage may vary depending on system state, e.g. the system may hang.

Continuing execution

When a step or continue instruction is received, the debugger will resume from the exception. This is done by returning from the exception callback where the generic Patina exception handling code will then restore register state and return from the exception.

If the continuing command is a step instruction, the debugger will set debug registers to indicate to the processor that after a single instruction has been executed, an exception should be taken. On X64 this is the Trap Flag in the RFlags, and on AArch64 this a combination of the SS bit in the MDSCR and the SS bit in the SPSR. These bits will always be cleared on break to ensure the system doesn't get stuck stepping.

Configuring the Debugger

Configuring the debugger is left to the platform as the decision on when and how to enable the debugger has environment, security, and other considerations that are specific to a platform and its use case. There are two supported methods for enabling the debugger: hard-coded enablement through use of the enablement routines in the PatinaDebugger struct.

Direct configuration through the PatinaDebugger initialization can be useful for quick configuration during development or controlled configuration through a platform designed mechanism. Thie configuration is done through the with_default_config routine allowing the caller to set enablement, initial breakpoint, and the initial breakpoint timeout.

Important

Debugger enablement on release platforms can be dangerous. It is critical that platforms that use the debugger ensure that enablement cannot be done without proper configuration or authorization.

Debugger Applications

While the GDB remote protocol is supported by other debugger applications, the Patina development has been primarily focused on Windbg support. This is because Windbg provides strong support for systems programming concepts as well as strong PDB support, which patina relies on.

Windbg Integrations

Windbg supports the GDB interface through an EXDI extension. This implementation uses a small subset of the full GDB protocol, but is sufficient for most operations. To supplement this support, the UefiExt extension has been modified to support the Patina debugger. The extension is critical for the developer experience while using Windbg.

Other Debugger Applications

While the debugger is designed to be compatible with GDB and other debugger applications that support the GDB protocol, no investment has currently been made into testing and tooling. Future support here would be welcomed as the need arises.