x86 Boot Chain: BIOS, UEFI & Kernel Handoff

Booting is a chain of explicit contracts: reset selects firmware, firmware selects a loader, the loader constructs a kernel image and handoff record, and only then does the kernel take ownership of the machine.

Boot is not one program and not one universal ABI. It is a sequence of handoffs in which each producer promises a particular machine state and data layout to the next consumer. The useful question at every boundary is therefore not merely “what runs next?” but “what exactly may the next stage assume, who owns each byte of memory, and which services are still callable?”

processor reset firmware owns the platform legacy BIOS contract POST · boot policy · INT 19h select a device and read one sector 512 bytes at physical 0x7C00 16-bit entry · boot drive convention stage 1 → larger loader BIOS I/O · memory discovery · mode setup UEFI image contract BootOrder · Boot#### · device path select a PE/COFF UEFI image image handle + System Table typed protocols and Boot Services UEFI OS loader load payloads · final map · exit services loader constructs the kernel image memory ownership · payloads · tables · explicit handoff kernel-specific entry contract
BIOS begins with a sector-sized real-mode contract; UEFI begins with a typed application and service tables. Neither contract, by itself, defines the kernel entry ABI.

The chain is a stack of contracts #

The major boundaries are:

  1. Reset → firmware. The processor starts at its architectural reset state. On current Intel 64 processors, the first fetched instruction is at physical address 0xFFFFFFF0; firmware owns the platform from there. This reset vector is not the boot sector and not the kernel entry point.
  2. Firmware → loader. Legacy BIOS convention loads a small sector and enters 16-bit code. UEFI loads a PE/COFF application and calls its typed entry point with an image handle and the EFI System Table.
  3. Loader → kernel. The loader must implement a kernel-specific protocol: Linux/x86, Multiboot2, an EFI stub, or a deliberately specified custom ABI. Firmware does not invent this last contract.
  4. Early kernel → initialized kernel. The entry stub must stop depending on loader-owned state, establish the kernel's own stack, tables, mappings, console, and allocators, then release or reclaim temporary boot data according to its declared lifetime.

The distinction prevents a common category error. “The machine boots through UEFI” says how the loader was launched. It does not say whether the kernel expects Linux boot_params, Multiboot2 tags, the UEFI image ABI, or a private BootInfo * in a chosen register.

Legacy BIOS path: from one sector to a real loader #

Firmware dispatch, not a jump straight from reset #

Historical PC firmware performs hardware initialization and POST, installs real-mode BIOS interfaces, selects a boot device, and invokes its bootstrap path. The IBM PC AT technical reference places the bootstrap at interrupt 0x19; its BIOS listing calls INT 19h after POST. That is historical evidence for the firmware dispatch boundary, not a promise that every later BIOS has identical internals. See the IBM PC AT Technical Reference.

For disk boot, the durable convention is demonstrated plainly by current SeaBIOS boot code: read one 512-byte sector through INT 13h into segment 0x07C0 (physical 0x7C00), optionally verify the 0xAA55 signature, place the boot drive number in DL, and far-call the loaded code. A robust stage 1 treats every other register, the stack, and incidental memory contents as unspecified unless its exact firmware contract says otherwise.

A boot sector is a constraint, not an architecture #

The loaded unit is one sector. On an MBR-partitioned disk, the traditional layout is:

Offset Size Conventional meaning
0x000 446 bytes bootstrap code and embedded data
0x1BE 64 bytes four 16-byte partition entries
0x1FE 2 bytes signature bytes 55 AA in file order

That table describes an MBR. A floppy-style volume boot record or another boot-sector format can use the first 510 bytes differently. “Boot sector” and “MBR” are therefore not synonyms.

The first-stage job should be mechanically small:

  • establish known segment registers and a stack before using either;
  • preserve the firmware's boot-drive identifier if later disk reads need it;
  • locate and read a larger second stage;
  • validate every disk result and every destination range;
  • transfer control under a documented private contract.

Filesystem parsing, menus, decompression, ELF loading, memory-map normalization, and mode transitions do not become simpler when squeezed into 446 bytes. Multi-stage loaders exist because the first read is tiny and because later code benefits from ordinary linking, symbols, checks, and structured data.

BIOS services have a lifetime #

