Operating Systems · Boot and Firmware

How Firmware Describes Physical Memory to the Kernel

The E820 and UEFI memory map formats, why reserved ranges outnumber usable ones, and how the kernel turns a list of address ranges into its own page allocator.

A kernel cannot simply assume that physical memory is one contiguous range starting at zero. Some of it is missing, some of it is present but unusable, some of it belongs to hardware that will misbehave if it is overwritten, and some of it holds data the kernel needs during its first milliseconds. The firmware is the only component that knows which is which, because it is the only component that has talked to the memory controller, enumerated the buses, and reserved the regions its own runtime needs. It is also the component that runs before any of this is possible, from the reset vector onward. Handing that knowledge across the boot boundary is a data-format problem, and the two formats in use reflect two very different assumptions about who is in charge.

What has to be conveyed

The set of facts is short but each one matters. Which address ranges hold DRAM that can be allocated. Which ranges hold DRAM that must not be touched because firmware still uses it. Which ranges are not memory at all but device apertures, where a load will return all-ones or, worse, have a side effect. Which ranges hold ACPI tables, the SMBIOS structure table, and any firmware-specific blobs the kernel will later want. And whether the machine supports memory above the 4 GiB boundary, which it may even when the installed DRAM is smaller than 4 GiB, because the region just below 4 GiB is usually carved out for PCI apertures and the remainder is remapped above.

None of this can be inferred by probing. Writing to a device aperture to see whether it behaves like memory can assert a bus transaction with consequences, and writing to firmware-owned DRAM can corrupt the very structures the kernel is about to read. The map must be given, not discovered.

The E820 contract

The legacy mechanism is a firmware service reached through a software interrupt, which returns one entry per call until it reports no more. Each entry is 20 bytes: a 64-bit base address, a 64-bit length, and a 32-bit type. A later revision appended a 32-bit extended-attributes field, which a caller requests by passing a buffer size of 24 bytes; a caller that passes 20 receives the shorter record and loses the attribute bits. The types are a small enumeration: 1 usable, 2 reserved, 3 ACPI reclaimable, 4 ACPI NVS, 5 bad memory.

The reclaimable and NVS distinctions are the interesting ones. Type 3 memory holds ACPI tables that the kernel will copy and then stop needing, so it can be reused after that point; type 4 holds firmware data that must survive, so it stays reserved for the lifetime of the boot. A kernel that treated all non-usable memory alike would either leak a few megabytes of reclaimable pages or free memory that the platform still reads.

The entries arrive in ascending address order, but nothing in the interface guarantees that ranges are disjoint or that adjacent entries are merged. Firmware produced by different vendors differs here. The kernel sorts and coalesces the list during early setup, then walks it to build two things: a bitmap or interval tree of usable frames for the page allocator, and a set of named reservations for ranges it must avoid.

The record layout is worth writing out because the field widths are the interface:

struct e820_entry {
    uint64_t base;      /* first physical address of the range */
    uint64_t length;    /* bytes, not pages */
    uint32_t type;      /* 1 usable, 2 reserved, 3 ACPI reclaim, 4 ACPI NVS, 5 bad */
} __attribute__((packed));

The packed attribute is what makes the size 20 rather than 24. Two 64-bit fields and one 32-bit field occupy 20 bytes, but a compiler rounds a structure’s size up to a multiple of its 8-byte alignment, so without the attribute sizeof reports 24 even though the last four bytes are padding. A later revision of the interface defined an extended entry that appends a 32-bit attributes field, making the record 24 bytes; a caller requests the extended form by passing 24 as the buffer size, and a caller that passes 20 receives the shorter record without the attribute bits. A caller that assumes 24-byte records against firmware that produces only 20-byte records reads every entry after the first from four bytes into the previous one.

The UEFI contract

