Memory Map of UEFI and Cleanup
In the previous chapter, we successfully loaded the Ymir Kernel into memory, so naturally, we want to transfer control to Ymir right away... However, in this chapter, we won't run Ymir yet. Before that, we'll perform some cleanup tasks that can only be done while still in the UEFI environment. Additionally, we'll get and inspect the memory map provided by UEFI. This memory map will later be used by Ymir and is also necessary for properly exiting Boot Services.
important
Source code for this chapter is in whiz-surtr-cleanup_memmap
branch.
Table of Contents
Cleanup of Open Files
We used UEFI's Simple File System Protocol to read and load Ymir image. You might recall that we opened the root directory, then the Ymir ELF file, and read its header into memory. It's good practice to clean up what we've used. Let's do cleanup now.
We've read ELF file twice:
- To parse the ELF header
- To load kernel segments
Among these, we need to keep the 2 that's required to run Ymir. So we only clean up the buffer for the ELF header. Since the buffer was allocated with AllocatePool(), we release it using the corresponding FreePool() API:
status = boot_service.freePool(header_buffer);
if (status != .Success) {
log.err("Failed to free memory for kernel ELF header.", .{});
return status;
}
Next, close the Ymir ELF file:
status = kernel.close();
if (status != .Success) {
log.err("Failed to close kernel file.", .{});
return status;
}
Now that there're no open files, let's close the root directory:
status = root_dir.close();
if (status != .Success) {
log.err("Failed to close filesystem volume.", .{});
return status;
}
Obtaining Memory Map
Although this isn't the part of cleanup, let's retrieve the memory map provided by UEFI. This memory map gives detailed information about all the memory currently in use, including the areas that Surtr allocated for the kernel by AllocatePages()
and AllocatePool()
. We'll pass this memory map to Ymir later, where it will be used to build its memory allocator.
Definition of Memory Map
The memory map provided by UEFI is just raw binary data, and parsing it requires several additional pieces of information. To handle this, we define a struct that consolidates all the necessary data for parsing.
Create a new file named surtr/defs.zig
. This file will be used to define data structures shared between Ymir and Surtr. Define the memory map struct as follows:
pub const MemoryMap = extern struct {
/// Total buffer size prepared to store the memory map.
buffer_size: usize,
/// Memory descriptors.
descriptors: [*]uefi.tables.MemoryDescriptor,
/// Total memory map size.
map_size: usize,
/// Map key used to check if the memory map has been changed.
map_key: usize,
/// Size in bytes of each memory descriptor.
descriptor_size: usize,
/// UEFI memory descriptor version.
descriptor_version: u32,
};
The core component of the memory map is an array of MemoryDescriptor
structs. Each descriptor represents a single contiguous block of memory. The size of this array is map_size / descriptor_size
.
Iterator for MemoryDescriptor
Just like iterating over ELF program headers, it would be convenient to easily iterate over MemoryDescriptor
entries as well. Based on the information in MemoryMap
, we define a struct to iterate through the MemoryDescriptor
entries:
pub const MemoryDescriptorIterator = struct {
const Self = @This();
const Md = uefi.tables.MemoryDescriptor;
descriptors: [*]Md,
current: *Md,
descriptor_size: usize,
total_size: usize,
pub fn new(map: MemoryMap) Self {
return Self {
.descriptors = map.descriptors,
.current = @ptrCast(map.descriptors),
.descriptor_size = map.descriptor_size,
.total_size = map.map_size,
};
}
pub fn next(self: *Self) ?*Md {
if (@intFromPtr(self.current) >= @intFromPtr(self.descriptors) + self.total_size) {
return null;
}
const md = self.current;
self.current = @ptrFromInt(@intFromPtr(self.current) + self.descriptor_size);
return md;
}
};
In new()
, we store the three elements necessary for iteration: descriptors
, descriptor_size
, and total_size
. We also set the pointer current
, which points to the current MemoryDescriptor
, to the start of the array.
The next()
method returns the current MemoryDescriptor
and advances the iterator to the next element. As mentioned earlier, MemoryDescriptor
s are stored in a contiguous array, with each element sized descriptor_size
. Therefore, adding descriptor_size
to current
moves the pointer to the next element. If there is no next element, it returns null
.
Obtaining and Printing Memory Map
Add the helper function to obtain the memory map in surtr/boot.zig
:
const map_buffer_size = page_size * 4;
var map_buffer: [map_buffer_size]u8 = undefined;
var map = defs.MemoryMap{
.buffer_size = map_buffer.len,
.descriptors = @alignCast(@ptrCast(&map_buffer)),
.map_key = 0,
.map_size = map_buffer.len,
.descriptor_size = 0,
.descriptor_version = 0,
};
status = getMemoryMap(&map, boot_service);
We'll implement the function getMemoryMap()
shortly. The memory map obtained here is essentially a copy of the actual memory map. Therefore, a buffer is required to hold this copied map. The required buffer size depends on the number of memory blocks in use, but for simplicity, we'll allocate a fixed buffer size equivalent to 4 pages1. This should be sufficient in most cases. The getMemoryMap()
function takes a MemoryMap
struct as an argument and fills it with the retrieved information. When passing the map as an argument, you only need to specify the address and size of the pre-allocated buffer.
Next , let's implement a function that obtains the memory map:
fn getMemoryMap(map: *defs.MemoryMap, boot_services: *uefi.tables.BootServices) uefi.Status {
return boot_services.getMemoryMap(
&map.map_size,
map.descriptors,
&map.map_key,
&map.descriptor_size,
&map.descriptor_version,
);
}
This is a simple wrapper around UEFI Runtime Services' GetMemoryMap()
, which retrieves the memory map.
Finally, print the obtained memory map:
var map_iter = defs.MemoryDescriptorIterator.new(map);
while (true) {
if (map_iter.next()) |md| {
log.debug(" 0x{X:0>16} - 0x{X:0>16} : {s}", .{
md.physical_start,
md.physical_start + md.number_of_pages * page_size,
@tagName(md.type),
});
} else break;
}
When executed, the memory map is displayed as shown below. For the meaning of each memory type, please refer to this table:
Example UEFI Memory Map
[DEBUG] (surtr): Memory Map (Physical): Buf=0x1FE91FA0, MapSize=0x1770, DescSize=0x30
[DEBUG] (surtr): 0x0000000000000000 - 0x0000000000001000 : BootServicesCode
[DEBUG] (surtr): 0x0000000000001000 - 0x00000000000A0000 : ConventionalMemory
[DEBUG] (surtr): 0x0000000000100000 - 0x0000000000101000 : LoaderData
[DEBUG] (surtr): 0x0000000000101000 - 0x0000000000800000 : ConventionalMemory
[DEBUG] (surtr): 0x0000000000800000 - 0x0000000000808000 : ACPIMemoryNVS
[DEBUG] (surtr): 0x0000000000808000 - 0x000000000080B000 : ConventionalMemory
[DEBUG] (surtr): 0x000000000080B000 - 0x000000000080C000 : ACPIMemoryNVS
[DEBUG] (surtr): 0x000000000080C000 - 0x0000000000810000 : ConventionalMemory
[DEBUG] (surtr): 0x0000000000810000 - 0x0000000000900000 : ACPIMemoryNVS
[DEBUG] (surtr): 0x0000000000900000 - 0x0000000001780000 : BootServicesData
[DEBUG] (surtr): 0x0000000001780000 - 0x000000001BEF7000 : ConventionalMemory
[DEBUG] (surtr): 0x000000001BEF7000 - 0x000000001BF17000 : BootServicesData
[DEBUG] (surtr): 0x000000001BF17000 - 0x000000001E256000 : ConventionalMemory
[DEBUG] (surtr): 0x000000001E256000 - 0x000000001E25F000 : LoaderCode
[DEBUG] (surtr): 0x000000001E25F000 - 0x000000001E265000 : ConventionalMemory
[DEBUG] (surtr): 0x000000001E265000 - 0x000000001E4DA000 : BootServicesData
[DEBUG] (surtr): 0x000000001E4DA000 - 0x000000001E4DB000 : ConventionalMemory
[DEBUG] (surtr): 0x000000001E4DB000 - 0x000000001E989000 : BootServicesData
[DEBUG] (surtr): 0x000000001E989000 - 0x000000001EA3D000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EA3D000 - 0x000000001EA6D000 : BootServicesData
[DEBUG] (surtr): 0x000000001EA6D000 - 0x000000001EB4E000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EB4E000 - 0x000000001EBBA000 : BootServicesData
[DEBUG] (surtr): 0x000000001EBBA000 - 0x000000001EBC3000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EBC3000 - 0x000000001EBC8000 : BootServicesData
[DEBUG] (surtr): 0x000000001EBC8000 - 0x000000001EC07000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC07000 - 0x000000001EC0A000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC0A000 - 0x000000001EC0D000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC0D000 - 0x000000001EC14000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC14000 - 0x000000001EC26000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC26000 - 0x000000001EC27000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC27000 - 0x000000001EC2A000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC2A000 - 0x000000001EC2F000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC2F000 - 0x000000001EC3D000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC3D000 - 0x000000001EC4C000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC4C000 - 0x000000001EC57000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC57000 - 0x000000001EC62000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC62000 - 0x000000001EC78000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EC78000 - 0x000000001EC81000 : BootServicesData
[DEBUG] (surtr): 0x000000001EC81000 - 0x000000001ECA5000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ECA5000 - 0x000000001ECA8000 : BootServicesData
[DEBUG] (surtr): 0x000000001ECA8000 - 0x000000001ECBC000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ECBC000 - 0x000000001ECC3000 : BootServicesData
[DEBUG] (surtr): 0x000000001ECC3000 - 0x000000001ECC9000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ECC9000 - 0x000000001ECCC000 : BootServicesData
[DEBUG] (surtr): 0x000000001ECCC000 - 0x000000001ECDA000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ECDA000 - 0x000000001ECDB000 : BootServicesData
[DEBUG] (surtr): 0x000000001ECDB000 - 0x000000001ECE6000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ECE6000 - 0x000000001ECE8000 : BootServicesData
[DEBUG] (surtr): 0x000000001ECE8000 - 0x000000001ECF5000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ECF5000 - 0x000000001ECFA000 : BootServicesData
[DEBUG] (surtr): 0x000000001ECFA000 - 0x000000001ED07000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED07000 - 0x000000001ED0A000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED0A000 - 0x000000001ED16000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED16000 - 0x000000001ED17000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED17000 - 0x000000001ED1A000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED1A000 - 0x000000001ED1C000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED1C000 - 0x000000001ED29000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED29000 - 0x000000001ED2C000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED2C000 - 0x000000001ED2D000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED2D000 - 0x000000001ED30000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED30000 - 0x000000001ED39000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED39000 - 0x000000001ED3A000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED3A000 - 0x000000001ED3C000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED3C000 - 0x000000001ED3E000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED3E000 - 0x000000001ED4D000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED4D000 - 0x000000001ED4F000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED4F000 - 0x000000001ED6C000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED6C000 - 0x000000001ED6D000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED6D000 - 0x000000001ED70000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED70000 - 0x000000001ED73000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED73000 - 0x000000001ED78000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED78000 - 0x000000001ED7B000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED7B000 - 0x000000001ED90000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED90000 - 0x000000001ED92000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED92000 - 0x000000001ED94000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED94000 - 0x000000001ED97000 : BootServicesData
[DEBUG] (surtr): 0x000000001ED97000 - 0x000000001ED9E000 : BootServicesCode
[DEBUG] (surtr): 0x000000001ED9E000 - 0x000000001EDA5000 : BootServicesData
[DEBUG] (surtr): 0x000000001EDA5000 - 0x000000001EDA9000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EDA9000 - 0x000000001EDAD000 : BootServicesData
[DEBUG] (surtr): 0x000000001EDAD000 - 0x000000001EDCC000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EDCC000 - 0x000000001EDCE000 : BootServicesData
[DEBUG] (surtr): 0x000000001EDCE000 - 0x000000001EDD9000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EDD9000 - 0x000000001EDDE000 : BootServicesData
[DEBUG] (surtr): 0x000000001EDDE000 - 0x000000001EDEF000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EDEF000 - 0x000000001EDF0000 : BootServicesData
[DEBUG] (surtr): 0x000000001EDF0000 - 0x000000001EDF8000 : BootServicesCode
[DEBUG] (surtr): 0x000000001EDF8000 - 0x000000001F000000 : BootServicesData
[DEBUG] (surtr): 0x000000001F000000 - 0x000000001F00B000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F00B000 - 0x000000001F010000 : BootServicesData
[DEBUG] (surtr): 0x000000001F010000 - 0x000000001F0D1000 : RuntimeServicesData
[DEBUG] (surtr): 0x000000001F0D1000 - 0x000000001F0EA000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F0EA000 - 0x000000001F0ED000 : BootServicesData
[DEBUG] (surtr): 0x000000001F0ED000 - 0x000000001F0F6000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F0F6000 - 0x000000001F0F8000 : BootServicesData
[DEBUG] (surtr): 0x000000001F0F8000 - 0x000000001F0F9000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F0F9000 - 0x000000001F0FB000 : BootServicesData
[DEBUG] (surtr): 0x000000001F0FB000 - 0x000000001F0FF000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F0FF000 - 0x000000001F101000 : BootServicesData
[DEBUG] (surtr): 0x000000001F101000 - 0x000000001F117000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F117000 - 0x000000001F118000 : BootServicesData
[DEBUG] (surtr): 0x000000001F118000 - 0x000000001F11A000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F11A000 - 0x000000001F12D000 : BootServicesData
[DEBUG] (surtr): 0x000000001F12D000 - 0x000000001F12F000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F12F000 - 0x000000001F52F000 : BootServicesData
[DEBUG] (surtr): 0x000000001F52F000 - 0x000000001F537000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F537000 - 0x000000001F53D000 : BootServicesData
[DEBUG] (surtr): 0x000000001F53D000 - 0x000000001F547000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F547000 - 0x000000001F548000 : BootServicesData
[DEBUG] (surtr): 0x000000001F548000 - 0x000000001F54D000 : BootServicesCode
[DEBUG] (surtr): 0x000000001F54D000 - 0x000000001F8ED000 : BootServicesData
[DEBUG] (surtr): 0x000000001F8ED000 - 0x000000001F9ED000 : RuntimeServicesData
[DEBUG] (surtr): 0x000000001F9ED000 - 0x000000001FAED000 : RuntimeServicesCode
[DEBUG] (surtr): 0x000000001FAED000 - 0x000000001FB6D000 : ReservedMemoryType
[DEBUG] (surtr): 0x000000001FB6D000 - 0x000000001FB7F000 : ACPIReclaimMemory
[DEBUG] (surtr): 0x000000001FB7F000 - 0x000000001FBFF000 : ACPIMemoryNVS
[DEBUG] (surtr): 0x000000001FBFF000 - 0x000000001FE00000 : BootServicesData
[DEBUG] (surtr): 0x000000001FE00000 - 0x000000001FE77000 : ConventionalMemory
[DEBUG] (surtr): 0x000000001FE77000 - 0x000000001FE97000 : BootServicesData
[DEBUG] (surtr): 0x000000001FE97000 - 0x000000001FECA000 : BootServicesCode
[DEBUG] (surtr): 0x000000001FECA000 - 0x000000001FEDB000 : BootServicesData
[DEBUG] (surtr): 0x000000001FEDB000 - 0x000000001FEF4000 : BootServicesCode
[DEBUG] (surtr): 0x000000001FEF4000 - 0x000000001FF78000 : RuntimeServicesData
[DEBUG] (surtr): 0x000000001FF78000 - 0x0000000020000000 : ACPIMemoryNVS
[DEBUG] (surtr): 0x00000000FEFFC000 - 0x00000000FF000000 : ReservedMemoryType
This completes the process of obtaining the memory map. Take a break with a cup of tea and take a look at the displayed memory map. Try checking what memory type the area allocated by AllocatePages()
has. Reflecting on your life so far might also be a good idea.
Exit Boot Services
As the final cleanup step before jumping to the kernel, we will exit the UEFI Boot Services. This is done by calling the Boot Services function ExitBootServices()
. After calling this function, Boot Services will no longer be available, but the Runtime Services will become accessible.
To call ExitBootServices()
, the UEFI OS loader (Surtr in this series) must guarantee that it has an up-to-date view of the current memory map. In other words, you need to tell UEFI, "I know everything." To do this, the function requires the key of the memory map that was obtained earlier:
log.info("Exiting boot services.", .{});
status = boot_service.exitBootServices(uefi.handle, map.map_key);
However, the memory map can change due to calls like AllocatePages()
or AllocatePool()
. If UEFI determines that the memory map (or its key) you provided is outdated, ExitBootServices()
will return an error. In that case, you need to retrieve the memory map again and assert that you have the latest memory map2:
if (status != .Success) {
map.buffer_size = map_buffer.len;
map.map_size = map_buffer.len;
status = getMemoryMap(&map, boot_service);
if (status != .Success) {
log.err("Failed to get memory map after failed to exit boot services.", .{});
return status;
}
status = boot_service.exitBootServices(uefi.handle, map.map_key);
if (status != .Success) {
log.err("Failed to exit boot services.", .{});
return status;
}
}
With this, you have successfully exited UEFI Boot Services. From now on, you must face this cold world on your own, like a true adult.
From this point onward, Boot Services are no longer available. The logging system we've been using relies on the Simple Text Output Protocol provided by Boot Services. Therefore, log output will no longer work. Try calling something like log.warn()
to output a message - you will most likely encounter an error. You won’t even be able to print anything anymore. Sometimes the system goes on the blink, and the whole thing it turns out wrong...3
If you want to implement this properly, one approach is to start with a reasonably small buffer, call getMemoryMap()
with it, and then allocate a larger buffer and retry if it returns an error.
In practice, Surtr doesn't perform any operations that would modify the memory map after retrieving it, so this if
block should never be reached.