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:

  1. Understand the UEFI architecture - Know how UEFI differs fundamentally from legacy BIOS, including its execution environment, driver model, and service architecture
  2. Set up a UEFI development environment - Configure GNU-EFI or EDK2 toolchains to compile UEFI applications
  3. Write UEFI applications in C - Create PE32+ executables that run before any operating system loads
  4. Use UEFI Boot Services - Access memory allocation, protocol location, and other pre-boot services
  5. Query system information via UEFI - Retrieve memory maps, firmware vendor information, and configuration data
  6. Test UEFI applications in QEMU - Use OVMF firmware to test without real hardware
  7. 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:

  1. All modern x86 systems use UEFI - Since ~2012, BIOS is legacy. Every new PC, Mac (Intel), and server boots via UEFI.

  2. Operating systems depend on it - Windows, Linux (via systemd-boot, GRUB2), macOS all use UEFI interfaces.

  3. Security starts here - Secure Boot, measured boot, and firmware verification happen at the UEFI level.

  4. It’s the first software to run - Before any OS code, UEFI controls the machine. Understanding it gives you complete system visibility.

  5. 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:

  1. Boots on UEFI firmware (real hardware or OVMF in QEMU)
  2. Displays a greeting message using UEFI text output
  3. Queries and displays firmware information (vendor, version)
  4. Retrieves and displays the system memory map
  5. 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:

  1. A working UEFI application - The same format as Windows’s bootmgfw.efi or Linux’s grubx64.efi

  2. Reusable development environment - Makefile and tools for future UEFI projects

  3. Foundation for Project 8 - This is the prerequisite for building a real bootloader

  4. 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

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:

  1. Accept parameters in a specific format (ImageHandle, SystemTable)
  2. Navigate the System Table to find required services
  3. Call those services using a defined calling convention
  4. Handle all resource allocation manually (no malloc!)
  5. Return control cleanly to the firmware

5.4 Concepts You Must Understand First

Before implementing, verify you understand:

  1. 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)
  2. Function pointers and vtables
    • Self-assessment: What does ConOut->OutputString(ConOut, L"Hi") mean? Why is ConOut passed twice?
    • Reference: UEFI protocols are like C++ virtual tables; the first parameter is always this
  3. 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: AllocatePool for small allocations, AllocatePages for page-aligned large allocations
  4. 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; objcopy converts 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:

  1. Draw the memory layout after your application is loaded:
    • Where is your code in memory?
    • Where is the System Table?
    • Where are Boot Services?
  2. 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?
  3. 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:

  1. Compile C to object file with special flags
  2. Link to shared object (.so) with EFI CRT0
  3. 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:

  1. “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.”
  2. “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.”
  3. “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.”
  4. “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.”
  5. “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)

  1. Install GNU-EFI, OVMF, QEMU
  2. Create Makefile with proper flags
  3. Write minimal efi_main that returns EFI_SUCCESS
  4. Verify it compiles and produces BOOTX64.EFI
  5. 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)

  1. Add ConOut->OutputString for greeting
  2. Implement PrintHex helper function
  3. Display firmware vendor and revision
  4. Add ClearScreen at start

Milestone: “Hello from UEFI!” appears on screen

Phase 3: Memory Map (Day 4-6)

  1. Implement GetMemoryMap with two-call pattern
  2. Parse and iterate descriptors correctly
  3. Format and print each region (type, start, size)
  4. Count total conventional memory

Milestone: Memory map displays correctly

Phase 4: Polish (Day 7)

  1. Add WaitForEvent for keypress
  2. Clean up output formatting
  3. Free allocated memory
  4. 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

  1. Format USB drive as FAT32
  2. Create /EFI/BOOT/ directory
  3. Copy BOOTX64.EFI
  4. Boot with USB in UEFI-only mode (disable CSM)
  5. 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

Tutorials

Open Source Examples

Tools

  • OVMF - Open Virtual Machine Firmware for QEMU
  • UEFITool - UEFI firmware image viewer/editor

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 make command
  • 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:

  1. Build works: make produces BOOTX64.EFI without errors

  2. 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
  3. Code quality:
    • Clean, readable C code
    • Proper error handling (check return values)
    • Memory properly freed
  4. Documentation:
    • Makefile with comments
    • Brief README explaining the project
  5. 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