Appendix C: Debugging Techniques
This appendix covers the essential debugging tools and techniques for UEFI and Project Mu firmware development. From simple debug prints to full source-level debugging with GDB, these methods will help you diagnose issues across all boot phases.
C.1 The DEBUG Macro
The DEBUG macro is the most fundamental debugging tool in UEFI. It is defined in DebugLib.h and controlled by PCDs.
Basic Usage
#include <Library/DebugLib.h>
DEBUG ((DEBUG_INFO, "MyDriver: entry point called\n"));
DEBUG ((DEBUG_WARN, "MyDriver: buffer size %d exceeds limit\n", Size));
DEBUG ((DEBUG_ERROR, "MyDriver: LocateProtocol failed - %r\n", Status));
Error Levels
| Level | Value | Purpose |
|---|---|---|
DEBUG_INIT |
0x00000001 | Initialization messages |
DEBUG_WARN |
0x00000002 | Warnings |
DEBUG_LOAD |
0x00000004 | Module load/unload |
DEBUG_FS |
0x00000008 | Filesystem operations |
DEBUG_POOL |
0x00000010 | Memory pool allocation |
DEBUG_PAGE |
0x00000020 | Page allocation |
DEBUG_INFO |
0x00000040 | General information |
DEBUG_DISPATCH |
0x00000080 | Driver dispatch |
DEBUG_VARIABLE |
0x00000100 | Variable operations |
DEBUG_BM |
0x00000400 | Boot manager |
DEBUG_BLKIO |
0x00001000 | Block I/O operations |
DEBUG_NET |
0x00004000 | Network stack |
DEBUG_UNDI |
0x00010000 | UNDI driver |
DEBUG_LOADFILE |
0x00020000 | Load file operations |
DEBUG_EVENT |
0x00080000 | Event/timer |
DEBUG_GCD |
0x00100000 | GCD operations |
DEBUG_CACHE |
0x00200000 | Cache operations |
DEBUG_VERBOSE |
0x00400000 | Verbose output |
DEBUG_ERROR |
0x80000000 | Error messages (always shown) |
PCD Configuration
Control what gets printed with these PCDs in your DSC:
[PcdsFixedAtBuild]
# Which debug levels are enabled (bitmask)
gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80400040
# 0x80000000 = DEBUG_ERROR
# 0x00400000 = DEBUG_VERBOSE
# 0x00000040 = DEBUG_INFO
# Debug property mask (features enabled)
gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x2F
# Bit 0: DEBUG_PROPERTY_DEBUG_ASSERT_ENABLED
# Bit 1: DEBUG_PROPERTY_DEBUG_PRINT_ENABLED
# Bit 2: DEBUG_PROPERTY_DEBUG_CODE_ENABLED
# Bit 3: DEBUG_PROPERTY_CLEAR_MEMORY_ENABLED
# Bit 5: DEBUG_PROPERTY_ASSERT_DEADLOOP_ENABLED
DebugLib Implementations
| Library INF | Output Target | Best For |
|---|---|---|
BaseDebugLibNull |
Nowhere (disabled) | Release builds |
BaseDebugLibSerialPort |
Serial port (COM1) | Hardware, QEMU |
UefiDebugLibConOut |
Console (screen) | Interactive debugging |
UefiDebugLibDebugPortProtocol |
Debug port protocol | Debugger integration |
PeiDxeDebugLibReportStatusCode |
Status code output | Production firmware |
C.2 Serial Output Configuration
Serial output is the most reliable debug channel, working from SEC through Runtime.
QEMU Serial Setup
# Output serial to terminal
qemu-system-x86_64 \
-bios OVMF.fd \
-serial stdio \
-nographic
# Output serial to file
qemu-system-x86_64 \
-bios OVMF.fd \
-serial file:debug.log \
-display gtk
# Output serial to TCP socket
qemu-system-x86_64 \
-bios OVMF.fd \
-serial tcp:localhost:1234,server,nowait
DSC Configuration for Serial Debug
[LibraryClasses]
DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
SerialPortLib|PcAtChipsetPkg/Library/SerialIoLib/SerialIoLib.inf
[PcdsFixedAtBuild]
# Serial port base address (0x3F8 = COM1)
gEfiMdeModulePkgTokenSpaceGuid.PcdSerialRegisterBase|0x3F8
# Baud rate
gEfiMdeModulePkgTokenSpaceGuid.PcdSerialBaudRate|115200
C.3 ASSERT Macro
ASSERT halts execution when a condition is false (in DEBUG builds):
#include <Library/DebugLib.h>
ASSERT (Buffer != NULL);
ASSERT (Size > 0 && Size <= MAX_BUFFER_SIZE);
ASSERT_EFI_ERROR (Status); // Asserts if Status is an error
// Return value assertion
ASSERT_RETURN_ERROR (ReturnValue); // For RETURN_STATUS
When an assert fires, the debug output shows:
ASSERT [MyDriver] /path/to/MyDriver.c(123): Buffer != NULL
If DEBUG_PROPERTY_ASSERT_DEADLOOP_ENABLED is set, the CPU enters a dead loop, which is ideal for attaching a debugger.
C.4 GDB with QEMU
QEMU’s built-in GDB stub enables source-level debugging of UEFI firmware.
Step 1: Launch QEMU with GDB Server
qemu-system-x86_64 \
-bios OVMF_CODE.fd \
-serial stdio \
-s \ # Enable GDB server on port 1234
-S \ # Freeze CPU at startup (wait for GDB)
-nographic
Step 2: Connect GDB
gdb
(gdb) target remote localhost:1234
(gdb) continue
Step 3: Load Debug Symbols
UEFI modules are loaded at runtime-determined addresses. You need to find the load address and load symbols accordingly:
# From the UEFI debug log, find the image base:
# Loading driver at 0x00007E8F000 EntryPoint=0x00007E90ABC MyDriver.efi
# In GDB:
(gdb) add-symbol-file Build/MyPlatform/DEBUG_GCC5/X64/MyDriver/MyDriver/DEBUG/MyDriver.debug 0x00007E8F000
Automated Symbol Loading
The OVMF debug log prints module load addresses. A helper script can parse these:
#!/bin/bash
# load_symbols.sh - Parse OVMF debug log and generate GDB commands
LOG_FILE="debug.log"
BUILD_DIR="Build/MyPlatform/DEBUG_GCC5/X64"
grep "Loading driver" "$LOG_FILE" | while read -r line; do
ADDR=$(echo "$line" | grep -oP '0x[0-9A-Fa-f]+' | head -1)
MODULE=$(echo "$line" | grep -oP '\S+\.efi' | sed 's/.efi//')
DEBUG_FILE=$(find "$BUILD_DIR" -name "${MODULE}.debug" 2>/dev/null | head -1)
if [ -n "$DEBUG_FILE" ]; then
echo "add-symbol-file $DEBUG_FILE $ADDR"
fi
done
Common GDB Commands for UEFI
(gdb) break MyDriverEntryPoint # Set breakpoint by function name
(gdb) break *0x7E90ABC # Set breakpoint by address
(gdb) info registers # View CPU registers
(gdb) x/32xw 0x7E8F000 # Examine memory (32 words)
(gdb) x/16xg $rsp # Examine stack (16 quad-words)
(gdb) print gST->FirmwareVendor # Print UEFI variable (with symbols)
(gdb) set *(UINT32 *)0x7E91000 = 0x42 # Modify memory
(gdb) stepi # Step one instruction
(gdb) nexti # Step over call
(gdb) continue # Resume execution
(gdb) bt # Backtrace
C.5 QEMU Monitor Commands
The QEMU monitor provides low-level system inspection. Access it with Ctrl-A C in -nographic mode, or via the monitor tab in GUI mode.
| Command | Description |
|---|---|
info registers |
Dump all CPU registers |
info mem |
Show page table mappings |
info tlb |
Show TLB entries |
info mtree |
Show memory region tree |
info pci |
Show PCI device tree |
x/Nx addr |
Examine N words at address |
xp /Nx paddr |
Examine physical address |
gdbserver |
Start GDB server on the fly |
stop |
Pause guest CPU |
cont |
Resume guest CPU |
log unimp |
Log unimplemented feature accesses |
savevm name |
Save VM snapshot |
loadvm name |
Restore VM snapshot |
Example: inspecting the UEFI System Table:
(qemu) xp /4xg 0x7F9E018 # Examine System Table header
(qemu) info pci # Check PCI enumeration
C.6 Phase-Specific Debugging
SEC Phase Debugging
SEC runs from flash with no memory. Debugging options are limited:
- Port 80h POST codes: Write to I/O port 0x80 for progress tracking
- Serial output: If serial is initialized in SEC
- LED/GPIO toggles: On physical hardware
// Post code output
IoWrite8 (0x80, 0x01); // SEC entry
IoWrite8 (0x80, 0x02); // CAR initialized
IoWrite8 (0x80, 0x03); // Entering PEI
PEI Phase Debugging
PEI runs with limited memory (initially just Cache-as-RAM). Use:
// PEI debug print (with PeiServicesLib)
DEBUG ((DEBUG_INFO, "PEI: memory discovered, %d MB\n",
MemorySize / (1024 * 1024)));
// Report status code for progress
REPORT_STATUS_CODE (
EFI_PROGRESS_CODE,
(EFI_SOFTWARE_PEI_MODULE | EFI_SW_PEI_CORE_PC_HANDOFF_TO_NEXT)
);
DXE Phase Debugging
DXE has full memory and serial support. This is the easiest phase to debug:
// Rich debug output
DEBUG ((DEBUG_INFO, "DXE Driver loaded at 0x%p\n", ImageHandle));
DEBUG ((DEBUG_INFO, " Protocol count: %d\n", ProtocolCount));
// Use ASSERT for development
ASSERT_EFI_ERROR (Status);
// Conditional debug code
DEBUG_CODE_BEGIN ();
DumpProtocolDatabase ();
DEBUG_CODE_END ();
SMM Debugging
SMM debugging is the most challenging due to the isolated execution environment:
- SMM runs in its own address space (SMRAM)
- Standard serial output may not work from SMM
- Use
SmmDebugLibfor serial output from SMM - QEMU can be configured to allow GDB debugging of SMM
# DSC: SMM-specific debug library
[LibraryClasses.common.DXE_SMM_DRIVER]
DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
C.7 Visual Studio Debugging (Windows)
For developers using Visual Studio on Windows:
Build with MSFT Toolchain
stuart_build -c PlatformBuild.py TOOL_CHAIN_TAG=VS2022
Attach to QEMU
- Start QEMU with
-s -Sflags. - In Visual Studio: Debug > Attach to Process > Connection Type: Remote (no authentication).
- Enter
localhost:1234as the qualifier. - Load symbols from the build output directory.
WinDbg with OVMF
For kernel-style debugging, WinDbg can connect to QEMU:
qemu-system-x86_64 \
-bios OVMF.fd \
-serial pipe:com_1 \
-m 256
Then in WinDbg: File > Kernel Debug > COM > Pipe > \\.\pipe\com_1
C.8 Shell-Based Debugging
The UEFI Shell provides several built-in debugging commands:
| Command | Description |
|---|---|
drivers |
List all loaded drivers with handles |
dh |
Display handle database |
dh -p <protocol> |
Find handles with a specific protocol |
devtree |
Display device tree hierarchy |
memmap |
Display memory map |
dmpstore |
Dump UEFI variable store |
smbiosview |
Display SMBIOS tables |
acpiview |
Display ACPI tables |
pci |
List PCI devices |
mm |
Read/write memory or I/O ports |
dmem |
Display memory contents |
Examples:
# List all drivers
Shell> drivers
# Show handles with PCI I/O protocol
Shell> dh -p PciIo
# Dump memory at address
Shell> dmem 0x7F9E000 0x100
# Read PCI config space
Shell> pci 00 1F 00
# Dump all UEFI variables
Shell> dmpstore -all
# Write to I/O port
Shell> mm 0x3F8 0x41 -IO -w 1
C.9 Common Debugging Patterns
Pattern 1: Status Code Tracing
// Track progress through a complex initialization
DEBUG ((DEBUG_INFO, "[MyDriver] Phase 1: Locate protocols\n"));
Status = LocateRequiredProtocols ();
DEBUG ((DEBUG_INFO, "[MyDriver] Phase 1: %r\n", Status));
DEBUG ((DEBUG_INFO, "[MyDriver] Phase 2: Initialize hardware\n"));
Status = InitializeHardware ();
DEBUG ((DEBUG_INFO, "[MyDriver] Phase 2: %r\n", Status));
Pattern 2: Memory Dump Helper
VOID
DumpHex (
IN CONST VOID *Data,
IN UINTN Length
)
{
CONST UINT8 *Bytes = (CONST UINT8 *)Data;
UINTN Index;
for (Index = 0; Index < Length; Index++) {
if (Index % 16 == 0) {
DEBUG ((DEBUG_INFO, "\n %04X: ", Index));
}
DEBUG ((DEBUG_INFO, "%02X ", Bytes[Index]));
}
DEBUG ((DEBUG_INFO, "\n"));
}
Pattern 3: Protocol Availability Check
VOID
DebugCheckProtocol (
IN EFI_GUID *ProtocolGuid,
IN CHAR8 *Name
)
{
EFI_STATUS Status;
VOID *Interface;
Status = gBS->LocateProtocol (ProtocolGuid, NULL, &Interface);
DEBUG ((DEBUG_INFO, " %-30a: %r (0x%p)\n",
Name, Status, Interface));
}
// Usage:
DebugCheckProtocol (&gEfiPciIoProtocolGuid, "PciIo");
DebugCheckProtocol (&gEfiGraphicsOutputProtocolGuid, "GOP");
Pattern 4: Timing Measurement
UINT64
GetTimestamp (
VOID
)
{
return AsmReadTsc (); // x86 timestamp counter
}
// Usage:
UINT64 Start = GetTimestamp ();
DoExpensiveOperation ();
UINT64 End = GetTimestamp ();
DEBUG ((DEBUG_INFO, "Operation took %ld cycles\n", End - Start));
Pattern 5: Deadloop for Debugger Attach
// Place at a point where you want to attach a debugger
volatile BOOLEAN Wait = TRUE;
DEBUG ((DEBUG_INFO, "Waiting for debugger at 0x%p...\n", &Wait));
while (Wait) {
CpuPause ();
}
// In GDB: set *(BOOLEAN *)0xADDRESS = 0
C.10 Build Report Analysis
Generate a build report to understand PCD resolution, library mapping, and more:
build -p MyPlatform.dsc -a X64 -t GCC5 -b DEBUG \
-y report.txt \
-Y PCD -Y LIBRARY -Y DEPEX -Y BUILD_FLAGS
The report shows:
- Which library INF was selected for each library class
- Final PCD values and where they were set (DSC, DEC, command-line)
- Dependency expressions for dispatch ordering
- Exact compiler flags applied to each module
This is often the fastest way to diagnose “wrong library linked” or “wrong PCD value” issues.
Summary
UEFI debugging ranges from simple DEBUG prints to full source-level debugging with GDB. The key tools are: DEBUG macro with serial output for print-style debugging, GDB with QEMU for source-level debugging, QEMU monitor for system-level inspection, UEFI Shell commands for runtime state examination, and build reports for configuration analysis. Master these tools and you can diagnose issues at any boot phase.