DXE Phase: Drivers, Protocols, and reading logs when nothing runs

Deep dive into DXE: dispatcher, DEPEX, Driver Binding, protocol database, and how to use serial log, POST codes, and UEFI Shell to debug real failures.

Updated 11 min read
Đọc bằng English Tiếng Việt 日本語
BIOS / UEFI cover

DXE is the phase you’ll debug the most

Flash a new BIOS, and the NVMe is still visible in the OS but no longer appears in the UEFI boot menu. The driver built cleanly, the source had no errors, the image flashed successfully. Where’s the bug: FDF, DEPEX, Driver Binding, or BDS?

This is the kind of failure that’s typical in DXE. Not because DXE breaks more than other phases, but because DXE is where everything converges: drivers run here, devices get enumerated here, protocols get installed here. When something goes wrong, DXE is often where the symptom first appears — even if the root cause is earlier.

My work is customizing BIOS for POS systems, kiosks, and payment terminals. That environment has no room for “debug slowly”: when a machine dies in a store during peak hours, you need to identify the failing layer fast.

This article follows exactly that perspective: reading DXE from a debug angle — what the log says, where POST codes land, and which layer to check when a device doesn’t come up.


What DXE does, from a systems view

When PEI hands off, DXE Core receives a HOB list describing the memory map and firmware volumes. From there, DXE Core:

  1. Builds memory services (AllocatePool, AllocatePages, …)
  2. Starts the DXE Dispatcher
  3. Dispatcher scans firmware volumes, checks DEPEX, dispatches drivers
  4. Each driver installs protocols onto the handle database
  5. New protocols → waiting drivers get unlocked → more dispatch
  6. Eventually transitions to BDS

Key point: DXE does not run drivers in file order. Dispatch order follows the dependency chain — who needs what protocol, and whether that protocol is installed yet.


POST codes in DXE: reading from port 0x80

A POST code (checkpoint) is a byte the firmware writes to a debug output — on most x86 platforms this is I/O port 0x80, or an equivalent path the platform/vendor provides — to signal which step it’s at. This is the first debug tool when a system hangs before serial log is available.

For AMI Aptio 5.x, from public AMI documentation, the DXE range is:

RangeMeaning
0x60–0x8FDXE execution (before BDS)
0x90–0xCFBDS execution
0xD0–0xDFDXE errors

Key DXE checkpoints:

CodeDescription
0x60DXE Core starting
0x61NVRAM initialization
0x62South Bridge Runtime Services
0x63CPU DXE initialization begins
0x68PCI host bridge initialization
0x69North Bridge DXE init
0x6ANorth Bridge DXE SMM init
0x70South Bridge DXE init
0x71South Bridge DXE SMM init
0x72South Bridge devices init

DXE error codes:

CodeDescription
0xD0CPU initialization error
0xD1North Bridge initialization error
0xD2South Bridge initialization error
0xD3Some Architectural Protocols not available
0xD4PCI resource allocation error
0xD5No space for Legacy Option ROM
0xD6No Console Output Devices found
0xD7No Console Input Devices found
0xD8Invalid password
0xD9Error loading Boot Option (LoadImage failed)
0xDABoot Option not active

Serial log: the main tool once past PEI

POST codes tell you which step, but not why it failed. Serial log is where the detailed information lives.

EDK II / MdePkg DEBUG macro

In firmware based on the UEFI/PI model or using EDK II-compatible tooling, logs typically follow the pattern of MdePkg’s DEBUG() macro. AMI Aptio and Insyde H2O have their own codebases and toolchains, but the way to read log levels and status is usually very similar:

DEBUG ((DEBUG_INFO, "MyDriver: LocateProtocol returned %r\n", Status));DEBUG ((DEBUG_ERROR, "MyDriver: Fatal error at line %d\n", __LINE__));

%r is an EDK II format specifier that prints EFI_STATUS as a string — "Success", "Not Found", "Invalid Parameter" — instead of a hex number. Very useful when reading logs.

Debug levels (ErrorLevel)

Each message has a level. The level filter is set via PCD PcdDebugPrintErrorLevel:

