Project 7: UEFI Hello World Application
Build your first UEFI application that boots on modern firmware, uses UEFI protocols for output, and queries system information—stepping into the world of modern boot development.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | ★★★☆☆ Advanced |
| Time Estimate | 1-2 weeks |
| Language | C (GNU-EFI or EDK2) |
| Prerequisites | C programming, basic BIOS vs UEFI concepts, command line comfort |
| Key Topics | UEFI development, System Table, Boot Services, OVMF testing, PE32+ executables |
1. Learning Objectives
After completing this project, you will be able to:
- Understand the UEFI architecture - Know how UEFI differs fundamentally from legacy BIOS, including its execution environment, driver model, and service architecture
- Set up a UEFI development environment - Configure GNU-EFI or EDK2 toolchains to compile UEFI applications
- Write UEFI applications in C - Create PE32+ executables that run before any operating system loads
- Use UEFI Boot Services - Access memory allocation, protocol location, and other pre-boot services
- Query system information via UEFI - Retrieve memory maps, firmware vendor information, and configuration data
- Test UEFI applications in QEMU - Use OVMF firmware to test without real hardware
- Create bootable UEFI media - Build properly formatted EFI System Partitions
2. Theoretical Foundation
2.1 Core Concepts
What is UEFI?
UEFI (Unified Extensible Firmware Interface) is the modern replacement for BIOS. Unlike BIOS, which runs in 16-bit real mode and uses interrupt-based services, UEFI provides:
┌─────────────────────────────────────────────────────────────────────────────┐
│ UEFI vs BIOS Comparison │
├───────────────────────────────────┬─────────────────────────────────────────┤
│ LEGACY BIOS │ UEFI │
├───────────────────────────────────┼─────────────────────────────────────────┤
│ 16-bit Real Mode │ 32-bit or 64-bit Protected/Long Mode │
│ 1MB addressable memory │ Full memory access from start │
│ INT-based services (INT 10h, 13h) │ Function pointer tables (protocols) │
│ 512-byte MBR limit for bootloader │ No size limit (PE32+ executables) │
│ No standard driver model │ Rich driver architecture (DXE) │
│ No built-in networking │ Built-in TCP/IP, HTTP boot │
│ No security model │ Secure Boot, authenticated updates │
│ No filesystem support │ FAT12/16/32 driver built-in │
│ No mouse support │ HII (Human Interface Infrastructure) │
└───────────────────────────────────┴─────────────────────────────────────────┘
UEFI Boot Phases
UEFI firmware goes through several phases before loading your application:
┌─────────────────────────────────────────────────────────────────────────────┐
│ UEFI Boot Phases │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ SEC │ Security Phase │
│ │ (early) │ • CPU reset, initial cache-as-RAM setup │
│ └────┬────┘ • Verify firmware integrity │
│ │ • Minimal initialization │
│ ▼ │
│ ┌─────────┐ │
│ │ PEI │ Pre-EFI Initialization │
│ │ │ • Memory initialization (DRAM training) │
│ └────┬────┘ • Early platform init (chipset, GPIO) │
│ │ • Pass memory info to DXE via HOBs │
│ ▼ │
│ ┌─────────┐ │
│ │ DXE │ Driver Execution Environment │
│ │ │ • Load drivers from firmware volumes │
│ └────┬────┘ • Build protocol database │
│ │ • Initialize all hardware │
│ ▼ │
│ ┌─────────┐ │
│ │ BDS │ Boot Device Selection ← YOUR APP LOADS HERE │
│ │ │ • Find boot options │
│ └────┬────┘ • Load boot manager or your bootloader │
│ │ • Boot Services still available │
│ ▼ │
│ ┌─────────┐ │
│ │ RT │ Runtime (after ExitBootServices) │
│ │ │ • Only Runtime Services available │
│ └─────────┘ • OS has taken over │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The EFI System Table
When your UEFI application’s entry point is called, you receive two critical parameters:
EFI_STATUS
EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable);
The System Table is your gateway to all UEFI services:
┌─────────────────────────────────────────────────────────────────────────────┐
│ EFI_SYSTEM_TABLE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ SystemTable │
│ ├── Hdr Table signature, version, CRC │
│ │ │
│ ├── FirmwareVendor ─────────────► L"American Megatrends" (wide string) │
│ ├── FirmwareRevision ───────────► 0x00020046 (version 2.70) │
│ │ │
│ ├── ConsoleInHandle │
│ ├── ConIn ──────────────────────► EFI_SIMPLE_TEXT_INPUT_PROTOCOL │
│ │ └── ReadKeyStroke() │
│ │ └── Reset() │
│ │ │
│ ├── ConsoleOutHandle │
│ ├── ConOut ─────────────────────► EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL │
│ │ └── OutputString() │
│ │ └── ClearScreen() │
│ │ └── SetAttribute() │
│ │ │
│ ├── ConsoleErrorHandle │
│ ├── StdErr ─────────────────────► (Same as ConOut, for errors) │
│ │ │
│ ├── RuntimeServices ────────────► Available AFTER ExitBootServices │
│ │ └── GetTime() │
│ │ └── GetVariable() / SetVariable() │
│ │ └── ResetSystem() │
│ │ │
│ ├── BootServices ───────────────► Available UNTIL ExitBootServices │
│ │ └── AllocatePages() / FreePages() │
│ │ └── AllocatePool() / FreePool() │
│ │ └── GetMemoryMap() │
│ │ └── LocateProtocol() │
│ │ └── HandleProtocol() │
│ │ └── LoadImage() / StartImage() │
│ │ └── ExitBootServices() │
│ │ │
│ ├── NumberOfTableEntries │
│ └── ConfigurationTable ─────────► ACPI tables, SMBIOS, etc. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Boot Services vs Runtime Services
This is a critical distinction:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Boot Services vs Runtime Services │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ BOOT SERVICES (Available until ExitBootServices) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Memory Services: │ │
│ │ • AllocatePages() - Allocate page-aligned memory │ │
│ │ • FreePages() - Return memory │ │
│ │ • GetMemoryMap() - Get complete memory layout │ │
│ │ • AllocatePool() - General allocation │ │
│ │ │ │
│ │ Protocol Services: │ │
│ │ • LocateProtocol() - Find a protocol by GUID │ │
│ │ • HandleProtocol() - Get protocol from specific handle │ │
│ │ • LocateHandleBuffer() - Find all handles with protocol │ │
│ │ │ │
│ │ Image Services: │ │
│ │ • LoadImage() - Load PE32+ from disk │ │
│ │ • StartImage() - Execute loaded image │ │
│ │ • Exit() - Return from image │ │
│ │ │ │
│ │ Event Services: │ │
│ │ • CreateEvent() - Timer, signaling │ │
│ │ • WaitForEvent() - Block until event │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ ExitBootServices() │ │
│ │ CALLED HERE │ │
│ └──────────┬──────────┘ │
│ │ │
│ ╔═══════════▼═══════════╗ │
│ ║ POINT OF NO RETURN ║ │
│ ╚═══════════════════════╝ │
│ │
│ RUNTIME SERVICES (Available after ExitBootServices) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Time Services: │ │
│ │ • GetTime() / SetTime() - RTC access │ │
│ │ │ │
│ │ Variable Services: │ │
│ │ • GetVariable() - Read NVRAM variables │ │
│ │ • SetVariable() - Write NVRAM variables │ │
│ │ • GetNextVariableName() - Enumerate variables │ │
│ │ │ │
│ │ System Services: │ │
│ │ • ResetSystem() - Reboot or shutdown │ │
│ │ • GetNextHighMonotonicCount() │ │
│ │ • UpdateCapsule() - Firmware updates │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 Why This Matters
Understanding UEFI is essential for modern systems programming because:
-
All modern x86 systems use UEFI - Since ~2012, BIOS is legacy. Every new PC, Mac (Intel), and server boots via UEFI.
-
Operating systems depend on it - Windows, Linux (via systemd-boot, GRUB2), macOS all use UEFI interfaces.
-
Security starts here - Secure Boot, measured boot, and firmware verification happen at the UEFI level.
-
It’s the first software to run - Before any OS code, UEFI controls the machine. Understanding it gives you complete system visibility.
-
Career relevance - Firmware engineering, security research, and systems programming all require UEFI knowledge.
2.3 Historical Context
- 1998: Intel creates EFI (Extensible Firmware Interface) for Itanium servers
- 2002: EFI 1.10 released, still Intel-only
- 2005: UEFI Forum formed (AMD, Apple, Dell, HP, IBM, Intel, Microsoft)
- 2007: UEFI 2.0 released, industry-wide adoption begins
- 2011: Secure Boot introduced in UEFI 2.3.1
- 2012: Windows 8 requires UEFI for logo certification
- 2020+: All new x86 systems are UEFI-only; CSM (Compatibility Support Module for BIOS) being deprecated
2.4 Common Misconceptions
Misconception 1: “UEFI is just a better BIOS”
- Reality: UEFI is a complete reimagining. It’s a full operating environment with drivers, filesystems, networking, and a shell. BIOS was just interrupt handlers.
Misconception 2: “UEFI applications are like regular programs”
- Reality: While written in C, UEFI apps run in a very constrained environment. No standard library, no OS services, specific calling conventions, and only UEFI protocols are available.
Misconception 3: “You need EDK2 to write UEFI apps”
- Reality: GNU-EFI is a lighter alternative that lets you use standard GCC. EDK2 is Intel’s full SDK and is more complex but more powerful.
Misconception 4: “UEFI is always 64-bit”
- Reality: UEFI can be 32-bit (IA32) or 64-bit (x64). Most modern systems are 64-bit. Your app must match the firmware’s native mode.
3. Project Specification
3.1 What You Will Build
A UEFI application (.efi file) that:
- Boots on UEFI firmware (real hardware or OVMF in QEMU)
- Displays a greeting message using UEFI text output
- Queries and displays firmware information (vendor, version)
- Retrieves and displays the system memory map
- Waits for a keypress before exiting
This is the UEFI equivalent of “Hello World”—your entry point into modern firmware development.
3.2 Functional Requirements
| ID | Requirement | Description |
|---|---|---|
| FR-1 | Boot successfully | Application loads and executes on UEFI firmware |
| FR-2 | Display greeting | Print “Hello from UEFI!” to console |
| FR-3 | Show firmware info | Display vendor name and UEFI revision |
| FR-4 | Display memory map | Show at least 5 memory regions with type and range |
| FR-5 | Wait for input | Pause until user presses a key |
| FR-6 | Clean exit | Return EFI_SUCCESS and allow firmware to continue |
3.3 Non-Functional Requirements
| ID | Requirement | Description |
|---|---|---|
| NFR-1 | Portable | Build with GNU-EFI (or EDK2) for x86-64 |
| NFR-2 | OVMF compatible | Run in QEMU with OVMF firmware |
| NFR-3 | Standard naming | Output file named BOOTX64.EFI |
| NFR-4 | FAT32 bootable | Create proper EFI System Partition structure |
| NFR-5 | Documented | Include Makefile with build instructions |
3.4 Example Usage / Output
# Build the UEFI application
$ make
Compiling hello.c...
Linking hello.so...
Creating hello.efi (PE32+)...
Build complete: BOOTX64.EFI
# Create FAT32 disk image with EFI System Partition structure
$ make disk
Creating 64MB FAT32 disk image...
Creating EFI/BOOT directory structure...
Copying BOOTX64.EFI...
Disk image ready: disk.img
# Run in QEMU with OVMF
$ make run
qemu-system-x86_64 -bios /usr/share/OVMF/OVMF_CODE.fd \
-drive file=disk.img,format=raw \
-net none
QEMU/OVMF Output:
Hello from UEFI!
================
Firmware Vendor: EDK II
UEFI Revision: 2.70
System Memory Map:
------------------
0x0000000000000000 - 0x000000000009FFFF Conventional Memory (640 KB)
0x0000000000100000 - 0x00000000007FFFFF Conventional Memory (7 MB)
0x0000000000800000 - 0x0000000000807FFF ACPI Memory NVS (32 KB)
0x0000000000808000 - 0x000000000080FFFF ACPI Reclaim Memory (32 KB)
0x0000000000810000 - 0x00000000BFFFFFFF Conventional Memory (3063 MB)
0x00000000FFC00000 - 0x00000000FFFFFFFF Memory Mapped I/O (4 MB)
...
Memory Map: 15 entries
Total Conventional Memory: 3071 MB
Press any key to continue...
3.5 Real World Outcome
When you complete this project, you will have:
-
A working UEFI application - The same format as Windows’s
bootmgfw.efior Linux’sgrubx64.efi -
Reusable development environment - Makefile and tools for future UEFI projects
-
Foundation for Project 8 - This is the prerequisite for building a real bootloader
-
Portfolio piece - Demonstrate firmware-level programming ability
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────────────────────┐
│ UEFI Hello World Architecture │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EFI System Partition │ │
│ │ (FAT32 filesystem on disk.img) │ │
│ │ │ │
│ │ / │ │
│ │ └── EFI/ │ │
│ │ └── BOOT/ │ │
│ │ └── BOOTX64.EFI ← Your application (PE32+ format) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Loaded by UEFI firmware │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ OVMF Firmware (QEMU) │ │
│ │ │ │
│ │ 1. Initialize hardware (virtualized) │ │
│ │ 2. Scan for EFI System Partition │ │
│ │ 3. Look for /EFI/BOOT/BOOTX64.EFI │ │
│ │ 4. Load PE32+ into memory │ │
│ │ 5. Call efi_main(ImageHandle, SystemTable) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Your UEFI Application │ │
│ │ │ │
│ │ efi_main(ImageHandle, SystemTable) │ │
│ │ ├── Store global pointers (ST, BS, RT) │ │
│ │ ├── Clear screen │ │
│ │ ├── Print greeting via ConOut->OutputString() │ │
│ │ ├── Display firmware info from SystemTable │ │
│ │ ├── Call BS->GetMemoryMap() │ │
│ │ ├── Parse and display memory descriptors │ │
│ │ ├── Wait for key via ConIn->ReadKeyStroke() │ │
│ │ └── Return EFI_SUCCESS │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 Key Components
Component 1: Entry Point (efi_main)
EFI_STATUS EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
// ImageHandle: Handle to our loaded image
// SystemTable: Pointer to all UEFI services
// Store for convenience
EFI_BOOT_SERVICES *BS = SystemTable->BootServices;
EFI_RUNTIME_SERVICES *RT = SystemTable->RuntimeServices;
SIMPLE_TEXT_OUTPUT_INTERFACE *ConOut = SystemTable->ConOut;
// Your code here...
return EFI_SUCCESS;
}
Component 2: Console Output
UEFI uses wide strings (UTF-16). The L prefix creates wide string literals:
ConOut->OutputString(ConOut, L"Hello from UEFI!\r\n");
Note: Line endings are \r\n (carriage return + newline).
Component 3: Memory Map Retrieval
EFI_MEMORY_DESCRIPTOR *MemoryMap = NULL;
UINTN MemoryMapSize = 0;
UINTN MapKey;
UINTN DescriptorSize;
UINT32 DescriptorVersion;
// First call to get required size
BS->GetMemoryMap(&MemoryMapSize, MemoryMap, &MapKey,
&DescriptorSize, &DescriptorVersion);
// Allocate buffer (add extra space for safety)
MemoryMapSize += 2 * DescriptorSize;
BS->AllocatePool(EfiLoaderData, MemoryMapSize, (VOID**)&MemoryMap);
// Second call to actually get the map
BS->GetMemoryMap(&MemoryMapSize, MemoryMap, &MapKey,
&DescriptorSize, &DescriptorVersion);
Component 4: Input Handling
EFI_INPUT_KEY Key;
UINTN EventIndex;
// Wait for a key press
BS->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &EventIndex);
SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key);
4.3 Data Structures
EFI_MEMORY_DESCRIPTOR
Each entry in the memory map is described by:
typedef struct {
UINT32 Type; // Memory type (see below)
EFI_PHYSICAL_ADDRESS PhysicalStart; // Start of region
EFI_VIRTUAL_ADDRESS VirtualStart; // Virtual address (0 if not set)
UINT64 NumberOfPages; // Size in 4KB pages
UINT64 Attribute; // Memory attributes (caching, etc.)
} EFI_MEMORY_DESCRIPTOR;
Memory types you’ll commonly see:
| Type | Name | Description |
|---|---|---|
| 0 | EfiReservedMemoryType | Unusable |
| 1 | EfiLoaderCode | Your bootloader’s code |
| 2 | EfiLoaderData | Your bootloader’s data |
| 3 | EfiBootServicesCode | UEFI Boot Services code |
| 4 | EfiBootServicesData | UEFI Boot Services data |
| 5 | EfiRuntimeServicesCode | UEFI Runtime Services code |
| 6 | EfiRuntimeServicesData | UEFI Runtime Services data |
| 7 | EfiConventionalMemory | Free, usable memory |
| 9 | EfiACPIReclaimMemory | ACPI tables (reclaimable after parsing) |
| 10 | EfiACPIMemoryNVS | ACPI non-volatile storage |
| 11 | EfiMemoryMappedIO | Memory-mapped I/O |
4.4 Algorithm Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ Execution Flow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ START │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 1. Initialize global pointers │ │
│ │ ST = SystemTable │ │
│ │ BS = ST->BootServices │ │
│ │ ConOut = ST->ConOut │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 2. Clear screen and set colors │ │
│ │ ConOut->ClearScreen(ConOut) │ │
│ │ ConOut->SetAttribute(ConOut, ...) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 3. Print greeting │ │
│ │ ConOut->OutputString(L"Hello...") │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 4. Display firmware info │ │
│ │ ST->FirmwareVendor (wide string) │ │
│ │ ST->FirmwareRevision (decode version) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 5. Get memory map size │ │
│ │ BS->GetMemoryMap(&Size, NULL, ...) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 6. Allocate buffer │ │
│ │ BS->AllocatePool(Size + slack, &buf) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 7. Get actual memory map │ │
│ │ BS->GetMemoryMap(&Size, buf, ...) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 8. Parse and display entries │ │
│ │ FOR each descriptor in map: │ │
│ │ Print type, start, end, size │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 9. Wait for keypress │ │
│ │ BS->WaitForEvent(ConIn->WaitForKey) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ 10. Free allocated memory │ │
│ │ BS->FreePool(buf) │ │
│ └───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ RETURN EFI_SUCCESS │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5. Implementation Guide
5.1 Development Environment Setup
Option A: GNU-EFI (Recommended for beginners)
GNU-EFI is simpler than EDK2 and uses standard GCC.
Ubuntu/Debian:
sudo apt update
sudo apt install gnu-efi gcc make ovmf qemu-system-x86
Fedora:
sudo dnf install gnu-efi-devel gcc make edk2-ovmf qemu-system-x86
macOS (Homebrew):
brew install x86_64-elf-gcc qemu
# GNU-EFI must be compiled from source on macOS
git clone https://git.code.sf.net/p/gnu-efi/code gnu-efi
cd gnu-efi && make
Verify OVMF location:
# Ubuntu/Debian
ls /usr/share/OVMF/OVMF_CODE.fd
# Fedora
ls /usr/share/edk2/ovmf/OVMF_CODE.fd
# Alternative location
ls /usr/share/qemu/OVMF.fd
Option B: EDK2 (Full UEFI Development Kit)
EDK2 is Intel’s complete SDK. More powerful but more complex.
git clone https://github.com/tianocore/edk2.git
cd edk2
git submodule update --init
make -C BaseTools
source edksetup.sh
For this project, GNU-EFI is recommended for its simplicity.
5.2 Project Structure
uefi-hello/
├── Makefile # Build automation
├── hello.c # Main source file
├── disk.img # Generated FAT32 disk image
└── build/
├── hello.o # Object file
├── hello.so # Shared object (intermediate)
└── BOOTX64.EFI # Final PE32+ executable
5.3 The Core Question You’re Answering
How does modern firmware provide a standardized interface for pre-boot code, and how can you write applications that interact with this interface?
Unlike BIOS, which provided ad-hoc interrupt services, UEFI provides a structured, object-oriented interface through protocols and services. Your application must:
- Accept parameters in a specific format (ImageHandle, SystemTable)
- Navigate the System Table to find required services
- Call those services using a defined calling convention
- Handle all resource allocation manually (no malloc!)
- Return control cleanly to the firmware
5.4 Concepts You Must Understand First
Before implementing, verify you understand:
- Wide strings (UTF-16)
- Self-assessment: What does
L"Hello"create in memory? How many bytes is each character? - Reference: UEFI strings are 16-bit Unicode (UCS-2 subset of UTF-16)
- Self-assessment: What does
- Function pointers and vtables
- Self-assessment: What does
ConOut->OutputString(ConOut, L"Hi")mean? Why isConOutpassed twice? - Reference: UEFI protocols are like C++ virtual tables; the first parameter is always
this
- Self-assessment: What does
- Memory allocation without malloc
- Self-assessment: How do you allocate memory when there’s no heap? What’s the difference between pages and pool?
- Reference:
AllocatePoolfor small allocations,AllocatePagesfor page-aligned large allocations
- PE32+ executable format
- Self-assessment: What’s the difference between ELF (Linux) and PE (Windows)? Why does UEFI use PE?
- Reference: UEFI adopted Windows PE format because Microsoft was involved;
objcopyconverts ELF to PE
5.5 Questions to Guide Your Design
Output:
- How do you print numbers in UEFI when there’s no printf?
- How do you handle hexadecimal formatting for memory addresses?
Memory map:
- What’s the difference between the size of the buffer and the descriptor size?
- Why do you need to add slack space before calling GetMemoryMap the second time?
- How do you iterate through variable-sized descriptors?
Error handling:
- What if AllocatePool fails? How do you handle EFI_STATUS return values?
- Should you free memory on error paths?
Testing:
- How do you verify your output is correct?
- What memory regions should you expect to see in QEMU?
5.6 Thinking Exercise
Before writing code, work through this mental exercise:
- Draw the memory layout after your application is loaded:
- Where is your code in memory?
- Where is the System Table?
- Where are Boot Services?
- Trace a protocol call:
- Starting with
ST->ConOut->OutputString(ST->ConOut, L"X") - What pointer dereferences happen?
- How does the firmware know which function to call?
- Starting with
- Predict the memory map:
- In a VM with 4GB RAM, what regions will you see?
- Which regions are usable by an OS?
- Which must be preserved?
Expected insights:
- The System Table is in firmware memory, not yours
- Protocol calls are indirect through function pointers
- Most RAM shows as “Conventional Memory”
- Firmware reserves some regions for itself
5.7 Hints in Layers
Hint 1: Getting Started (Conceptual Direction)
Your application is a single C file with one entry point. The UEFI headers from GNU-EFI provide all type definitions. Your Makefile handles the complex compilation steps (compile to ELF, convert to PE32+).
Start by getting a minimal “hello world” working before adding memory map display. The greeting proves your environment works.
Hint 2: Makefile Structure (More Specific Guidance)
The GNU-EFI build process is:
- Compile C to object file with special flags
- Link to shared object (.so) with EFI CRT0
- Convert ELF shared object to PE32+ with objcopy
Key compiler flags:
CFLAGS = -I/usr/include/efi -I/usr/include/efi/x86_64 \
-fno-stack-protector -fpic -fshort-wchar \
-mno-red-zone -DEFI_FUNCTION_WRAPPER
Key linker flags:
LDFLAGS = -nostdlib -znocombreloc -T /usr/lib/elf_x86_64_efi.lds \
-shared -Bsymbolic -L/usr/lib /usr/lib/crt0-efi-x86_64.o
Hint 3: Console Output (Technical Details)
UEFI has no printf. You must use OutputString with wide strings. For numbers, you need a helper function:
// Convert UINTN to hex string
CHAR16 HexDigits[] = L"0123456789ABCDEF";
void PrintHex(UINTN value) {
CHAR16 buffer[17]; // 16 digits + null
buffer[16] = L'\0';
for (int i = 15; i >= 0; i--) {
buffer[i] = HexDigits[value & 0xF];
value >>= 4;
}
ST->ConOut->OutputString(ST->ConOut, buffer);
}
Hint 4: Memory Map Iteration (Advanced Details)
The memory map is NOT an array of fixed-size elements. You must use DescriptorSize:
UINT8 *ptr = (UINT8 *)MemoryMap;
UINTN count = MemoryMapSize / DescriptorSize;
for (UINTN i = 0; i < count; i++) {
EFI_MEMORY_DESCRIPTOR *desc = (EFI_MEMORY_DESCRIPTOR *)ptr;
// Process desc
ptr += DescriptorSize; // NOT sizeof(EFI_MEMORY_DESCRIPTOR)!
}
5.8 The Interview Questions They’ll Ask
If you mention UEFI development on your resume, expect:
- “Explain the difference between Boot Services and Runtime Services”
- Strong answer: “Boot Services are available until ExitBootServices() is called and include memory allocation, protocol location, and image loading. Runtime Services persist after the OS takes over and include time, variable storage, and reset functionality. An OS must call ExitBootServices before taking control.”
- “What is the EFI System Table and what does it contain?”
- Strong answer: “It’s the root structure passed to every UEFI application. Contains pointers to ConIn/ConOut for console, BootServices and RuntimeServices tables, firmware vendor/version, and the configuration table (ACPI, SMBIOS, etc.). All UEFI functionality is accessed through it.”
- “Why does UEFI use PE32+ instead of ELF for executables?”
- Strong answer: “Historical reasons—Microsoft was a key UEFI Forum member, and PE was already well-documented. UEFI needed a format that supported relocations (position-independent loading), and PE/COFF fit well. ELF would have worked but PE was chosen for industry compatibility.”
- “How does UEFI memory allocation differ from userspace malloc?”
- Strong answer: “No heap exists. AllocatePool provides general allocation from UEFI’s memory, tagged by type (loader, boot services, etc.). AllocatePages gives page-aligned regions. All allocation must be explicitly freed. After ExitBootServices, boot services memory can be reclaimed.”
- “Walk me through what happens when a UEFI system boots”
- Strong answer: “SEC phase initializes cache-as-RAM. PEI trains memory and passes info via HOBs. DXE loads drivers and builds protocol database. BDS finds boot entries and loads the bootloader. The bootloader calls ExitBootServices and jumps to the kernel, which uses Runtime Services.”
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| UEFI Architecture | “Beyond BIOS” by Vincent Zimmer | Ch. 1-4 |
| UEFI Programming | “Beyond BIOS” by Vincent Zimmer | Ch. 5-7 |
| PE Format | “Practical Binary Analysis” by Dennis Andriesse | Ch. 3 |
| Low-level C | “Low-Level Programming” by Igor Zhirkov | Ch. 1-3 |
5.10 Implementation Phases
Phase 1: Build Environment (Day 1-2)
- Install GNU-EFI, OVMF, QEMU
- Create Makefile with proper flags
- Write minimal efi_main that returns EFI_SUCCESS
- Verify it compiles and produces BOOTX64.EFI
- Create disk.img and test in QEMU (should boot and exit cleanly)
Milestone: QEMU boots your app without crashing
Phase 2: Console Output (Day 2-3)
- Add ConOut->OutputString for greeting
- Implement PrintHex helper function
- Display firmware vendor and revision
- Add ClearScreen at start
Milestone: “Hello from UEFI!” appears on screen
Phase 3: Memory Map (Day 4-6)
- Implement GetMemoryMap with two-call pattern
- Parse and iterate descriptors correctly
- Format and print each region (type, start, size)
- Count total conventional memory
Milestone: Memory map displays correctly
Phase 4: Polish (Day 7)
- Add WaitForEvent for keypress
- Clean up output formatting
- Free allocated memory
- Add error handling
Milestone: Complete, polished output
5.11 Key Implementation Decisions
| Decision | Recommendation | Rationale |
|---|---|---|
| Toolchain | GNU-EFI | Simpler than EDK2 for learning |
| Print numbers | Custom function | No printf in UEFI |
| Memory map size | Add 2x descriptor size | Map can grow between calls |
| Descriptor iteration | Use returned size | Future firmware may add fields |
| Error handling | Check all returns | UEFI calls can fail |
6. Testing Strategy
6.1 Unit Testing (Limited)
UEFI code is hard to unit test. Focus on integration testing in QEMU.
6.2 Integration Testing
# Basic boot test
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw -net none
# With serial output for debugging
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw \
-serial stdio -net none
# With GDB for debugging
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw \
-s -S -net none
# Then in another terminal: gdb, target remote :1234
6.3 Test Cases
| Test | Expected Result | Verification |
|---|---|---|
| Boot | App loads | See greeting message |
| Firmware info | Vendor displayed | “EDK II” for OVMF |
| Memory map | At least 5 entries | Count printed entries |
| Memory total | Reasonable value | Close to VM RAM setting |
| Keypress | App pauses | Must press key to exit |
| Exit | Clean return | No crash, returns to shell |
6.4 Testing on Real Hardware
- Format USB drive as FAT32
- Create
/EFI/BOOT/directory - Copy BOOTX64.EFI
- Boot with USB in UEFI-only mode (disable CSM)
- May need to disable Secure Boot
7. Common Pitfalls & Debugging
7.1 Build Errors
| Problem | Symptom | Solution |
|---|---|---|
| Missing headers | efi.h not found |
Check GNU-EFI include paths |
| Wrong architecture | Linker errors | Ensure x86_64 flags, not i386 |
| CRT0 not found | crt0-efi-x86_64.o: No such file |
Install gnu-efi package |
| objcopy fails | PE conversion error | Ensure binutils installed |
7.2 Runtime Errors
| Problem | Symptom | Solution |
|---|---|---|
| No output | Black screen | Check if app is loaded (OVMF shell) |
| Garbage output | Strange characters | Ensure wide strings (L””) |
| Crash on memmap | Triple fault | Check buffer size, add slack |
| Infinite loop | Hangs | Use QEMU monitor (Ctrl+Alt+2) |
7.3 Debugging Techniques
QEMU Monitor:
# Press Ctrl+Alt+2 for monitor
info registers # CPU state
xp /10x 0x100000 # Examine memory
UEFI Shell (if your app crashes early):
# OVMF drops to shell if boot fails
Shell> fs0:
fs0:\> dir EFI\BOOT
fs0:\> BOOTX64.EFI
Serial Output:
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw \
-serial stdio -nographic
GDB Debugging:
# Terminal 1
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw -s -S
# Terminal 2
gdb
(gdb) target remote :1234
(gdb) break efi_main
(gdb) continue
8. Extensions & Challenges
Once the basic project works, try these extensions:
Extension 1: Color Output (Easy)
Use SetAttribute() to display different memory types in different colors.
ConOut->SetAttribute(ConOut, EFI_RED | EFI_BACKGROUND_BLACK);
Extension 2: ACPI Table Discovery (Medium)
Find and display ACPI RSDP from the Configuration Table.
Extension 3: Graphics Mode (Medium)
Locate GOP (Graphics Output Protocol) and display resolution options.
Extension 4: File Reading (Advanced)
Read a file from the ESP using Simple File System Protocol. This is the foundation for Project 8.
Extension 5: Boot on Real Hardware (Advanced)
Test on actual UEFI hardware. Debug Secure Boot issues.
9. Real-World Connections
Production Bootloaders Using UEFI
| Bootloader | How It Uses UEFI |
|---|---|
| systemd-boot | Simple UEFI boot manager, minimal, Linux-focused |
| GRUB2 | Full UEFI support with filesystem drivers |
| rEFInd | UEFI boot manager with theming |
| Windows Boot Manager | Microsoft’s UEFI bootloader |
| Limine | Modern bootloader supporting Stivale2 protocol |
Career Applications
- Firmware Engineering: BIOS/UEFI development at Intel, AMI, Phoenix
- Security Research: Secure Boot bypass, rootkit detection, firmware analysis
- Embedded Systems: Custom bootloaders for ARM/x86 devices
- Cloud Infrastructure: Custom firmware for servers
- OS Development: Writing bootloaders for hobby or production OS
10. Resources
Official Documentation
- UEFI Specification - The definitive reference
- TianoCore EDK2 Wiki - Intel’s UEFI development resources
- GNU-EFI SourceForge - GNU-EFI project
Tutorials
- OSDev Wiki - UEFI - Community documentation
- UEFI Programming Guide - UEFI Forum resources
Open Source Examples
- systemd-boot - Simple, readable UEFI bootloader
- Limine - Modern UEFI bootloader
Tools
11. Self-Assessment Checklist
Before considering this project complete, verify:
Understanding
- I can explain the difference between BIOS and UEFI boot
- I understand the UEFI boot phases (SEC, PEI, DXE, BDS, RT)
- I know what the System Table contains and how to access it
- I can explain Boot Services vs Runtime Services
- I understand why UEFI uses PE32+ executables
Implementation
- My application boots successfully in QEMU/OVMF
- Greeting message displays correctly
- Firmware vendor and version are shown
- Memory map displays with types and addresses
- Application waits for keypress before exiting
- No memory leaks (all allocations freed)
Tooling
- I can build the application with a single
makecommand - I can create a bootable disk image
- I can debug with QEMU serial output
- I know how to access UEFI Shell if boot fails
Portfolio
- Code is clean and well-commented
- README explains how to build and run
- I can explain the project in an interview
12. Submission / Completion Criteria
Your project is complete when:
-
Build works:
makeproduces BOOTX64.EFI without errors - Functionality verified:
- Application boots in QEMU with OVMF
- Displays “Hello from UEFI!” (or similar greeting)
- Shows firmware vendor (“EDK II” in OVMF)
- Displays at least 5 memory map entries with types and addresses
- Waits for keypress before returning
- Code quality:
- Clean, readable C code
- Proper error handling (check return values)
- Memory properly freed
- Documentation:
- Makefile with comments
- Brief README explaining the project
- Ready for Project 8:
- You understand how to use Boot Services
- You can locate and use UEFI protocols
- You have a working UEFI development environment
Congratulations! Upon completing this project, you’ve written your first UEFI application—the same format used by every modern bootloader. You’re now ready for Project 8: UEFI Bootloader That Loads an ELF Kernel, where you’ll build a real bootloader that loads and executes an operating system kernel.
Next project: P08 - UEFI ELF Kernel Loader