Custom Exception Handlers in UEFI Firmware

Implementing custom exception handlers in UEFI firmware allows developers to intercept processor-level faults—such as page faults, invalid opcodes, or division errors—prior to standard platform termination. This article outlines the architectural mechanics of handling processor exceptions in UEFI, detailing how developers locate the relevant architectural protocol, register interrupt callbacks using the processor context, parse low-level CPU state, and either recover execution flow or log critical crash telemetry for hardware debugging.

Core Protocol: EFI_CPU_ARCH_PROTOCOL

In the UEFI environment, low-level CPU control is abstracted through the EFI_CPU_ARCH_PROTOCOL. This protocol exposes architectural services, including interrupt and exception vector management, cache control, and processor timer configuration.

To manage exceptions, developers rely on the RegisterInterruptHandler service exposed by this protocol:

typedef
EFI_STATUS
(EFIAPI *EFI_CPU_REGISTER_INTERRUPT_HANDLER)(
  IN EFI_CPU_ARCH_PROTOCOL          *This,
  IN EFI_EXCEPTION_TYPE             InterruptType,
  IN EFI_CPU_INTERRUPT_HANDLER      InterruptHandler
  );

Step-by-Step Implementation

1. Locate the CPU Architectural Protocol

The driver must first query the UEFI Boot Services table to locate an instance of EFI_CPU_ARCH_PROTOCOL. This protocol is typically produced by a CPU DXE driver early in the DXE phase.

EFI_STATUS Status;
EFI_CPU_ARCH_PROTOCOL *CpuArch = NULL;

Status = gBS->LocateProtocol(
                &gEfiCpuArchProtocolGuid,
                NULL,
                (VOID **)&CpuArch
                );

if (EFI_ERROR(Status)) {
    // Protocol not found; handle error
    return Status;
}

2. Implement the Exception Handler Callback

The exception handler must match the EFI_CPU_INTERRUPT_HANDLER function signature. The callback receives the interrupt vector type and a pointer to EFI_SYSTEM_CONTEXT.

The EFI_SYSTEM_CONTEXT union provides direct access to the architectural registers at the precise moment the exception triggered. For x86_64 systems, this resolves to EFI_SYSTEM_CONTEXT_X64, containing register states such as RIP, RSP, RFLAGS, and control registers like CR2 (useful for page fault address inspection).

VOID
EFIAPI
CustomExceptionHandler (
  IN EFI_EXCEPTION_TYPE   InterruptType,
  IN EFI_SYSTEM_CONTEXT   SystemContext
  )
{
    // Example for x86_64 architecture
    EFI_SYSTEM_CONTEXT_X64 *Context = SystemContext.SystemContextX64;

    // Read offending instruction pointer
    UINT64 FaultingIp = Context->Rip;

    // Handle or inspect state based on the exception vector
    if (InterruptType == EXCEPT_IA32_PAGE_FAULT) {
        UINT64 FaultingAddress = Context->Cr2;
        // Process address or record diagnostic information
    }

    // Returning from this function attempts to execute at Context->Rip.
    // Modifying Context->Rip advances execution past the faulting instruction.
}

3. Register the Handler for Specific Vectors

Once the callback is defined, it must be bound to specific exception types. Common exception definitions are provided in the UefiCpuPkg header files (e.g., EXCEPT_IA32_DIVIDE_ERROR, EXCEPT_IA32_PAGE_FAULT).

Status = CpuArch->RegisterInterruptHandler(
                    CpuArch,
                    EXCEPT_IA32_PAGE_FAULT,
                    CustomExceptionHandler
                    );

To remove the custom handler and restore default firmware behavior, call RegisterInterruptHandler with NULL as the InterruptHandler argument.

Execution Control and Recovery

When an exception occurs, the underlying Interrupt Descriptor Table (IDT) redirects execution to a common dispatcher, which saves the register state and invokes the registered EFI_CPU_INTERRUPT_HANDLER.

From the handler, developers have two primary courses of action:

  1. Recovery and Continuation: The developer can resolve the fault condition, alter registers inside SystemContext (such as incrementing Context->Rip to skip an offending instruction, or remapping a missing page), and return. The CPU dispatcher then restores the modified register state and issues an IRET instruction to resume execution.
  2. Diagnostic Termination: If the fault is unrecoverable, the handler can serialize the complete register dump to non-volatile storage, a serial port, or a hardware debug port before halting the processor via a CpuDeadLoop() or initiating a controlled reset using the Reset System runtime service.

Critical Constraints

  • Boot Services Reentrancy: Exception handlers run in interrupt context. Invoking complex UEFI Boot Services (such as dynamic memory allocation via AllocatePool or file system access) inside the handler can cause deadlocks if the exception occurred while a boot service lock was held.
  • Stack Integrity: Stack overflow exceptions (EXCEPT_IA32_DOUBLE_FAULT) cannot be reliably processed using the normal stack. Advanced implementations must utilize Task State Segments (TSS) with an Interrupt Stack Table (IST) entry to provide a dedicated scratch stack for catastrophic failure handlers.