Legacy BIOS disk, video, keyboard, and memory-discovery interfaces are primarily real-mode services. A loader can use them while it still maintains the expected real-mode environment. Once it changes execution mode, interrupt tables, PIC state, or mappings, a BIOS call is no longer automatically safe merely because the interrupt number still exists.

A practical BIOS loader therefore gathers firmware-dependent data before the final departure:

  • a physical memory map, preferably from the extended memory-map interface rather than a single “amount of RAM” number;
  • boot-device identity if the kernel wants it as a hint;
  • video or framebuffer information if display output must survive the handoff;
  • ACPI or other platform-table roots when the chosen kernel protocol carries them;
  • all kernel, module, command-line, and early-stack bytes.

It then enables A20 if required, prepares GDT/page tables/IDT policy, transitions through protected mode and possibly IA-32e mode, and enters the kernel. The exact transition belongs to the execution-modes chapter; it is part of the loader-to-kernel contract, not an incidental prelude.

UEFI path: an application with typed services #

UEFI replaces the boot-sector entry convention with a firmware-managed application environment. The UEFI 2.11 specification defines the boot manager, image loading, protocols, memory map, and service lifetimes.

Selection and image entry #

For normal boot, firmware reads BootOrder and corresponding Boot#### NVRAM load options. A load option identifies a device path and a UEFI image. For removable media with no explicit filename, the default x64 path is:

text
\EFI\BOOT\BOOTX64.EFI

The image is PE/COFF, not a raw sector and not automatically ELF. Firmware loads it with LoadImage(), starts it with StartImage(), and invokes an entry point conceptually shaped like:

c
EFI_STATUS EFIAPI efi_main(
    EFI_HANDLE image_handle,
    EFI_SYSTEM_TABLE *system_table
);

The System Table leads to console handles, configuration tables, Boot Services, and Runtime Services. Protocol handles expose filesystems, disks, graphics, networking, loaded-image metadata, and other capabilities. The loader should use those interfaces rather than assuming PC-compatible port layouts while it remains a UEFI application.

ExitBootServices() is an ownership boundary #

Boot Services are valid only until a successful ExitBootServices(). Runtime Services have a different lifetime and require the kernel to preserve and, if used virtually, correctly map their designated memory. The critical sequence is:

  1. Finish file I/O, console output, protocol discovery, and ordinary allocations.
  2. Call GetMemoryMap(). If the supplied buffer is too small, allocate enough space with margin and retry.
  3. Save the returned descriptors, descriptor size/version, and MapKey in the handoff data.
  4. Avoid operations that can allocate or free memory.
  5. Call ExitBootServices(image_handle, MapKey) immediately.
  6. If it returns EFI_INVALID_PARAMETER, obtain a new map and key, then retry. Do not continue with the stale map.

The key changes whenever the firmware memory map changes. An innocent allocation, log operation, or protocol call between the final GetMemoryMap() and ExitBootServices() can therefore invalidate the key. After success, Boot Services and handle-based device protocols are unavailable; the OS owns available memory, while Runtime Services regions and other reserved types must remain protected according to their descriptors.

finish firmware work files · protocols · allocations final handoff storage GetMemoryMap() descriptors + stride current MapKey ExitBootServices() same image handle matching MapKey status result stale key → obtain a fresh map and retry success OS owns available memory Boot Services are invalid preserve reserved/runtime regions Before success: firmware owns resources; allocations can change the key. After success: no Boot Service or handle-based device protocol may be called.
The loader may retry with a fresh map when the key is stale. After success it must follow the post-exit path; calling a Boot Service is a contract violation.

Secure Boot answers a different question #

Image authentication can decide whether firmware is willing to execute a loader. It does not prove that the loader interprets an ELF file correctly, preserves the memory map, passes the right ABI, or configures CPU state safely. Trust and correctness are separate chains; a useful design records both instead of using “Secure Boot passed” as evidence that kernel entry is valid.

Firmware contract versus kernel contract #

The loader must choose exactly one kernel-facing protocol and implement all of it. Similar-looking fields do not make protocols interchangeable.

