Demilade Sonuga's blog
All postsHeap Memory
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