Demilade Sonuga's blog

All posts

Heap Memory

2023-03-15 · 12 min read

The first stop on our journey to event handling is creating a heap. But first, let's stop and ask ourselves: what exactly is a heap?

The Stack And The Heap

When we talk about memory, the things we often hear are the stack and the heap. The stack is that first-in first-out memory segment where local variables are stored while the heap is that mysterious memory pile that we can use to store stuff whose size we don't know until runtime.

While this view of the stack and the heap is useful as a memory model when you're writing normal code, it can no longer hold up when we're banging out code for levels this low.

As it turns out, there isn't actually a stack (strictly speaking). The "stack" is just normal memory that is typically used for local variables.

The heap, too, is just a chunk of normal memory. But the thing about the heap is that it is provided and managed by the OS. Special functions are used to request memory from it (allocation) and special functions are used to return memory (deallocation).

So, that's all it takes to create a heap: get a chunk of memory and declare it heap memory.

Creating A Heap

To get this chunk of memory, there are a few things we could do but we'll do the easiest. Throughout this project, we've been able to avoid having to deal with memory management because the UEFI firmware handled most of that for us. And now we're heading back to the firmware for this, too.

Looking through our model of the UEFI services, you'll see that the BootServices already has a function that can be used for this purpose: alloc_pool.

#[repr(C)]
pub struct BootServices {
// ...Others
alloc_pool: extern "efiapi" fn(
pool_type: MemType,
size: usize,
buffer: *mut *mut u8
) -> Status,
// ...Others
}

According to the spec, the pool_type is the memory type (defined also by the spec). The size is the number of bytes to allocate. And the buffer will end up holding a pointer to a valid memory location if the allocation is successful.

Before exiting the Boot Services and gaining control of the full system, we can use this function to allocate memory that we can then use for a heap.

We can just use this function directly in main.rs but it would be better if we create a safe Rusty wrapper for it and then use that wrapper instead.

In uefi.rs

impl BootServices {
// Allocate a chunk of memory
pub fn alloc_pool(&self, mem_type: MemType, size: usize) {

}
}

Our function takes two arguments: the memory type and the size in bytes which will be passed into the actual function we're wrapping. The buffer to hold the result can simply be allocated inside the function.

The function's result is the address of the allocated memory. And since there is a possibility that the function could fail, we return a Result with a pointer.

impl BootServices {
// DELETED: pub fn alloc_pool(mem_type: MemType, size: usize) {
pub fn alloc_pool(&self, mem_type: MemType, size: usize) -> Result<*mut u8, Status> {

}
}

If the function succeeds, the allocated memory's address is returned. If the function fails, the status failure it failed with is returned.

The function body itself is quite straightforward. It's just a matter of creating a variable whose purpose is to hold the allocated memory's address. Calling the actual alloc_pool with the address of that variable and the function arguments. Then checking if it's a success. If it's a success, return the memory address in an Ok, else return the error status in an Err.

impl BootServices {
pub fn alloc_pool(&self, mem_type: MemType, size: usize) -> Result<*mut u8, Status> {
let mut buffer: *mut u8 = core::ptr::null_mut();
let status = (self.alloc_pool)(
mem_type,
size,
&mut buffer as *mut _
);
if status == STATUS_SUCCESS {
Ok(buffer)
} else {
Err(status)
}
}
}

Now, it's just a matter of calling it in efi_main, before exiting boot services.

In main.rs

// ...Others

mod uefi;
// DELETED: use uefi::{SystemTable, Screen, PixelFormat, Pixel, pre_graphics_print_str, print_str, printint, NO_OF_PIXELS_IN_A_ROW, BootServices};
use uefi::{SystemTable, Screen, PixelFormat, Pixel, pre_graphics_print_str, print_str, printint, NO_OF_PIXELS_IN_A_ROW, BootServices, MemType}; // NEW

// ...Others

// A heap size of 1 megabyte
const HEAP_SIZE: usize = 2usize.pow(20);

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others
init_screen(screen);

let screen = get_screen().unwrap();

// NEW:
// Allocating memory for the heap
let heap_mem = boot_services.alloc_pool(MemType::EfiLoaderData, HEAP_SIZE);
if heap_mem.is_err() {
panic!("Heap allocation failed with error status {}", heap_mem.unwrap_err());
}

boot_services.exit_boot_services(handle).unwrap();

let cs = setup_gdt();
// ...Others
}

For the MemType, we use the EfiLoaderData because according to the spec, it is the type for data portions for the data portions of a loaded UEFI app. And the heap is purely for data.

As for the heap size, we're using 1 Mib for now. We'll increase it if we need to later.

For this code to work, we have to make the MemType enum in uefi.rs public:

#[repr(u32)]
// DELETED: enum MemType { /* ...Others */ }
pub enum MemType { /* ...Others */ } // NEW

And that's it: we have our heap memory.

Take Away

  • Both stack and heap memory are just normal memory that's used in different ways.

For the full code, go to the repo

In The Next Post

We'll get on to the next step: allocators.

References

  • Boot Services AllocatePool: UEFI spec v2.7, section 7.2
  • Memory types: UEFI spec v2.7, section 7.2, table 25

You can download the UEFI spec here