Project 8: UEFI Bootloader That Loads an ELF Kernel
Build a complete UEFI bootloader that parses ELF64 executables, sets up a framebuffer, exits UEFI boot services, and hands off control to an operating system kernel.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | ★★★★☆ Expert |
| Time Estimate | 2-3 weeks |
| Language | C |
| Prerequisites | Project 7 completed, understanding of executable formats, memory layout concepts |
| Key Topics | ELF64 parsing, Graphics Output Protocol, ExitBootServices, kernel handoff, boot information structures |
1. Learning Objectives
After completing this project, you will be able to:
- Parse ELF64 executable format - Read and validate ELF headers, understand program headers, and load segments
- Use UEFI File System Protocol - Navigate directories and read files from the EFI System Partition
- Configure graphics via GOP - Set up a linear framebuffer for kernel graphics output
- Execute ExitBootServices correctly - Handle the critical transition from UEFI to kernel control
- Design boot information structures - Pass memory map, framebuffer, and other info to the kernel
- Implement a complete boot sequence - From UEFI application to running kernel
2. Theoretical Foundation
2.1 Core Concepts
The ELF Format
ELF (Executable and Linkable Format) is the standard executable format for Unix-like systems. Your kernel will be an ELF64 file that your bootloader must parse and load.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ELF64 File Structure │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ELF Header (64 bytes) │ │
│ │ e_ident[16] : Magic number, class, endianness │ │
│ │ e_type : Executable (ET_EXEC) or shared object (ET_DYN) │ │
│ │ e_machine : Architecture (EM_X86_64 = 0x3E) │ │
│ │ e_entry : Entry point virtual address │ │
│ │ e_phoff : Program header table offset │ │
│ │ e_phentsize : Size of each program header entry │ │
│ │ e_phnum : Number of program header entries │ │
│ │ e_shoff : Section header table offset (not needed) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Program Header Table (at e_phoff) │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ Program Header 0: │ │ │
│ │ │ p_type : PT_LOAD (loadable segment) │ │ │
│ │ │ p_flags : Permissions (R/W/X) │ │ │
│ │ │ p_offset : Offset in file where segment data starts │ │ │
│ │ │ p_vaddr : Virtual address to load segment │ │ │
│ │ │ p_paddr : Physical address (usually same as vaddr) │ │ │
│ │ │ p_filesz : Size of segment in file │ │ │
│ │ │ p_memsz : Size of segment in memory (may be > filesz) │ │ │
│ │ │ p_align : Alignment requirement │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ Program Header 1: │ │ │
│ │ │ p_type : PT_LOAD │ │ │
│ │ │ ... │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ ... more headers ... │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Segment Data │ │
│ │ (The actual code and data to be loaded into memory) │ │
│ │ │ │
│ │ .text section (code) │ │
│ │ .rodata section (read-only data) │ │
│ │ .data section (initialized data) │ │
│ │ .bss section (zero-initialized data - NOT in file!) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key ELF Concepts for Bootloaders
The Magic Number:
Bytes 0-3: 0x7F, 'E', 'L', 'F'
If these don’t match, it’s not a valid ELF file.
Class (32-bit vs 64-bit):
Byte 4: 1 = 32-bit (ELFCLASS32)
2 = 64-bit (ELFCLASS64)
For modern kernels, we need ELFCLASS64.
Entry Point:
The e_entry field contains the virtual address where execution should begin after loading.
PT_LOAD Segments:
Only segments with p_type == PT_LOAD need to be loaded into memory. Other types (PT_NOTE, PT_GNU_STACK, etc.) are informational.
Memory vs File Size:
When p_memsz > p_filesz, the extra bytes are the BSS section (uninitialized data). You must zero-fill this region!
Graphics Output Protocol (GOP)
UEFI’s GOP provides direct framebuffer access, replacing VBE/VESA:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Graphics Output Protocol (GOP) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ EFI_GRAPHICS_OUTPUT_PROTOCOL │
│ ├── QueryMode() Get info about a mode │
│ ├── SetMode() Activate a graphics mode │
│ ├── Blt() Block transfer (draw rectangles) │
│ └── Mode Current mode info │
│ ├── MaxMode Number of available modes │
│ ├── Mode Current mode number │
│ ├── Info Pointer to mode information │
│ │ ├── HorizontalResolution │
│ │ ├── VerticalResolution │
│ │ ├── PixelFormat │
│ │ │ ├── PixelRedGreenBlueReserved8BitPerColor (RGBX) │
│ │ │ ├── PixelBlueGreenRedReserved8BitPerColor (BGRX) │
│ │ │ └── PixelBitMask (custom layout) │
│ │ └── PixelsPerScanLine (stride / 4) │
│ ├── SizeOfInfo │
│ └── FrameBufferBase Physical address of framebuffer! │
│ FrameBufferSize Size in bytes │
│ │
│ Framebuffer Memory Layout: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Row 0: | Pixel 0 | Pixel 1 | Pixel 2 | ... | Padding | │ │
│ │ Row 1: | Pixel 0 | Pixel 1 | Pixel 2 | ... | Padding | │ │
│ │ ... │ │
│ │ │ │
│ │ Each pixel (BGRX format): │ │
│ │ ┌──────┬──────┬──────┬──────┐ │ │
│ │ │ Blue │Green │ Red │ Rsvd │ (4 bytes total) │ │
│ │ │ 0x00 │ 0xFF │ 0x00 │ 0x00 │ = Green pixel │ │
│ │ └──────┴──────┴──────┴──────┘ │ │
│ │ │ │
│ │ Address of pixel (x, y): │ │
│ │ FrameBufferBase + (y * PixelsPerScanLine + x) * 4 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The ExitBootServices Transition
This is the most critical part of your bootloader. When you call ExitBootServices():
┌─────────────────────────────────────────────────────────────────────────────┐
│ ExitBootServices Transition │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ BEFORE ExitBootServices: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Boot Services available │ │
│ │ • UEFI controls hardware │ │
│ │ • Memory map stable (until next Boot Services call) │ │
│ │ • Console output works │ │
│ │ • File system access works │ │
│ │ • Protocol location works │ │
│ │ • Timer and event services work │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ ExitBootServices( │ │
│ │ ImageHandle, │ │
│ │ MapKey │ │
│ │ ) │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ │ MapKey must match current map! │
│ │ (Memory map can change between calls) │
│ │ │
│ ▼ │
│ AFTER ExitBootServices: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Boot Services INVALID (calling them = undefined behavior) │ │
│ │ • YOU control hardware now │ │
│ │ • Memory map is final (save it before call!) │ │
│ │ • Console output DEAD (must use framebuffer) │ │
│ │ • No more file access │ │
│ │ • Only RuntimeServices work │ │
│ │ • Must jump to kernel IMMEDIATELY │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ COMMON FAILURE: │
│ GetMemoryMap() → AllocatePool() → GetMemoryMap() → ExitBootServices() │
│ ↑ │
│ │ │
│ └── Memory map changed here! MapKey invalid! │
│ │
│ CORRECT PATTERN: │
│ 1. Do ALL memory allocations first │
│ 2. Load kernel, set up framebuffer, prepare boot info │
│ 3. GetMemoryMap() → ExitBootServices() immediately (no allocations!) │
│ 4. If ExitBootServices fails, loop back to step 3 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Boot Information Structure
Your kernel needs information from the bootloader. This is passed via a structure:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Boot Information Structure │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ typedef struct { │
│ // Magic number to verify struct integrity │
│ UINT64 magic; │
│ │
│ // Framebuffer information │
│ UINT64 framebuffer_addr; │
│ UINT32 framebuffer_width; │
│ UINT32 framebuffer_height; │
│ UINT32 framebuffer_pitch; // Bytes per row │
│ UINT32 framebuffer_bpp; // Bits per pixel (usually 32) │
│ │
│ // Memory map │
│ UINT64 mmap_addr; // Pointer to memory map │
│ UINT64 mmap_size; // Total size of map │
│ UINT64 mmap_desc_size; // Size of each descriptor │
│ │
│ // ACPI tables │
│ UINT64 acpi_rsdp; // Pointer to RSDP │
│ │
│ } BootInfo; │
│ │
│ This structure is placed at a known address or passed in a register. │
│ Common conventions: │
│ • Limine: RDI points to Limine request structure │
│ • Stivale2: RDI points to stivale2 struct │
│ • Custom: Often at 0x1000 or passed in RBX │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 Why This Matters
Building a real bootloader that loads an ELF kernel teaches you:
- Executable formats - How binaries are structured and loaded
- Memory management at boot - Physical memory layout, segment loading
- Graphics initialization - Framebuffer setup before any drivers exist
- The boot handoff - The critical transition from firmware to OS
- ABI design - How to pass data between bootloader and kernel
These skills directly apply to:
- Operating system development
- Firmware engineering
- Security research (boot kits, secure boot)
- Embedded systems
- Understanding how real systems work
2.3 Historical Context
The ELF format was developed at Unix System Laboratories in the late 1980s and became the standard for Unix-like systems by the mid-1990s. Before ELF:
- a.out - Original Unix format (very limited)
- COFF - Common Object File Format (predecessor to PE on Windows)
- PE/COFF - What Windows uses (and UEFI)
UEFI bootloaders loading ELF kernels bridge two worlds: Windows-derived firmware (PE executables) loading Unix-derived kernels (ELF executables).
2.4 Common Misconceptions
Misconception 1: “Just copy the ELF file into memory and jump to entry”
- Reality: You must parse program headers, load each PT_LOAD segment to its specified virtual address, and zero-fill BSS regions.
Misconception 2: “ExitBootServices is simple—just call it”
- Reality: The memory map key must match. Memory map can change between GetMemoryMap and ExitBootServices. You often need a retry loop.
Misconception 3: “GOP mode 0 is always available”
- Reality: Different firmware has different default modes. Query available modes and select an appropriate one.
Misconception 4: “I can use printf after ExitBootServices”
- Reality: No Boot Services means no console. You must draw directly to the framebuffer—or have no output at all!
3. Project Specification
3.1 What You Will Build
A complete UEFI bootloader that:
- Loads an ELF64 kernel from
/EFI/BOOT/kernel.elf - Parses ELF headers and loads PT_LOAD segments
- Sets up a graphics framebuffer via GOP
- Retrieves the final memory map
- Prepares a boot information structure
- Exits Boot Services
- Jumps to the kernel entry point with boot info
3.2 Functional Requirements
| ID | Requirement | Description |
|---|---|---|
| FR-1 | Load ELF file | Read kernel.elf from EFI System Partition |
| FR-2 | Validate ELF | Verify magic, class (64-bit), machine (x86-64) |
| FR-3 | Parse program headers | Iterate through all headers |
| FR-4 | Load segments | Copy PT_LOAD segments to specified addresses |
| FR-5 | Zero BSS | Fill p_memsz - p_filesz with zeros |
| FR-6 | Setup GOP | Configure graphics mode, get framebuffer |
| FR-7 | Get memory map | Retrieve final UEFI memory map |
| FR-8 | Build boot info | Populate boot information structure |
| FR-9 | Exit Boot Services | Correctly call ExitBootServices |
| FR-10 | Jump to kernel | Transfer control to e_entry |
3.3 Non-Functional Requirements
| ID | Requirement | Description |
|---|---|---|
| NFR-1 | Error handling | Report errors before ExitBootServices |
| NFR-2 | Configurable | Graphics mode selection logic |
| NFR-3 | Documented | Clear code comments |
| NFR-4 | Testable | Includes minimal test kernel |
3.4 Example Usage / Output
Bootloader output (before ExitBootServices):
UEFI Bootloader v1.0
====================
Loading kernel from \EFI\BOOT\kernel.elf...
File size: 524288 bytes
Parsing ELF header...
Magic: 0x7F 'E' 'L' 'F' - Valid!
Class: 64-bit
Machine: x86-64
Entry point: 0xFFFFFFFF80100000
Loading segments:
Segment 0: PT_LOAD
File offset: 0x1000
Virtual addr: 0xFFFFFFFF80000000
File size: 0x40000
Memory size: 0x40000
Loading... OK
Segment 1: PT_LOAD
File offset: 0x41000
Virtual addr: 0xFFFFFFFF80040000
File size: 0x10000
Memory size: 0x20000
Loading... OK
Zero-filling BSS (0x10000 bytes)... OK
Setting up graphics...
Mode 0: 800x600 @ 32bpp
Mode 1: 1024x768 @ 32bpp <-- Selected
Mode 2: 1280x1024 @ 32bpp
Framebuffer at: 0x80000000
Pitch: 4096 bytes/row
Getting final memory map...
15 entries, descriptor size 48
Boot info prepared at 0x1000
Exiting Boot Services...
Jumping to kernel at 0xFFFFFFFF80100000!
Kernel output (to framebuffer):
[Screen shows graphical output from your kernel]
Kernel successfully booted!
Framebuffer: 1024x768
Memory: 3071 MB detected
3.5 Real World Outcome
Upon completion, you will have:
- A real bootloader - Capable of loading actual operating system kernels
- Understanding of boot protocols - Similar to Limine, systemd-boot, or Stivale2
- ELF parsing skills - Applicable to debuggers, linkers, loaders
- Graphics initialization - Foundation for any graphical kernel
- Complete boot chain - From firmware to running kernel
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────────────────────┐
│ UEFI ELF Loader Architecture │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EFI System Partition │ │
│ │ /EFI/BOOT/ │ │
│ │ ├── BOOTX64.EFI (Your bootloader) │ │
│ │ └── kernel.elf (64-bit ELF kernel) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Bootloader Phases │ │
│ │ │ │
│ │ Phase 1: Initialization │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ • Store SystemTable pointers ││ │
│ │ │ • Print banner ││ │
│ │ │ • Locate LoadedImage protocol (get boot device) ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Phase 2: File Loading │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ • Get SimpleFileSystem protocol from boot device ││ │
│ │ │ • Open root directory ││ │
│ │ │ • Open kernel.elf file ││ │
│ │ │ • Read file into buffer ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Phase 3: ELF Parsing & Loading │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ • Validate ELF magic, class, machine ││ │
│ │ │ • Read program headers ││ │
│ │ │ • For each PT_LOAD segment: ││ │
│ │ │ ├── Allocate pages at p_paddr ││ │
│ │ │ ├── Copy p_filesz bytes from file ││ │
│ │ │ └── Zero-fill (p_memsz - p_filesz) bytes ││ │
│ │ │ • Record entry point ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Phase 4: Graphics Setup │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ • Locate GOP protocol ││ │
│ │ │ • Query available modes ││ │
│ │ │ • Select appropriate mode (1024x768 or best fit) ││ │
│ │ │ • SetMode to activate ││ │
│ │ │ • Record framebuffer address, size, format ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Phase 5: Boot Info Preparation │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ • Allocate boot info structure ││ │
│ │ │ • Allocate memory for memory map copy ││ │
│ │ │ • Find ACPI RSDP from ConfigurationTable ││ │
│ │ │ • Populate framebuffer info ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Phase 6: ExitBootServices │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ retry: ││ │
│ │ │ GetMemoryMap(&size, buf, &key, &descSize, &descVer) ││ │
│ │ │ Copy memory map to boot info ││ │
│ │ │ status = ExitBootServices(ImageHandle, key) ││ │
│ │ │ if (status == EFI_INVALID_PARAMETER) ││ │
│ │ │ goto retry; ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Phase 7: Kernel Entry │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │
│ │ │ • Set up registers (RDI = boot_info pointer) ││ │
│ │ │ • Jump to e_entry ││ │
│ │ │ • Never returns! ││ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Kernel Running │ │
│ │ • Receives boot_info in RDI │ │
│ │ • Has framebuffer for output │ │
│ │ • Has memory map for physical memory management │ │
│ │ • Full control of machine │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 Key Components
Component 1: File System Access
// Get device handle from which we booted
EFI_LOADED_IMAGE_PROTOCOL *LoadedImage;
BS->HandleProtocol(ImageHandle, &gEfiLoadedImageProtocolGuid, (void**)&LoadedImage);
// Get file system from boot device
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
BS->HandleProtocol(LoadedImage->DeviceHandle,
&gEfiSimpleFileSystemProtocolGuid,
(void**)&FileSystem);
// Open root directory
EFI_FILE_PROTOCOL *Root;
FileSystem->OpenVolume(FileSystem, &Root);
// Open kernel file
EFI_FILE_PROTOCOL *KernelFile;
Root->Open(Root, &KernelFile, L"\\EFI\\BOOT\\kernel.elf",
EFI_FILE_MODE_READ, 0);
Component 2: ELF Header Structures
typedef struct {
unsigned char e_ident[16];
UINT16 e_type;
UINT16 e_machine;
UINT32 e_version;
UINT64 e_entry;
UINT64 e_phoff;
UINT64 e_shoff;
UINT32 e_flags;
UINT16 e_ehsize;
UINT16 e_phentsize;
UINT16 e_phnum;
UINT16 e_shentsize;
UINT16 e_shnum;
UINT16 e_shstrndx;
} Elf64_Ehdr;
typedef struct {
UINT32 p_type;
UINT32 p_flags;
UINT64 p_offset;
UINT64 p_vaddr;
UINT64 p_paddr;
UINT64 p_filesz;
UINT64 p_memsz;
UINT64 p_align;
} Elf64_Phdr;
#define PT_LOAD 1
#define ELFMAG0 0x7f
#define ELFMAG1 'E'
#define ELFMAG2 'L'
#define ELFMAG3 'F'
#define ELFCLASS64 2
#define EM_X86_64 62
Component 3: Segment Loading
void LoadSegment(Elf64_Phdr *phdr, UINT8 *file_base) {
// Calculate number of pages needed
UINTN pages = (phdr->p_memsz + 4095) / 4096;
// Allocate at specific physical address
EFI_PHYSICAL_ADDRESS addr = phdr->p_paddr;
BS->AllocatePages(AllocateAddress, EfiLoaderData, pages, &addr);
// Copy file data
memcpy((void*)phdr->p_paddr,
file_base + phdr->p_offset,
phdr->p_filesz);
// Zero BSS
if (phdr->p_memsz > phdr->p_filesz) {
memset((void*)(phdr->p_paddr + phdr->p_filesz),
0,
phdr->p_memsz - phdr->p_filesz);
}
}
Component 4: GOP Setup
// Locate GOP
EFI_GRAPHICS_OUTPUT_PROTOCOL *GOP;
BS->LocateProtocol(&gEfiGraphicsOutputProtocolGuid, NULL, (void**)&GOP);
// Find 1024x768 mode (or best available)
UINTN SelectedMode = GOP->Mode->Mode; // Default to current
for (UINTN i = 0; i < GOP->Mode->MaxMode; i++) {
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
UINTN InfoSize;
GOP->QueryMode(GOP, i, &InfoSize, &Info);
if (Info->HorizontalResolution == 1024 &&
Info->VerticalResolution == 768) {
SelectedMode = i;
break;
}
}
// Set the mode
GOP->SetMode(GOP, SelectedMode);
// Record framebuffer info
UINT64 fb_addr = GOP->Mode->FrameBufferBase;
UINT32 fb_size = GOP->Mode->FrameBufferSize;
4.3 Data Structures
Boot Information Structure
#define BOOT_INFO_MAGIC 0x424F4F54494E464F // "BOOTINFO"
typedef struct {
UINT64 magic;
// Framebuffer
UINT64 fb_addr;
UINT32 fb_width;
UINT32 fb_height;
UINT32 fb_pitch;
UINT32 fb_bpp;
UINT8 fb_red_mask_shift;
UINT8 fb_green_mask_shift;
UINT8 fb_blue_mask_shift;
UINT8 reserved1;
// Memory map
UINT64 mmap_addr;
UINT64 mmap_entries;
UINT64 mmap_entry_size;
// ACPI
UINT64 rsdp_addr;
// Reserved for future use
UINT64 reserved[8];
} __attribute__((packed)) BootInfo;
Memory Map Entry (converted from UEFI)
typedef struct {
UINT64 base;
UINT64 length;
UINT32 type; // 1=usable, 2=reserved, 3=ACPI reclaimable, etc.
UINT32 reserved;
} __attribute__((packed)) MemoryMapEntry;
4.4 Algorithm Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ Loading Algorithm │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ LoadKernel(filename): │
│ │ │
│ ├── Open file and read into buffer │
│ │ │
│ ├── Validate ELF header: │
│ │ if magic != [0x7F, 'E', 'L', 'F'] → ERROR │
│ │ if class != ELFCLASS64 → ERROR │
│ │ if machine != EM_X86_64 → ERROR │
│ │ │
│ ├── Get entry point: entry = ehdr->e_entry │
│ │ │
│ ├── For each program header (i = 0 to e_phnum): │
│ │ │ │
│ │ ├── phdr = file_base + e_phoff + (i * e_phentsize) │
│ │ │ │
│ │ ├── if phdr->p_type != PT_LOAD: │
│ │ │ continue // Skip non-loadable segments │
│ │ │ │
│ │ ├── pages = ALIGN_UP(p_memsz, 4096) / 4096 │
│ │ │ │
│ │ ├── AllocatePages(AllocateAddress, pages, &p_paddr) │
│ │ │ │
│ │ ├── Copy p_filesz bytes from (file + p_offset) to p_paddr │
│ │ │ │
│ │ └── Zero (p_memsz - p_filesz) bytes at (p_paddr + p_filesz) │
│ │ │
│ └── Return entry point │
│ │
│ NOTE: For higher-half kernels (e_entry > 0xFFFFFFFF80000000), │
│ you may need to set up paging before jumping. For simplicity, │
│ this project assumes identity-mapped lower-half kernels. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5. Implementation Guide
5.1 Development Environment Setup
All prerequisites from Project 7, plus:
# Cross-compiler for building test kernel
sudo apt install gcc-multilib
# ELF tools for inspection
sudo apt install binutils
5.2 Project Structure
uefi-elf-loader/
├── Makefile
├── bootloader/
│ ├── main.c # Entry point, orchestrates boot
│ ├── elf.h # ELF structure definitions
│ ├── elf.c # ELF parsing functions
│ ├── file.c # File system operations
│ ├── graphics.c # GOP setup
│ ├── memory.c # Memory map handling
│ └── bootinfo.h # Boot info structure
├── kernel/
│ ├── kernel.c # Minimal test kernel
│ ├── linker.ld # Kernel linker script
│ └── Makefile
├── disk.img
└── build/
├── BOOTX64.EFI
└── kernel.elf
5.3 The Core Question You’re Answering
How does a bootloader transform a static executable file on disk into running code in memory, and how do we transfer control from firmware to kernel?
This project answers the fundamental question of program loading: parsing executable metadata, allocating physical memory, copying code and data to correct addresses, initializing BSS, and jumping to the entry point with necessary system information.
5.4 Concepts You Must Understand First
- ELF file layout (Chapter 2, “Practical Binary Analysis”)
- Self-assessment: What’s the difference between sections and segments? Which do we care about for loading?
- Virtual vs physical addresses
- Self-assessment: What’s
p_vaddrvsp_paddr? When are they different?
- Self-assessment: What’s
- Memory allocation types
- Self-assessment: What’s
AllocateAddressvsAllocateAnyPages? When do you use each?
- Self-assessment: What’s
- Calling conventions
- Self-assessment: How do you pass a pointer to the kernel in x86-64? What register?
5.5 Questions to Guide Your Design
ELF Loading:
- How do you handle a kernel with multiple PT_LOAD segments?
- What if the kernel expects to be loaded at a high address (higher-half)?
- How do you verify the ELF is for x86-64 and not ARM or 32-bit?
Graphics:
- What resolution should you default to if 1024x768 isn’t available?
- How do you detect the pixel format (RGB vs BGR)?
ExitBootServices:
- What happens if ExitBootServices fails?
- Why might the memory map change between calls?
- How do you handle the retry loop?
Kernel handoff:
- How does the kernel know where the boot info is?
- What state should registers be in when jumping to kernel?
5.6 Thinking Exercise
Before coding, trace through this scenario:
Your kernel.elf has:
- ELF header at offset 0
- 2 program headers starting at offset 64
- Segment 0: PT_LOAD, p_offset=0x1000, p_vaddr=0x100000, p_filesz=0x5000, p_memsz=0x5000
- Segment 1: PT_LOAD, p_offset=0x6000, p_vaddr=0x105000, p_filesz=0x2000, p_memsz=0x4000
- Entry point: 0x100000
Questions:
- How many bytes do you read from the file for segment 0?
- How many pages do you allocate for segment 1?
- How many bytes do you zero-fill for segment 1’s BSS?
- What address do you jump to?
Answers:
- 0x5000 bytes (p_filesz)
- 1 page (0x4000 bytes / 4096 = 1 page)
- 0x2000 bytes (p_memsz - p_filesz)
- 0x100000 (e_entry)
5.7 Hints in Layers
Hint 1: Getting Started (Conceptual Direction)
Start by building on Project 7. Your bootloader already boots and can use UEFI services. The new parts are:
- File system access to read kernel.elf
- ELF parsing (just read the structs and validate)
- GOP for graphics
- The tricky ExitBootServices dance
Get file reading working first. Then add ELF validation. Then segment loading. Save graphics and ExitBootServices for last.
Hint 2: File System Access (More Specific)
The key insight is the protocol chain:
ImageHandle
→ LoadedImage Protocol
→ DeviceHandle
→ SimpleFileSystem Protocol
→ OpenVolume
→ Root Directory
→ Open
→ kernel.elf
To read the file:
// Get file size
EFI_FILE_INFO *FileInfo;
UINTN InfoSize = sizeof(EFI_FILE_INFO) + 256; // Extra for filename
BS->AllocatePool(EfiLoaderData, InfoSize, (void**)&FileInfo);
KernelFile->GetInfo(KernelFile, &gEfiFileInfoGuid, &InfoSize, FileInfo);
UINTN FileSize = FileInfo->FileSize;
// Allocate buffer and read
UINT8 *FileBuffer;
BS->AllocatePool(EfiLoaderData, FileSize, (void**)&FileBuffer);
KernelFile->Read(KernelFile, &FileSize, FileBuffer);
Hint 3: ELF Loading Details (Technical)
Key validation:
Elf64_Ehdr *ehdr = (Elf64_Ehdr *)FileBuffer;
if (ehdr->e_ident[0] != 0x7F || ehdr->e_ident[1] != 'E' ||
ehdr->e_ident[2] != 'L' || ehdr->e_ident[3] != 'F') {
Print(L"Invalid ELF magic!\r\n");
return EFI_INVALID_PARAMETER;
}
if (ehdr->e_ident[4] != ELFCLASS64) {
Print(L"Not 64-bit ELF!\r\n");
return EFI_INVALID_PARAMETER;
}
if (ehdr->e_machine != EM_X86_64) {
Print(L"Not x86-64!\r\n");
return EFI_INVALID_PARAMETER;
}
Loading segments:
for (UINTN i = 0; i < ehdr->e_phnum; i++) {
Elf64_Phdr *phdr = (Elf64_Phdr *)(FileBuffer + ehdr->e_phoff +
i * ehdr->e_phentsize);
if (phdr->p_type != PT_LOAD) continue;
UINTN pages = (phdr->p_memsz + 0xFFF) / 0x1000;
EFI_PHYSICAL_ADDRESS addr = phdr->p_paddr;
EFI_STATUS status = BS->AllocatePages(AllocateAddress,
EfiLoaderData,
pages,
&addr);
if (EFI_ERROR(status)) {
Print(L"Failed to allocate at 0x%lx\r\n", phdr->p_paddr);
return status;
}
// Copy
CopyMem((void*)phdr->p_paddr,
FileBuffer + phdr->p_offset,
phdr->p_filesz);
// Zero BSS
if (phdr->p_memsz > phdr->p_filesz) {
SetMem((void*)(phdr->p_paddr + phdr->p_filesz),
phdr->p_memsz - phdr->p_filesz,
0);
}
}
Hint 4: ExitBootServices Pattern (Advanced)
The critical pattern:
UINTN MapSize = 0, MapKey, DescSize;
UINT32 DescVer;
EFI_MEMORY_DESCRIPTOR *Map = NULL;
// Allocate buffer for memory map BEFORE the loop
BS->GetMemoryMap(&MapSize, NULL, &MapKey, &DescSize, &DescVer);
MapSize += 2 * DescSize; // Safety margin
BS->AllocatePool(EfiLoaderData, MapSize, (void**)&Map);
// Retry loop
while (1) {
MapSize = /* original allocated size */;
EFI_STATUS status = BS->GetMemoryMap(&MapSize, Map, &MapKey,
&DescSize, &DescVer);
if (EFI_ERROR(status)) {
Print(L"GetMemoryMap failed\r\n");
return status;
}
status = BS->ExitBootServices(ImageHandle, MapKey);
if (status == EFI_SUCCESS) break;
// Map changed, retry (but DO NOT allocate - that changes the map!)
if (status == EFI_INVALID_PARAMETER) continue;
// Other error
Print(L"ExitBootServices failed: %r\r\n", status);
return status;
}
// NOW we're past Boot Services - no more Print()!
// Jump to kernel immediately
typedef void (*KernelEntry)(BootInfo *);
KernelEntry entry = (KernelEntry)kernel_entry;
entry(boot_info);
// Never reached
while(1);
5.8 The Interview Questions They’ll Ask
- “Walk me through parsing an ELF file for loading”
- Strong answer: “Read the 64-byte header, validate magic and class. The e_phoff field points to program headers, e_phnum says how many. For each header with p_type == PT_LOAD, allocate p_memsz bytes at p_paddr, copy p_filesz bytes from file offset p_offset, zero-fill the difference. Entry point is e_entry.”
- “Why might ExitBootServices fail?”
- Strong answer: “The MapKey parameter must match the current memory map. Any Boot Services call can change the map. Common pattern: GetMemoryMap, then immediately ExitBootServices. If it fails with EFI_INVALID_PARAMETER, the map changed—retry without any allocations.”
- “How do you pass information from bootloader to kernel?”
- Strong answer: “Define a boot info structure both agree on. Populate it with framebuffer address, dimensions, pixel format, memory map pointer, ACPI RSDP. Pass pointer in RDI (x86-64 calling convention) or at a fixed address. Add a magic number for validation.”
- “What’s the difference between p_filesz and p_memsz in an ELF segment?”
- Strong answer: “p_filesz is bytes in the file, p_memsz is bytes in memory. The difference is BSS—uninitialized data that’s zero-filled. You copy p_filesz bytes from file, then zero-fill (p_memsz - p_filesz) bytes.”
- “Why can’t you use Boot Services after ExitBootServices?”
- Strong answer: “UEFI frees Boot Services memory for the OS to use. Calling them causes undefined behavior. Only Runtime Services remain. That’s why you must save the memory map and set up framebuffer before calling ExitBootServices.”
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| ELF Format | “Practical Binary Analysis” by Dennis Andriesse | Ch. 2 |
| ELF Loading | “Linkers and Loaders” by John Levine | Ch. 3-4 |
| UEFI Protocols | “Beyond BIOS” by Vincent Zimmer | Ch. 5-7 |
| OS Bootloaders | “Operating Systems: Three Easy Pieces” | Appendix |
5.10 Implementation Phases
Phase 1: File System Access (Days 1-2)
- Locate LoadedImage and SimpleFileSystem protocols
- Open and read a test file
- Verify file contents
Phase 2: ELF Parsing (Days 3-4)
- Define ELF structures
- Validate ELF header
- Iterate program headers
- Print segment information
Phase 3: Segment Loading (Days 5-6)
- Allocate memory for each PT_LOAD segment
- Copy segment data
- Zero-fill BSS
Phase 4: Graphics Setup (Days 7-8)
- Locate GOP
- Query available modes
- Select and set mode
- Record framebuffer info
Phase 5: Boot Info and Handoff (Days 9-11)
- Design boot info structure
- Find ACPI RSDP
- Implement ExitBootServices retry loop
- Jump to kernel
Phase 6: Test Kernel (Days 12-14)
- Write minimal kernel that draws to framebuffer
- Verify it receives boot info correctly
- Debug any issues
5.11 Key Implementation Decisions
| Decision | Recommendation | Rationale |
|---|---|---|
| Kernel address | Lower-half (< 2GB) | Avoids paging setup in bootloader |
| Graphics mode | 1024x768 or current | Good default, widely supported |
| Boot info location | Passed in RDI | Standard x86-64 calling convention |
| Memory map format | Convert to simple format | UEFI format is complex |
6. Testing Strategy
6.1 Unit Testing
Test ELF parsing separately:
# Create test ELF file
cat > test.c << 'EOF'
void _start() { while(1); }
EOF
gcc -nostdlib -static -o test.elf test.c
# Inspect with readelf
readelf -h test.elf # Header
readelf -l test.elf # Program headers
6.2 Integration Testing
# Test with OVMF
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw -net none
# With serial for debugging
qemu-system-x86_64 -bios OVMF.fd -drive file=disk.img,format=raw \
-serial stdio -net none
6.3 Test Cases
| Test | Expected | Verification |
|---|---|---|
| Missing kernel | Error message | “kernel.elf not found” |
| Invalid ELF | Error message | “Invalid ELF magic” |
| Wrong arch | Error message | “Not x86-64” |
| Valid load | Kernel runs | Framebuffer shows output |
7. Common Pitfalls & Debugging
7.1 Build Errors
| Problem | Symptom | Solution |
|---|---|---|
| ELF struct size wrong | Parsing fails | Check packed attribute |
| Wrong GUID | Protocol not found | Verify GUID definitions |
| Kernel too big | Allocation fails | Check available memory |
7.2 Runtime Errors
| Problem | Symptom | Solution |
|---|---|---|
| Black screen after jump | Kernel not running | Check entry point address |
| ExitBootServices loop | Never exits | Don’t allocate in retry loop |
| Corrupted display | Garbage pixels | Check pixel format (RGB vs BGR) |
| Triple fault | System resets | Check segment loading addresses |
7.3 Debugging Techniques
Check ELF with readelf:
readelf -h kernel.elf
readelf -l kernel.elf
Debug ExitBootServices:
// Before ExitBootServices, print MapKey
Print(L"MapKey: %lx\r\n", MapKey);
Test framebuffer works:
// After GOP setup, draw test pattern
UINT32 *fb = (UINT32 *)GOP->Mode->FrameBufferBase;
for (int i = 0; i < 1000; i++) fb[i] = 0x00FF0000; // Red
Kernel debugging (before you have serial):
// In kernel, draw colored rectangle based on progress
void debug_pixel(int x, UINT32 color) {
UINT32 *fb = (UINT32 *)boot_info->fb_addr;
for (int y = 0; y < 10; y++)
fb[y * boot_info->fb_pitch/4 + x] = color;
}
8. Extensions & Challenges
Extension 1: Higher-Half Kernel (Hard)
Set up identity paging and map the kernel to the higher half (0xFFFFFFFF80000000).
Extension 2: Initramfs Loading
Load an additional file (initramfs) and pass its address to the kernel.
Extension 3: Command Line
Pass a kernel command line string from a config file.
Extension 4: Multiple Kernels
Implement a boot menu to choose between kernels.
Extension 5: Stivale2 Protocol
Implement the Stivale2 boot protocol for compatibility with Limine-compatible kernels.
9. Real-World Connections
Production Bootloaders
| Bootloader | How It Works |
|---|---|
| Limine | Loads ELF/PE kernels, Stivale2 protocol |
| systemd-boot | Simple EFI stub loader |
| BOOTBOOT | Minimal, multi-platform |
| GRUB2 | Full filesystem support, scripting |
Career Applications
This project demonstrates:
- Binary format expertise (valuable for security, reverse engineering)
- Low-level memory management
- Firmware interface knowledge
- Complete system understanding from power-on to kernel
10. Resources
ELF Documentation
UEFI Protocols
- UEFI Specification - GOP Ch. 12
- UEFI Specification - File System Ch. 13
Example Code
11. Self-Assessment Checklist
Understanding
- I can explain ELF header fields
- I understand PT_LOAD segments
- I know why BSS must be zero-filled
- I can explain ExitBootServices requirements
- I understand GOP and framebuffers
Implementation
- Bootloader loads kernel.elf
- ELF validation works
- Segments load to correct addresses
- BSS is zeroed
- Graphics mode is set
- ExitBootServices succeeds
- Kernel runs and draws to screen
Debugging
- I can use readelf to inspect ELF files
- I can debug ExitBootServices failures
- I can verify framebuffer works
12. Submission / Completion Criteria
Your project is complete when:
-
Bootloader builds without errors
- Test kernel runs:
- Receives boot info correctly
- Draws to framebuffer
- Shows memory map entries
- Handles errors:
- Missing kernel file
- Invalid ELF format
- Allocation failures
- Code quality:
- Clean, modular code
- Proper error handling
- Comments explaining key steps
- Documentation:
- README with build instructions
- Boot info structure documented
Congratulations! You’ve built a real bootloader that loads an operating system kernel. This is the same architecture used by production bootloaders like Limine and systemd-boot. You now understand the complete boot chain from UEFI firmware to running kernel.
Previous: P07 - UEFI Hello World Next: P09 - Raspberry Pi Bare-Metal