UEFI replaces the interrupt-driven call with a table of function pointers and a descriptor array. The firmware fills a buffer with EFI_MEMORY_DESCRIPTOR records of a caller-declared size, and the caller calls again if the buffer was too small; the required size is reported so the retry can be sized correctly. Each descriptor carries a type, a physical start, a virtual start, a page count, and attribute bits. Because the count is in 4 KiB pages rather than bytes, a map covering a machine with a hole at the 4 GiB boundary is a list of page-aligned ranges, and the size of the array scales with the number of distinct ranges rather than with the amount of memory.

The type enumeration is larger and encodes more policy. EfiConventionalMemory is the equivalent of E820 usable. EfiLoaderCode and EfiLoaderData are memory the boot loader used and which the kernel may take over. EfiBootServicesCode and EfiBootServicesData are reclaimable once ExitBootServices has been called. EfiRuntimeServicesCode and EfiRuntimeServicesData must remain mapped because the firmware’s runtime calls, which the kernel invokes for variables and reset, execute from them. EfiACPIReclaimMemory and EfiACPIMemoryNVS mirror the ACPI distinction. EfiMemoryMappedIO and EfiMemoryMappedIOPortSpace mark apertures that must never be allocated.

The critical structural difference is that the UEFI map describes memory the firmware may still change. Before ExitBootServices returns success, the firmware may allocate, move, or free memory at any time, so a map taken earlier is a stale snapshot. The call itself takes a map key obtained with the map, and if the firmware’s view changed between the two calls the key mismatches and the call fails, forcing a retry. This is a genuine race, not a formality: a timer event or a device notification can allocate memory between the two calls, and the loop that retries until the key matches is the standard way to handle it.

From a range list to a page allocator

The kernel does not keep the firmware’s format. Both formats are converted into an internal early reservation structure, and the usable ranges are registered with the bootmem or memblock allocator, which hands out memory before the real page allocator exists. Memblock keeps two arrays: one of available regions and one of reserved regions, each a sorted list of base-and-size pairs. Allocation during early boot walks the available array and carves from the first region that fits, which is why early allocations cluster at low addresses.

The reserved array is where the firmware’s warnings land. ACPI tables are reserved and then, once parsed, moved into the kernel’s own copy and released. The initramfs image is reserved until it has been unpacked. The kernel’s own text and data are reserved. Crashkernel regions, if configured, are reserved from the start so that a second kernel can be loaded into them later. On x86, the first megabyte is largely reserved because of the interrupt vector table, the BIOS data area, and the real-mode trampoline the kernel uses to bring up secondary processors.

Only after all of this is recorded does the page allocator take over, and its frame bitmap is sized from the usable ranges rather than from the highest address. A machine with a 64 GiB hole in the middle of its physical map therefore costs bitmap space in proportion to its actual DRAM, not to its address span.

Where the map lies

Firmware bugs in memory maps are common enough that the kernel carries a table of quirks keyed on firmware vendor and version strings. Ranges may be marked usable that overlap a reserved range; the kernel resolves this by treating the intersection as reserved, since a wrong reservation wastes memory while a wrong usable range corrupts data. A range may extend past the top of installed DRAM, producing a machine check on first touch; the kernel clips ranges against the values it reads from the memory controller. A machine may report memory above 4 GiB as usable without reporting the PCI hole below it, which would make the allocator hand out aperture addresses.

The deeper problem is that the map is a snapshot of a policy the firmware applied for its own convenience. It does not say why a range is reserved, so the kernel cannot know whether it may ever be released. The ACPI reclaimable type is the only case where the firmware commits to a lifetime, and even that depends on the kernel having actually copied the tables.

Once the kernel knows which memory it owns, it still needs to be running in a mode that can address it. On x86 that means the boot loader must have established long mode and page tables before the kernel’s first instruction, a transition with its own ordering constraints described in entering long mode from a boot loader.

References

  1. Advanced Configuration and Power Interface Specification — UEFI Forum
  2. Unified Extensible Firmware Interface Specification — UEFI Forum
  3. Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3: System Programming Guide — Intel Corporation