Kernel-facing contract Image recognition Entry state Information transfer Important boundary
Custom whatever the loader validates fully project-defined project-defined versioned structure every assumption must be written down and tested
Linux/x86 boot protocol Linux real-mode header and HdrS fields protocol-version-specific boot_params, setup header fields, command line, initrd, setup data old compatibility fields and load ceilings matter
Multiboot2 header with magic, architecture, checksum, and tags specified i386 or EFI state aligned, typed tag list addressed through a defined register many fields are advisory; unspecified registers remain unspecified
Linux EFI stub Linux image presents a PE/COFF-compatible EFI entry UEFI image ABI stub translates firmware state into Linux's internal boot path the kernel image itself performs the loader-facing EFI work

The current Linux/x86 boot protocol documents a real-mode header beginning around offset 0x1F1, boot_params, load addresses, command line, initrd, and newer extension mechanisms. It is not a generic rule for arbitrary kernels.

Multiboot2 specifies recognizer tags in the image and an 8-byte-aligned boot-information structure composed of typed tags. For its classic i386 entry, EAX carries magic 0x36D76289, EBX points to the information structure, A20 is enabled, protected mode is active, paging and interrupts are disabled, and the kernel must promptly establish its own stack, GDT, and IDT. Its EFI variants deliberately define different state and can declare that Boot Services remain active. “Multiboot2” is therefore not one vague register convention.

The Linux EFI stub shows a third composition: an x86 bzImage masquerades as a PE/COFF image, so firmware can launch the kernel image directly and the embedded stub performs the EFI-loader work. Omitting an external loader does not omit the loader responsibilities; it moves them into the image.

Loading an executable is constructing a memory image #

A flat binary says little beyond “copy these bytes somewhere and jump according to an external convention.” ELF and PE/COFF carry structure, but structure is not self-executing. A loader must validate it and create the promised memory image.

For an ELF kernel, the ELF program-loading specification makes program headers—not section headers—the load plan. For each PT_LOAD segment, a loader must:

  1. validate that file offsets, sizes, arithmetic, and destination ranges do not overflow or escape the image;
  2. enforce p_filesz <= p_memsz and the required p_align congruence;
  3. reserve a non-overlapping destination with the required alignment;
  4. copy p_filesz bytes beginning at p_offset;
  5. zero the remaining p_memsz - p_filesz bytes, which commonly materialize .bss;
  6. record or install permissions implied by p_flags when the kernel's paging contract uses them.

ELF defines p_vaddr, while the generic ABI leaves p_paddr unspecified for ordinary application executables. A bare-metal kernel format must therefore state whether its load addresses and e_entry are physical, virtual, identity-mapped, relocated by a fixed slide, or interpreted through another rule. Jumping to e_entry as a physical address simply because the number looks plausible is not a contract.

Design the handoff record as an ABI #

A durable custom handoff begins with identity and evolution fields:

c
struct BootInfo {
    uint64_t magic;
    uint32_t version;
    uint32_t size;
    // Versioned fields follow.
};

The complete contract should state:

Category Questions that must have one answer
Entry Which symbol/address? Which calling convention? Which register carries the pointer? Is a return legal?
CPU state 32-bit protected, compatibility, or 64-bit mode? Paging on? Interrupt flag? Direction flag? FPU/SIMD initialized?
Segmentation Which selectors are live? May the kernel reload them? Are GDTR/IDTR/TSS valid or explicitly unusable?
Stack Initial RSP, alignment, mapped extent, guard policy, and ownership?
Addresses Is each field physical, virtual, or an offset? What mapping makes it reachable?
Memory Full typed map, descriptor stride/version, reserved ranges, loader ranges, and the exact moment the snapshot became final?
Payloads Kernel segments, initrd/modules, command line, symbols, and their byte lengths?
Platform ACPI root, SMBIOS, framebuffer base/size/pitch/format, and optional UEFI System Table/runtime map?
Lifetime Which objects are immutable, reclaimable after copy, reclaimable after initialization, or permanently reserved?
Failure What magic/version/range checks happen before the first dereference?

Pointers alone are insufficient. A pointer needs an address space, a size, an alignment, an owner, and a lifetime. “Memory map at 0x1234” is not usable until the kernel also knows whether 0x1234 is physical or virtual, how entries are stepped, and whether the loader can still mutate the backing allocation.

A worked UEFI-to-x86-64 sequence #