DefineValueMeaning
DEBUG_INIT0x00000001Initialization
DEBUG_WARN0x00000002Warnings
DEBUG_LOAD0x00000004Image load events
DEBUG_FS0x00000008File system
DEBUG_POOL0x00000010Alloc/Free pool
DEBUG_PAGE0x00000020Alloc/Free pages
DEBUG_INFO0x00000040General info
DEBUG_DISPATCH0x00000080Dispatcher info
DEBUG_VARIABLE0x00000100Variable operations
DEBUG_BM0x00000400Boot Manager
DEBUG_ERROR0x80000000Errors (always shown)

Key patterns when reading serial log

When you have serial log, look for these patterns:

Driver entry point:

Loading driver: MyDriverDxeMyDriver: Entry point called

DEPEX not satisfied, driver not dispatched: If DXE dispatcher logging is enabled (DEBUG_DISPATCH), you may see:

DEPEX not satisfied for: MyDriverDxe.efi  Needs: gEfiPciIoProtocolGuid (not installed yet)

Protocol installed successfully:

MyDriver: InstallProtocolInterface success, Handle=0x...

Protocol locate failed:

MyDriver: LocateProtocol(gEfiBlockIoProtocolGuid) = Not Found

DXE Dispatcher: why a driver doesn’t run

This is the most common DXE question: “The driver built successfully — why didn’t the entry point run?”

Three most common causes:

1. Not in the FV/FDF

# DSC — this driver will be built[Components]  MyPkg/MyDriverDxe/MyDriverDxe.inf# FDF — NOT in the firmware volume → not in the ROM[FV.MAIN_FV]  # MyPkg/MyDriverDxe/MyDriverDxe.inf  ← missing this line

The driver built and the .efi file exists, but it was not packaged into the firmware image. The DXE Dispatcher never sees it.

2. DEPEX not yet satisfied

# In the driver's INF[Depex]  gEfiPciRootBridgeIoProtocolGuid AND  gEfiCpuArchProtocolGuid

If gEfiPciRootBridgeIoProtocolGuid has not been installed when the dispatcher runs its first pass, this driver is held back. Each time a new driver installs a protocol, the dispatcher runs again to see if any held drivers can now be unlocked.

Common reasons DEPEX isn’t satisfied:

  • The dependency protocol genuinely doesn’t exist yet (the driver providing it is also waiting or hasn’t run)
  • A typo in the GUID in DEPEX: doesn’t match the GUID the other driver installed

3. Driver ran but log is filtered

The entry point was called, but all messages use DEBUG_INFO while PcdDebugPrintErrorLevel doesn’t include that bit. It looks like the driver didn’t run, but it’s actually running silently.

Checklist when a driver doesn't run


Driver Binding: why a device doesn’t appear in the boot menu

The driver dispatched, the entry point ran, but the device still doesn’t appear in the boot menu. This is the next layer to check: Driver Binding.

In UEFI, a bus/device driver doesn’t bind at the moment its entry point runs. The entry point only installs EFI_DRIVER_BINDING_PROTOCOL. The actual binding happens when firmware calls ConnectController(), typically from BDS or when firmware explicitly connects devices.

01 ConnectController

BDS trigger

BDS calls ConnectController for storage/bus controller

02 Supported()

Probe

Firmware asks each driver: can you manage this controller?

03 Start()

Bind

Driver claims controller, installs Block I/O + Device Path for storage; partition driver creates partition handle; filesystem driver installs Simple File System

04 BDS Boot

Boot option

BDS finds Simple File System on ESP, resolves boot option

Driver Binding flow: each driver layer publishes its own protocol

Supported() being too restrictive or having wrong logic is the most common reason a device doesn’t get bound.

