How to Write Cross-Platform UEFI Applications
Writing platform-independent Unified Extensible Firmware Interface (UEFI) applications requires adhering strictly to the UEFI specification's hardware abstraction layer while avoiding processor-specific operations. This article covers the fundamental strategies for developing portable UEFI software across architectures like x86-64, AArch64, ARM, and RISC-V. By leveraging standard UEFI protocols, avoiding inline assembly, adhering to portable data types, and using build systems like EDK II, developers can maintain a single codebase that compiles cleanly for any supported CPU architecture.
Rely Strictly on UEFI Services and Protocols
The core philosophy of UEFI is hardware abstraction. To ensure an application runs across different processor types, developers must never bypass the firmware layer to interact directly with hardware.
- Use Boot Services and Runtime Services: Never
access hardware timers, interrupt controllers, or platform registers
directly. Rely on
gBS->CreateEvent(),gBS->SetTimer(), and memory allocation services likegBS->AllocatePool()instead of custom hardware loops or processor-specific instructions. - Leverage Standard Protocols: Use standard protocols
such as
EFI_SIMPLE_TEXT_INPUT_PROTOCOL,EFI_BLOCK_IO_PROTOCOL, orEFI_GRAPHICS_OUTPUT_PROTOCOL(GOP) for I/O and display operations. These protocols are implemented by the platform firmware to abstract the underlying chipset and GPU differences.
Eliminate Architecture-Specific Assembly
Inline assembly is the primary cause of architectural lock-in. A portable UEFI application must be written entirely in standard C.
- Avoid Direct Port I/O: Standard x86 instructions
like
inandoutdo not exist on ARM or RISC-V architectures. If low-level bus access is required, rely on theEFI_PCI_ROOT_BRIDGE_IO_PROTOCOLorEFI_CPU_IO2_PROTOCOL, which abstract register reading and writing across memory-mapped I/O (MMIO) and port I/O systems. - Avoid CPU-Specific Feature Probing: Do not use
instructions like
cpuidon x86 or direct system register reads on ARM. If system capabilities must be inspected, query the platform configuration tables or device paths provided in the UEFI System Table.
Use Standardized UEFI Data Types
Data width and alignment differ between architectures. Using native C
types like int, long, or raw pointers without
abstraction leads to memory faults and undefined behavior.
- Adopt Specification Types: Always use UEFI-defined
fixed-width integer types such as
UINT8,UINT16,UINT32, andUINT64. - Use Architecture-Neutral Pointers: Use
UINTNandINTNfor integers that match the native pointer size of the target platform (32-bit on 32-bit systems, 64-bit on 64-bit systems). - Respect Memory Alignment: Many RISC and ARM
processors enforce strict alignment rules and will generate alignment
faults if unaligned memory accesses occur. Always use unaligned access
helper functions (such as
ReadUnaligned32()andWriteUnaligned32()from EDK II) when parsing packed binary data or external file formats.
Maintain Endianness and Structure Portability
While the most common UEFI implementations are little-endian, writing strictly portable code requires avoiding endian-dependent assumptions.
- Structure Packing: Avoid compiler-specific packing
pragmas where possible, or use standard macros provided by the
development framework (such as
PACKEDin EDK II) to ensure structures remain binary-compatible across different compilers and targets. - Explicit Byte Swapping: If reading data formats that use network byte order (big-endian), utilize explicit conversion functions to preserve correctness on both little-endian and big-endian systems.
Leverage EDK II for Multi-Architecture Builds
The TianoCore EDK II framework provides the standard build infrastructure for cross-platform UEFI development.
- Use MdePkg: Base your application on the standard
MdePkg(Module Development Environment Package). It provides architecture-independent headers, standard library equivalents, and wrappers for basic operations. - Configure Target Architectures: Configure the
application build description (
.inf) file to list all supported architectures in theVALID_ARCHITECTURESfield (e.g.,IA32 X64 ARM AARCH64 RISCV64). - Avoid Conditional Compilation: While
#ifdefmacros for specific architectures (likeMDE_CPU_X64orMDE_CPU_AARCH64) exist, keep them isolated to platform-specific drivers. The top-level application logic should remain free of architectural branches.
Distribute via Standard Directory Structures
Because UEFI binaries are compiled as architecture-specific PE32+
executables, running on multiple platforms requires distributing
separate binaries for each target architecture. Follow the standard EFI
System Partition (ESP) layout by deploying binaries to their recognized
default paths, such as \EFI\BOOT\BOOTX64.EFI,
\EFI\BOOT\BOOTAA64.EFI, or
\EFI\BOOT\BOOTRISCV64.EFI, ensuring the firmware
automatically loads the appropriate binary for the host CPU.