Assume a custom x86-64 kernel with ELF load segments and a BootInfo * in RDI under the System V AMD64 calling convention. A coherent loader can proceed as follows:

  1. UEFI invokes the loader under the UEFI x64 image ABI. A small wrapper keeps that ABI separate from the kernel ABI.
  2. The loader opens the kernel and optional initrd, validates their complete lengths and formats, reserves destination pages, copies every PT_LOAD, and zeros each tail.
  3. It allocates a kernel-owned early stack, handoff block, page tables, and any copied platform data. All ranges are recorded as unavailable to the early allocator.
  4. It constructs mappings that cover the current transition code, new stack, handoff block, kernel segments, and any physical-map window required by the entry stub.
  5. It finishes diagnostics and protocol I/O, then obtains the final UEFI memory map and exits Boot Services with the matching key.
  6. With no firmware Boot Service remaining, it loads the kernel's required tables/mappings, sets RSP and the agreed flags/registers, places the validated virtual BootInfo pointer in RDI, and calls or jumps to the declared entry point.
  7. The kernel validates magic, version, size, canonical addresses, ranges, and overlaps before consuming optional fields. It copies anything whose later lifetime is uncertain before reclaiming loader memory.

If the custom ABI instead promises Microsoft x64 calling convention, physical pointers, or entry with paging disabled, the sequence changes. The point is not that the example is universal; it is that every dependency is visible.

Diagnosing the boundary that actually failed #

Symptom Boundary to inspect first Decisive evidence
Firmware reports no boot target firmware → loader selection Boot#### device path, ESP filesystem, fallback filename, PE machine type
UEFI app starts and returns to menu image → loader control flow EFI status, loader log before return, image entry symbol
BIOS stage 1 prints but stage 2 never starts sector → stage 2 boot drive in DL, LBA/CHS request, carry/status, load address, stage-2 signature
Loader reaches ELF parse, kernel entry is nonsense file → memory image program headers, copied ranges, zero tails, relocation/address rule, e_entry mapping
ExitBootServices() fails intermittently loader → firmware ownership map/key pair and any allocation or output between the two calls
First kernel memory scan corrupts itself handoff → allocator normalized map plus explicit loader/kernel/module/page-table reservations
Instant reset near mode or IDT setup CPU-state contract first fault, descriptor bases/limits, transition mapping, double-fault stack—not only the final reset
Screen goes blank after UEFI exit service lifetime or framebuffer contract no post-exit console call; framebuffer base, pitch, dimensions, pixel layout, mapping

For emulation, QEMU's GDB interface provides a clean temporal boundary: -S prevents guest execution at startup and -s exposes a GDB server on TCP port 1234. Keep symbol files for each linked stage, break at the firmware-visible loader entry and the kernel entry, and inspect the actual mode/register/table state at both points. A breakpoint that never fires answers a different question from one that fires and then faults.

The strongest boot trace records one line per ownership transition: selected image, loaded ranges, final memory-map generation/key, Boot Services exit result, mode and control registers, GDTR/IDTR, stack, handoff pointer, and entry address. That trace turns “black screen” into a finite contract audit.

Visual source audit #

The IBM PC AT scan was inspected at the real-mode interrupt table, POST-to-INT 19h listing, bootstrap-vector entry, and reserved-memory pages. These are valuable primary historical evidence but too scan-specific and too low-level to serve as the article's cross-era model, so none were reproduced. The Multiboot2 PDF's entry-state and tag-layout pages were inspected; their monospaced wire layouts are accurately and more accessibly represented by the protocol comparison and handoff table above. Intel's reset and system-state figures were inspected for the reset-vector distinction. The two retained SVGs are independent, theme-aware syntheses of verified control and ownership relationships; they intentionally do not reproduce firmware implementation internals that the specifications leave opaque.

Sources

  1. docsIntel 64 and IA-32 Architectures Software Developer's Manualpdf
  2. docsIBM Personal Computer AT Technical Referencepdf
  3. docsUnified Extensible Firmware Interface Specification, Release 2.11
  4. docsThe Multiboot2 Specification, Version 2.0pdf
  5. docsThe Linux/x86 Boot Protocol
  6. docsThe EFI Boot Stub
  7. docsELF Object File Format: Program Loading
  8. codeSeaBIOS Boot Code
  9. docsQEMU System Emulation: GDB Usage

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close