Chapter 11: Memory Services
Master the UEFI memory model – allocations, memory types, the memory map, and the pitfalls that catch even experienced firmware engineers.
11.1 The UEFI Memory Model
UEFI operates in flat physical address mode with paging optionally enabled. Unlike an operating system, there is no virtual memory in the traditional sense during boot. All pointers are physical addresses (or identity-mapped virtual addresses). This changes only after ExitBootServices(), when the OS may choose to call SetVirtualAddressMap() to establish its own mapping.
graph TD
subgraph PhysicalMemory["Physical Address Space"]
direction TB
A["0x00000000\nLegacy Region / Reserved"]
B["0x00100000\nUsable Memory\n(EfiConventionalMemory)"]
C["Firmware Reserved\n(EfiReservedMemoryType)"]
D["MMIO Regions\n(EfiMemoryMappedIO)"]
E["Boot Services Code/Data\n(freed after ExitBootServices)"]
F["Runtime Services Code/Data\n(persists into OS)"]
G["ACPI Tables\n(EfiACPIReclaimMemory)"]
H["Loader Data\n(OS boot loader allocations)"]
end
style PhysicalMemory fill:#1a1a2e,stroke:#e94560,color:#fff
style E fill:#0f3460,stroke:#e94560,color:#fff
style F fill:#533483,stroke:#e94560,color:#fff
11.2 Memory Types
Every memory allocation in UEFI has a type that tells the system (and the OS) how the memory is used and when it can be reclaimed.
| Memory Type | Value | Description | Freed after ExitBootServices? |
|---|---|---|---|
EfiReservedMemoryType |
0 | Not usable by UEFI or OS | No |
EfiLoaderCode |
1 | Code loaded by the OS boot loader | OS decides |
EfiLoaderData |
2 | Data allocated by the OS boot loader | OS decides |
EfiBootServicesCode |
3 | UEFI Boot Service driver code | Yes |
EfiBootServicesData |
4 | UEFI Boot Service driver data | Yes |
EfiRuntimeServicesCode |
5 | UEFI Runtime driver code | No – persists |
EfiRuntimeServicesData |
6 | UEFI Runtime driver data | No – persists |
EfiConventionalMemory |
7 | Free, unallocated memory | N/A (already free) |
EfiUnusableMemory |
8 | Memory with errors | No |
EfiACPIReclaimMemory |
9 | ACPI tables – OS may reclaim after parsing | OS decides |
EfiACPINvs |
10 | ACPI NVS – must not be reclaimed | No |
EfiMemoryMappedIO |
11 | Memory-mapped I/O registers | No |
EfiMemoryMappedIOPortSpace |
12 | I/O port space (IA-64 specific) | No |
EfiPalCode |
13 | Processor Abstraction Layer (IA-64) | No |
EfiPersistentMemory |
14 | Persistent memory (NVDIMM) | No |
The most common types you will use in driver code are
EfiBootServicesData(the default forAllocatePool) andEfiRuntimeServicesData(for data that must survive into the OS runtime). Choosing the wrong type can cause the OS to reclaim memory your runtime driver still needs, leading to crashes after boot.
11.3 Pool Allocation: AllocatePool / FreePool
Pool allocation is the UEFI equivalent of malloc() and free(). It is the simplest and most common allocation interface.
AllocatePool
EFI_STATUS
EFIAPI
gBS->AllocatePool (
IN EFI_MEMORY_TYPE PoolType,
IN UINTN Size,
OUT VOID **Buffer
);
FreePool
EFI_STATUS
EFIAPI
gBS->FreePool (
IN VOID *Buffer
);
Basic usage
CHAR16 *String;
Status = gBS->AllocatePool (
EfiBootServicesData,
256 * sizeof (CHAR16),
(VOID **)&String
);
if (EFI_ERROR (Status)) {
return Status;
}
StrCpyS (String, 256, L"Hello from pool memory!");
// ... use String ...
gBS->FreePool (String);
Library wrappers
The MemoryAllocationLib provides convenient wrappers that most EDK II / Project Mu code uses:
#include <Library/MemoryAllocationLib.h>
// Allocate zeroed memory (EfiBootServicesData)
VOID *Buffer = AllocateZeroPool (1024);
// Allocate runtime memory
VOID *RtBuffer = AllocateRuntimeZeroPool (256);
// Allocate and copy
VOID *Copy = AllocateCopyPool (SourceSize, SourceBuffer);
// Free
FreePool (Buffer);
FreePool (RtBuffer);
FreePool (Copy);
| Library function | Memory type | Zeroed? |
|---|---|---|
AllocatePool(Size) |
EfiBootServicesData |
No |
AllocateZeroPool(Size) |
EfiBootServicesData |
Yes |
AllocateRuntimePool(Size) |
EfiRuntimeServicesData |
No |
AllocateRuntimeZeroPool(Size) |
EfiRuntimeServicesData |
Yes |
AllocateCopyPool(Size, Src) |
EfiBootServicesData |
Copied from Src |
AllocateRuntimeCopyPool(Size, Src) |
EfiRuntimeServicesData |
Copied from Src |
ReallocatePool(Old, New, Ptr) |
EfiBootServicesData |
Old data copied |
Pool allocations are not page-aligned. If you need page-aligned memory (for DMA buffers, page tables, or MMIO mappings), use
AllocatePages()instead.
11.4 Page Allocation: AllocatePages / FreePages
Page allocation gives you memory in multiples of 4 KiB pages with control over the physical address.
AllocatePages
EFI_STATUS
EFIAPI
gBS->AllocatePages (
IN EFI_ALLOCATE_TYPE Type,
IN EFI_MEMORY_TYPE MemoryType,
IN UINTN Pages,
IN OUT EFI_PHYSICAL_ADDRESS *Memory
);
The Type parameter controls where the memory comes from:
| AllocateType | Behavior |
|---|---|
AllocateAnyPages |
Allocate from anywhere in memory |
AllocateMaxAddress |
Allocate at or below the address in *Memory |
AllocateAddress |
Allocate at the exact address in *Memory |
Examples
// Allocate 4 pages (16 KiB) anywhere
EFI_PHYSICAL_ADDRESS DmaBuffer;
Status = gBS->AllocatePages (
AllocateAnyPages,
EfiBootServicesData,
4, // Number of 4K pages
&DmaBuffer
);
if (!EFI_ERROR (Status)) {
// DmaBuffer now contains a page-aligned physical address
ZeroMem ((VOID *)(UINTN)DmaBuffer, 4 * EFI_PAGE_SIZE);
}
// Allocate below 4 GB (required for some 32-bit DMA hardware)
EFI_PHYSICAL_ADDRESS Below4G = 0xFFFFFFFF;
Status = gBS->AllocatePages (
AllocateMaxAddress,
EfiBootServicesData,
1,
&Below4G
);
// Allocate at a specific physical address (rare, for legacy compatibility)
EFI_PHYSICAL_ADDRESS LegacyAddr = 0x000E0000;
Status = gBS->AllocatePages (
AllocateAddress,
EfiReservedMemoryType,
16, // 64 KiB
&LegacyAddr
);
FreePages
gBS->FreePages (DmaBuffer, 4); // Free 4 pages starting at DmaBuffer
Macro helpers
// Convert a byte size to a page count (rounding up)
#define EFI_SIZE_TO_PAGES(Size) (((Size) + EFI_PAGE_SIZE - 1) / EFI_PAGE_SIZE)
// Convert a page count to bytes
#define EFI_PAGES_TO_SIZE(Pages) ((Pages) * EFI_PAGE_SIZE)
11.5 The Memory Map
The memory map is a table describing every region of physical memory: its start address, size, type, and attributes. It is one of the most important data structures in the UEFI-to-OS handoff.
GetMemoryMap
EFI_STATUS
EFIAPI
gBS->GetMemoryMap (
IN OUT UINTN *MemoryMapSize,
OUT EFI_MEMORY_DESCRIPTOR *MemoryMap,
OUT UINTN *MapKey,
OUT UINTN *DescriptorSize,
OUT UINT32 *DescriptorVersion
);
The memory descriptor
typedef struct {
UINT32 Type; // EFI_MEMORY_TYPE
EFI_PHYSICAL_ADDRESS PhysicalStart;
EFI_VIRTUAL_ADDRESS VirtualStart;
UINT64 NumberOfPages;
UINT64 Attribute; // Memory attributes (cacheability, etc.)
} EFI_MEMORY_DESCRIPTOR;
Reading the memory map correctly
The memory map must be read using a two-call pattern because its size is not known in advance and the act of allocating memory changes the map:
EFI_STATUS
GetAndPrintMemoryMap (
VOID
)
{
EFI_STATUS Status;
UINTN MemoryMapSize = 0;
EFI_MEMORY_DESCRIPTOR *MemoryMap = NULL;
UINTN MapKey;
UINTN DescriptorSize;
UINT32 DescriptorVersion;
UINTN Index;
UINTN EntryCount;
//
// First call: get the required buffer size
//
Status = gBS->GetMemoryMap (
&MemoryMapSize,
NULL,
&MapKey,
&DescriptorSize,
&DescriptorVersion
);
// Expected: EFI_BUFFER_TOO_SMALL, MemoryMapSize now has the needed size
//
// Add extra space because AllocatePool will change the map
//
MemoryMapSize += 2 * DescriptorSize;
Status = gBS->AllocatePool (
EfiBootServicesData,
MemoryMapSize,
(VOID **)&MemoryMap
);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Second call: get the actual map
//
Status = gBS->GetMemoryMap (
&MemoryMapSize,
MemoryMap,
&MapKey,
&DescriptorSize,
&DescriptorVersion
);
if (EFI_ERROR (Status)) {
gBS->FreePool (MemoryMap);
return Status;
}
//
// Iterate entries -- note: use DescriptorSize, not sizeof()
//
EntryCount = MemoryMapSize / DescriptorSize;
Print (L"Memory Map: %u entries (descriptor size = %u)\n\n", EntryCount, DescriptorSize);
Print (L"%-20s %-18s %-12s %-10s\n", L"Type", L"PhysicalStart", L"Pages", L"Attribute");
Print (L"%-20s %-18s %-12s %-10s\n", L"----", L"-------------", L"-----", L"---------");
for (Index = 0; Index < EntryCount; Index++) {
EFI_MEMORY_DESCRIPTOR *Desc;
//
// CRITICAL: walk by DescriptorSize, not sizeof(EFI_MEMORY_DESCRIPTOR)
//
Desc = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)MemoryMap + Index * DescriptorSize);
Print (
L"%-20u 0x%016lx %-12lu 0x%08lx\n",
Desc->Type,
Desc->PhysicalStart,
Desc->NumberOfPages,
Desc->Attribute
);
}
gBS->FreePool (MemoryMap);
return EFI_SUCCESS;
}
Never use
sizeof(EFI_MEMORY_DESCRIPTOR)to iterate the memory map. The firmware may use a larger descriptor than the structure defined in headers. Always use theDescriptorSizereturned byGetMemoryMap(). This is one of the most common bugs in UEFI code.
The MapKey
The MapKey is a snapshot identifier for the memory map. It changes every time the map changes (e.g., after any allocation or free). The key is critical for ExitBootServices() – you must pass the exact MapKey that corresponds to the current state of the map.
sequenceDiagram
participant Loader as OS Loader
participant BS as Boot Services
Loader->>BS: GetMemoryMap() → MapKey=42
Note over Loader: Process the map...
Loader->>BS: AllocatePool() for something
Note over BS: MapKey is now 43!
Loader->>BS: ExitBootServices(MapKey=42)
BS-->>Loader: EFI_INVALID_PARAMETER!
Note over Loader: Must retry...
Loader->>BS: GetMemoryMap() → MapKey=43
Loader->>BS: ExitBootServices(MapKey=43)
BS-->>Loader: EFI_SUCCESS
11.6 Memory Attributes
Each memory descriptor includes an Attribute field with flags describing the cacheability and access permissions:
| Attribute | Value | Meaning |
|---|---|---|
EFI_MEMORY_UC |
0x0000000000000001 | Uncacheable |
EFI_MEMORY_WC |
0x0000000000000002 | Write-combining |
EFI_MEMORY_WT |
0x0000000000000004 | Write-through |
EFI_MEMORY_WB |
0x0000000000000008 | Write-back (normal RAM) |
EFI_MEMORY_UCE |
0x0000000000000010 | Uncacheable, exported |
EFI_MEMORY_WP |
0x0000000000001000 | Write-protected |
EFI_MEMORY_RP |
0x0000000000002000 | Read-protected |
EFI_MEMORY_XP |
0x0000000000004000 | Execute-protected (NX) |
EFI_MEMORY_NV |
0x0000000000008000 | Non-volatile (persistent) |
EFI_MEMORY_RUNTIME |
0x8000000000000000 | Needs virtual mapping at runtime |
The
EFI_MEMORY_RUNTIMEattribute is set on memory regions that Runtime Services code and data live in. The OS uses this flag to know which regions it must include inSetVirtualAddressMap().
11.7 Practical Allocation Patterns
Pattern 1: Allocating a structure
MY_PRIVATE_DATA *Private;
Private = AllocateZeroPool (sizeof (MY_PRIVATE_DATA));
if (Private == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Private->Signature = MY_PRIVATE_SIGNATURE;
// ... initialize other fields ...
// Later:
FreePool (Private);
Pattern 2: Allocating an array
UINTN Count = 64;
UINT32 *Array;
Array = AllocateZeroPool (Count * sizeof (UINT32));
if (Array == NULL) {
return EFI_OUT_OF_RESOURCES;
}
for (UINTN i = 0; i < Count; i++) {
Array[i] = (UINT32)i;
}
// Grow the array
UINT32 *NewArray = ReallocatePool (
Count * sizeof (UINT32),
Count * 2 * sizeof (UINT32),
Array
);
if (NewArray == NULL) {
FreePool (Array);
return EFI_OUT_OF_RESOURCES;
}
Array = NewArray;
Count *= 2;
Pattern 3: DMA buffer with physical address constraints
EFI_PHYSICAL_ADDRESS DmaBuffer;
UINTN Pages;
Pages = EFI_SIZE_TO_PAGES (DMA_BUFFER_SIZE);
//
// Some devices require buffers below 4 GiB
//
DmaBuffer = 0xFFFFFFFF;
Status = gBS->AllocatePages (
AllocateMaxAddress,
EfiBootServicesData,
Pages,
&DmaBuffer
);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Program the device with the physical address
//
DeviceRegs->DmaBaseAddress = (UINT32)DmaBuffer;
// Later in Stop():
gBS->FreePages (DmaBuffer, Pages);
Pattern 4: Runtime-persistent data
//
// This buffer survives ExitBootServices() and is available
// to Runtime Services code.
//
MY_RUNTIME_DATA *RtData;
RtData = AllocateRuntimeZeroPool (sizeof (MY_RUNTIME_DATA));
if (RtData == NULL) {
return EFI_OUT_OF_RESOURCES;
}
//
// After SetVirtualAddressMap(), physical addresses become invalid.
// You must convert pointers in your runtime driver's
// SetVirtualAddressMap event handler:
//
VOID
EFIAPI
VirtualAddressChangeEvent (
IN EFI_EVENT Event,
IN VOID *Context
)
{
gRT->ConvertPointer (0, (VOID **)&mRtData);
gRT->ConvertPointer (0, (VOID **)&mRtData->SomePointer);
}
11.8 Common Memory Bugs and Debugging
Bug 1: Using sizeof instead of DescriptorSize for memory map iteration
// BUG: Silent data corruption, reads wrong entries
for (i = 0; i < Count; i++) {
Desc = &MemoryMap[i]; // WRONG if DescriptorSize > sizeof(EFI_MEMORY_DESCRIPTOR)
}
// FIX:
for (i = 0; i < Count; i++) {
Desc = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)MemoryMap + i * DescriptorSize);
}
Bug 2: Pool allocation after ExitBootServices
// BUG: Boot Services are gone!
Status = gBS->AllocatePool (EfiRuntimeServicesData, 256, &Ptr);
// This will triple-fault or return garbage.
// FIX: Allocate all memory you need BEFORE ExitBootServices.
Bug 3: Not adding extra space when getting the memory map for ExitBootServices
// BUG: GetMemoryMap + AllocatePool changes the map, invalidating MapKey
gBS->GetMemoryMap (&Size, NULL, &Key, &DescSize, &DescVer);
gBS->AllocatePool (EfiLoaderData, Size, &Map);
gBS->GetMemoryMap (&Size, Map, &Key, &DescSize, &DescVer);
gBS->ExitBootServices (ImageHandle, Key); // May fail!
// FIX: Add extra space and retry loop
Size += 2 * DescSize;
gBS->AllocatePool (EfiLoaderData, Size, &Map);
gBS->GetMemoryMap (&Size, Map, &Key, &DescSize, &DescVer);
Status = gBS->ExitBootServices (ImageHandle, Key);
if (Status == EFI_INVALID_PARAMETER) {
// Map changed, get it again (no allocation this time -- reuse buffer)
gBS->GetMemoryMap (&Size, Map, &Key, &DescSize, &DescVer);
gBS->ExitBootServices (ImageHandle, Key);
}
Bug 4: Memory leak in error paths
// BUG: Memory leak if second allocation fails
Buffer1 = AllocatePool (Size1);
Buffer2 = AllocatePool (Size2);
if (Buffer2 == NULL) {
return EFI_OUT_OF_RESOURCES; // Buffer1 leaked!
}
// FIX: Clean up on all error paths
Buffer1 = AllocatePool (Size1);
if (Buffer1 == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Buffer2 = AllocatePool (Size2);
if (Buffer2 == NULL) {
FreePool (Buffer1);
return EFI_OUT_OF_RESOURCES;
}
Bug 5: Using freed memory
// BUG: Use-after-free
FreePool (Private);
Private->SomeField = 0; // Undefined behavior!
// FIX: Clear the pointer after freeing
FreePool (Private);
Private = NULL;
Bug 6: Forgetting ConvertPointer in runtime drivers
// BUG: Runtime driver accesses physical address after virtual mapping
VOID *RuntimeData; // Allocated with AllocateRuntimePool
// After OS calls SetVirtualAddressMap, this pointer is invalid.
// The driver crashes when a Runtime Service tries to use it.
// FIX: Register for the Virtual Address Change event
EFI_EVENT VaChangeEvent;
gBS->CreateEventEx (
EVT_NOTIFY_SIGNAL,
TPL_NOTIFY,
VirtualAddressChangeCallback,
NULL,
&gEfiEventVirtualAddressChangeGuid,
&VaChangeEvent
);
VOID EFIAPI
VirtualAddressChangeCallback (IN EFI_EVENT Event, IN VOID *Context)
{
gRT->ConvertPointer (0, &RuntimeData);
}
11.9 Memory Map Visualization
A typical QEMU Q35 memory map might look like this:
Type PhysicalStart Pages Size
---- ------------- ----- ----
EfiBootServicesCode 0x0000000000000000 1 4 KiB
EfiConventionalMemory 0x0000000000001000 159 636 KiB
EfiConventionalMemory 0x0000000000100000 1792 7 MiB
EfiBootServicesData 0x0000000000800000 256 1 MiB
EfiConventionalMemory 0x0000000000900000 30464 119 MiB
EfiBootServicesData 0x0000000007F00000 128 512 KiB
EfiBootServicesCode 0x0000000007F80000 64 256 KiB
EfiRuntimeServicesData 0x0000000007FC0000 32 128 KiB
EfiRuntimeServicesCode 0x0000000007FE0000 32 128 KiB
EfiReservedMemoryType 0x00000000FEC00000 1 4 KiB (IOAPIC)
EfiReservedMemoryType 0x00000000FEE00000 1 4 KiB (LAPIC)
EfiMemoryMappedIO 0x00000000FFE00000 512 2 MiB (Firmware Flash)
After
ExitBootServices(), the OS reclaims allEfiBootServicesCodeandEfiBootServicesDataregions as free RAM. OnlyEfiRuntimeServices*,EfiACPINvs,EfiReservedMemoryType, andEfiPersistentMemoryregions must be preserved.
11.10 Debugging Memory Issues
Using the UEFI Shell
Shell> memmap # Display the current memory map
Shell> mem 0x7FC0000 # Dump raw memory at an address
Shell> dmem 0x7FC0000 100 # Dump 0x100 bytes
Using DEBUG macros
DEBUG ((DEBUG_INFO, "Allocated %u bytes at %p\n", Size, Buffer));
DEBUG ((DEBUG_INFO, "Memory map has %u entries\n", MapSize / DescSize));
Using the heap guard feature (Project Mu)
Project Mu and EDK II support heap guard features that can detect buffer overflows and use-after-free bugs:
# In your DSC file:
[PcdsFixedAtBuild]
gEfiMdeModulePkgTokenSpaceGuid.PcdHeapGuardPropertyMask|0x0F
gEfiMdeModulePkgTokenSpaceGuid.PcdHeapGuardPageType|0x7FF4
gEfiMdeModulePkgTokenSpaceGuid.PcdHeapGuardPoolType|0x7FF4
These PCDs enable guard pages around allocations. When code writes past the end of a buffer or accesses freed memory, the CPU triggers a page fault, making the bug immediately visible.
Complete source code: The full working example for this chapter is available at
examples/UefiMuGuidePkg/MemoryExample/.
Summary
UEFI memory services provide two allocation interfaces – pool (variable-size, like malloc) and page (page-aligned, fixed granularity) – each tagged with a memory type that determines the lifetime of the allocation. The memory map is the authoritative description of physical memory and is critical for the ExitBootServices transition. Understanding memory types, the two-call GetMemoryMap pattern, and the DescriptorSize iteration rule will prevent the majority of memory-related bugs in your firmware code.
In the next chapter, we will explore Boot Services and Runtime Services in full – the two service tables that define everything UEFI code can do.
Hands-on exercise: Write a UEFI application that retrieves the memory map and calculates summary statistics: total usable RAM, total runtime-reserved RAM, largest contiguous free region, and number of MMIO regions. Print the results in a formatted table. Test it in QEMU with different memory sizes by passing
-m 256Mvs-m 2Gto see how the map changes.