EFI_STATUSEFIAPIMyDriverSupported (  IN EFI_DRIVER_BINDING_PROTOCOL  *This,  IN EFI_HANDLE                   ControllerHandle,  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath OPTIONAL  ){  // Check whether this controller is storage we support  Status = gBS->OpenProtocol (                  ControllerHandle,                  &gEfiPciIoProtocolGuid,                  (VOID **)&PciIo,                  This->DriverBindingHandle,                  ControllerHandle,                  EFI_OPEN_PROTOCOL_BY_DRIVER                  );  if (EFI_ERROR (Status)) {    return Status;  // No PCI I/O → not supported  }  // Read Vendor ID / Device ID to confirm  PciIo->Pci.Read (PciIo, EfiPciIoWidthUint32, 0, 1, &VendorDeviceId);  // Must CloseProtocol before returning  gBS->CloseProtocol (         ControllerHandle,         &gEfiPciIoProtocolGuid,         This->DriverBindingHandle,         ControllerHandle         );  if (VendorDeviceId != MY_DEVICE_ID) {    return EFI_UNSUPPORTED;  }  return EFI_SUCCESS;}

Debugging with UEFI Shell

When UEFI Shell is available, several commands are useful for inspecting DXE state:

drivers — see which drivers loaded

Shell> driversDRV  VERSION  DRIVER NAME          IMAGE NAME===  =======  ====================  =======================1F   00000010 Decompress Driver    Decompress20   00000010 PC-AT ISA            IsaIo2C   00000010 PCI Bus Driver       PciBusDxe...

If your driver isn’t listed here, it hasn’t been dispatched (or it’s not in the FV).

dh -d <handle> — see protocols on a handle

Shell> dh -d 4AHandle 4A: PciIo DevicePath BlockIo DiskIo SimpleFileSystem

If you see PciIo but not SimpleFileSystem, the storage driver recognized the controller but hasn’t published enough protocols for BDS to boot from it.

devtree — see the device tree

Shell> devtree...  Ctrl[4A] PCI(0x1F,0x2) - SATA Controller    Ctrl[4B] Unit 0 ATA Hard Drive (8.0 GB)      Ctrl[4C] - FAT File System

If a storage controller is in devtree but has no child ATA Hard Drive: driver binding failed in Start(). If the drive is there but no FAT File System: the filesystem driver couldn’t bind.


Real-world scenario: NVMe disk not in boot menu

I run into this fairly often: flash a new BIOS, NVMe is still detected in the OS but doesn’t appear in the UEFI boot menu.

Approach by layer:

Step 1: Check whether DXE loaded the NVMe driver

In UEFI Shell, run drivers. Look for “NVM Express” or whatever the NVMe driver is named. If it’s missing → driver not in FV, or DEPEX failed.

Step 2: Check whether the NVMe handle has the needed protocols

Shell> dh -p BlockIo

Lists all handles with BlockIo. If no NVMe-related handle appears, Start() hasn’t run or failed when installing BlockIo.

Step 3: Try forcing a connect

Sometimes BDS doesn’t auto-connect all controllers. Try:

Shell> reconnect -rShell> map -r

reconnect -r forces a full reconnect. map -r refreshes filesystem mappings. If NVMe appears after this, the problem is BDS connect policy, not the driver itself.

Step 4: Check UEFI variable boot options

Shell> dmpstore BootOrderShell> dmpstore Boot0000

If Boot#### has a stale Device Path or the option is inactive, NVMe may be working fine but still not selected for boot.


Summary

DXE is the phase you’ll debug the most. When something goes wrong, approach it by layer:

Debugging DXE by symptom
Symptom Layer to suspect What to check
POST code 0xD3 Architectural Protocol missing CPU Arch, Timer, or other core protocol not installed
POST code 0xD4 PCI resource error MMIO/IO aperture for PCI devices
POST code 0xD6/D7 Console device missing Console output/input device not created
POST code 0xD9 LoadImage failed Boot option or Device Path is wrong
Driver missing from `drivers` Dispatcher hasn't run it FDF, DEPEX, MODULE_TYPE
Driver present but no BlockIo Driver Binding failed Supported() or Start()
BlockIo present but won't boot BDS path wrong Device Path, Boot####, BootOrder

References


Found this article useful?

Share it with someone learning firmware, BIOS/UEFI, or embedded systems, or support the author.

Nội dung liên quan

Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.

Đọc thêm về BIOS/UEFI

Khám phá các bài viết về BIOS/UEFI, embedded firmware, debugging và system-level thinking.