Demilade Sonuga's blog

All posts

Getting Ready For Event Handling

2023-01-14 · 44 min read

We have the initial game setting on the scene now, but it's still just a dumb picture. For our game to become a real game, it must be able to animate believably when we press certain keys. When the right arrow key on the keyboard is pressed, the paddle should move to the right. When the left arrow is pressed, it should move to the left. Once the game has started, the ball should keep bouncing around the screen until it leaves the screen (in the case of a lost game) or all blocks have been broken (in the case of the game won).

For our game to become responsive to key presses, we first have to know when a key has been pressed. But how can we know when a key has been pressed?

The answer to the question lies in the interrupt system. This interrupt system is the primary mechanism used by the computer system to detect when some asynchronous event, such as pressing a key on a keyboard, occurs. When a key on the keyboard is pressed, an electric signal called an interrupt is sent to the CPU. When this signal is received by the processor, the processor stops whatever it's currently doing and does whatever we tell it to do upon a key press. The code that says what to do during a specific interrupt is an interrupt service routine.

The thing about interrupts is that before we can use them, we have a fair bit of setting up to do. For one thing, they first have to be enabled.

Before we can dive into this interrupt stuff, there are a few things we need to clear up with our UEFI firmware.

According to the UEFI spec, when the firmware loads the application, there are a few things that are put into place by the firmware. Among those things, interrupts are already enabled. But the problem here is, no interrupt services are supported other than some things Boot Services has to offer.

To handle interrupts the way we need to, we need to gain full control of the computer from the UEFI firmware. To do this, Boot Services offers a function, ExitBootServices.

The role of this function is to terminate the Boot Services. When this function is executed successfully, our code will have full control of the system and the Boot Services will no longer be available for use.

In the spec, the function is described as:

typedef
EFI_STATUS
(EFIAPI *EFI_EXIT_BOOT_SERVICES) (
    IN EFI_HANDLE ImageHandle,
    IN UINTN MapKey
);

EFI_HANDLE is just a void pointer, represented as *const core::ffi::c_void in Rust code and UINTN is just usize in Rust.

According to the spec, ImageHandle is the handle that identifies the image that is going to exit. Take a look at efi_main's signature:

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

The first argument to this function, handle, is what we're talking about when we say "handle that identifies the image that is going to exit".

As for MapKey, the spec defines this as the key to the latest memory map.

A memory map is a structure that tells what chunks of memory we have and what they're being used for in the system. This is an important thing to have at this low level of operation in the computer system.

Before leaving the Boot Services, the UEFI system forces us to get the latest memory map. This is because the only way to get MapKey is by getting the latest memory map.

To get the memory map, there is another Boot Services function we must call: GetMemoryMap.

According to the spec:

typedef
EFI_STATUS
(EFIAPI *EFI_GET_MEMORY_MAP) (
    IN OUT UINTN *MemoryMapSize,
    IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap,
    OUT UINTN *MapKey,
    OUT UINTN *DescriptorSize,
    OUT UINT32 *DescriptorVersion
);

Now, we have to understand the operation of this function and the GetMemoryMap and use them to exit the Boot Services before we can get on with event handling.

The first argument, MemoryMapSize, is a pointer to a usize. Upon a call to the function, this argument must be pointing to the size of the buffer MemoryMap. Upon execution of this function, the usize pointed to by MemoryMapSize will be the size of the buffer given by the firmware if the buffer we gave was big enough. Otherwise, if our buffer isn't big enough, it will be the size of the buffer needed to contain the map.

The second argument is a pointer to EFI_MEMORY_DESCRIPTOR. This is the pointer to the buffer for the memory map itself. A single EFI_MEMORY_DESCRIPTOR describes a single chunk of memory. It is defined as:

typedef struct {
    UINT32 Type;
    EFI_PHYSICAL_ADDRESS PhysicalStart;
    EFI_VIRTUAL_ADDRESS VirtualStart;
    UINT64 NumberOfPages;
    UINT64 Attribute;
} EFI_MEMORY_DESCRIPTOR;

I don't think the exact meaning of these fields is really necessary for this project, so we won't dive into them. We just want to get the map and get on with the game.

The third argument, MapKey, is a pointer to a usize. The value behind this pointer will be the map key of the memory map we just got.

The fourth argument, DescriptorSize, is a pointer to a usize that will contain the size in bytes of a single EFI_MEMORY_DESCRIPTOR.

The fifth argument, DescriptorVersion is a pointer to a u32 that will contain the version of the EFI_MEMORY_DESCRIPTOR being used by the firmware. Again, I don't think this is something we need to worry about.

The last piece of information we need now is the positions of the GetMemoryMap and ExitBootServices functions in the Boot Services table.

From the spec, the GetMemoryMap function is at an offset of 56 bytes and the ExitBootServices is at an offset of 232 from the starting address of the Boot Services.

The information we have now is enough to get the latest map key and exit Boot Services. Before moving on, think of a way to implement this in Rust yourself.

To get this down in our Rust code, we have to start by modifying our BootServices struct to recognize the existence of these functions.

In uefi.rs, our BootServices currently looks like this:

#[repr(C)]
pub struct BootServices {
unneeded: [u8; 320],
locate_protocol: extern "efiapi" fn(
protocol: *const Guid,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void,
) -> Status,
}

Somewhere in those unneeded bytes are the function pointers we're looking for.

Firstly, GetMemoryMap is at an offset of 56:

#[repr(C)]
pub struct BootServices {
unneeded1: [u8; 56], // NEW
// NEW:
// Retrieves the current memory map
get_mem_map: extern "efiapi" fn(
mem_map_size: *mut usize,
mem_map: *mut MemDescriptor,
map_key: *mut usize,
descriptor_size: *mut usize,
descriptor_version: *mut u32
) -> Status,
// DELETED: unneeded: [u8; 320],
locate_protocol: extern "efiapi" fn(
protocol: *const Guid,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void,
) -> Status,
}

The ExitBootServices is at an offset of 232. The amount of bytes in between the starting point of the BootServices and the exit_boot_services has to be 232. We already have an unneeded field of 56 bytes. The get_mem_map function pointer is 8 bytes because pointers on x86_64 are 8 bytes in size. That leaves an unneeded field of 232 - (56 + 8) bytes == 168 bytes.

#[repr(C)]
pub struct BootServices {
unneeded1: [u8; 56],
get_mem_map: extern "efiapi" fn(
mem_map_size: *mut usize,
mem_map: *mut MemDescriptor,
map_key: *mut usize,
descriptor_size: *mut usize,
descriptor_version: *mut u32
) -> Status,
// NEW:
unneeded2: [u8; 168],
// Terminates the Boot Services and leaves the code with full control
exit_boot_services: extern "efiapi" fn(
image_handle: *const core::ffi::c_void,
map_key: usize
) -> Status,
locate_protocol: extern "efiapi" fn(
protocol: *const Guid,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void,
) -> Status,
}

locate_protocol is supposed to be at an offset of 320 bytes from the start of BootServices. 320 - (56 + 8 + 168 + 8) == 80 bytes. So, that's an unneeded 80 bytes in between exit_boot_services and locate_protocol.

#[repr(C)]
pub struct BootServices {
unneeded1: [u8; 56],
get_mem_map: extern "efiapi" fn(
mem_map_size: *mut usize,
mem_map: *mut MemDescriptor,
map_key: *mut usize,
descriptor_size: *mut usize,
descriptor_version: *mut u32
) -> Status,
unneeded2: [u8; 168],
exit_boot_services: extern "efiapi" fn(
image_handle: *mut core::ffi::c_void,
map_key: usize
) -> Status,
unneeded: [u8; 80], // NEW
locate_protocol: extern "efiapi" fn(
protocol: *const Guid,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void,
) -> Status,
}

Before we go on to safely wrap these functions as we did with the other UEFI functions, let's first see how exactly we want them to be used:

In main.rs

#[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:
// Exiting Boot Services to gain full control over the system
boot_services.exit_boot_services();

game::blasterball(screen);

0
}

Now, let's work backward from here. We need to be able to exit the Boot Services completely with a safe exit_boot_services function defined for BootServices.

impl BootServices {
pub fn locate_protocol(&self, protocol_guid: &Guid) -> Result<*mut core::ffi::c_void, usize> {
// ... Others
}

pub fn locate_gop(&self) -> Result<&GraphicsOutput, Status> {
// ... Others
}

// NEW:
// Terminates the Boot Services
pub fn exit_boot_services(&self) -> Result<(), Status> {

}
}

To call the actual exit_boot_services, we need to first get the latest map key. To get the latest map key, we have to get the latest memory map. To get the latest memory map, we first need to know how big it is so that we know how much memory to allocate for the map.

Think of how to implement this exit_boot_services before continuing.

The memory map is a list of EFI_MEMORY_DESCRIPTORs. We need a representation of this in code:

// A description of a single region of memory
#[repr(C)]
struct MemDescriptor {
_type: u32,
phys_start: u64,
virt_start: u64,
no_of_pages: u64,
attrribute: u64
}

In the spec, EFI_PHYSICAL_ADDRESS and EFI_VIRTUAL_ADDRESS are defined as UINT64, 64-bit unsigned integers.

The algorithm we'll use to proceed should look something like this:

  1. Get the required size of the memory map.
  2. Allocate memory to hold the memory map.
  3. Get the memory map.
  4. Exit the Boot Services with the map key.

To carry out step 1, we have to remember that upon a call to get_mem_map, the mem_map_size pointer will be pointing to the size of the memory map, if the size we put there isn't big enough.

The problem that comes next: how do we know when the function terminates with this error?

When we started talking about UEFI statuses, we only mentioned that 0 is the success status. But there are many other error statuses, including the EFI_BUFFER_TOO_SMALL status code. This status code is returned whenever a buffer too small error occurs.

In our situation, if we pass a pointer to a size that is too small to hold the memory map, the buffer too small status will be returned and the value behind that pointer will be modified by the firmware to contain the size of the memory needed to hold the map.

Breaking this down into more readable steps:

  1. Call get_mem_map with a very small mem_map_size, maybe 0, so that the firmware will give us the required size of the memory map.
  2. Verify that the status returned is the buffer too small status.
  3. Retrieve the required memory map size.

Before we translate this to Rust, there's one more thing we need to know. All UEFI error statuses have the leftmost bit set. So if the spec says the error code for some error is 4, what they actually mean is that it's (1 << 63) | 4. The (1 << 63) | x sets the 64th bit of the number x. It's 64 in our case because x86_64 is a 64-bit system, so usize is 64 bits.

According to the UEFI spec, the value for the EFI_BUFFER_TOO_SMALL code is 5. So, the full error code that will be returned will be (1 << 63) | 5.

In uefi.rs:

// ... Others

pub const STATUS_SUCCESS: Status = 0;
// This bit is always set in the status code when a UEFI function
// returns an error status code
const ERROR_BIT: Status = 1 << 63;
// The status that is returned when a buffer-too-small error occurs
// during the execution of a UEFI function
pub const STATUS_BUFFER_TOO_SMALL: Status = ERROR_BIT | 5;

// ... Others

pub fn exit_boot_services(&self) -> Result<(), Status> {
// Getting the required size of the memory map

// Setting mem_map_size to 0 so that the firmware will place the
// required size to hold the map on a call to get_mem_map
let mut mem_map_size: usize = 0;
// Setting this to the null pointer because we don't know
// how much memory to allocate yet
let mut mem_map: *mut MemDescriptor = core::ptr::null_mut();
let mut map_key: usize = 0;
let mut descriptor_size: usize = core::mem::size_of::<MemDescriptor>();
let mut descriptor_version: u32 = 0;

let get_map_status = (self.get_mem_map)(
&mut mem_map_size,
mem_map,
&mut map_key,
&mut descriptor_size,
&mut descriptor_version
);
if get_map_status != STATUS_BUFFER_TOO_SMALL {
return Err(get_map_status)
}
// mem_map_size now contains the size of memory required to
// hold the current memory map

}

At this point, we have the required size of the memory map. Step 2 is to allocate memory to hold the memory map.

To allocate memory, Boot Services has a function, EFI_ALLOCATE_POOL. It is described as:

typedef
EFI_STATUS
(EFIAPI *EFI_ALLOCATE_POOL) (
    IN EFI_MEMORY_TYPE PoolType,
    IN UINTN Size,
    OUT VOID **Buffer
);

The first argument PoolType tells us what type of pool memory to allocate. Size tells the number of bytes to allocate and Buffer will hold a pointer to the allocated buffer after a successful call.

The EFI_MEMORY_TYPE is defined as:

typedef enum {
    EfiReservedMemoryType,
    EfiLoaderCode,
    EfiLoaderData,
    EfiBootServicesCode,
    EfiBootServicesData,
    EfiRuntimeServicesCode,
    EfiRuntimeServicesData,
    EfiConventionalMemory,
    EfiUnusableMemory,
    EfiACPIReclaimMemory,
    EfiACPIMemoryNVS,
    EfiMemoryMappedIO,
    EfiMemoryMappedIOPortSpace,
    EfiPalCode,
    EfiPersistentMemory,
    EfiMaxMemoryType
} EFI_MEMORY_TYPE;

EFI_MEMORY_TYPE is just an enum that says something about what some segment of memory is being used for.

In uefi.rs, this will look like this:

// Tells what a segment of memory is for
#[repr(u32)]
enum MemType {
EfiReservedMemoryType = 0,
EfiLoaderCode,
EfiLoaderData,
EfiBootServicesCode,
EfiBootServicesData,
EfiRuntimeServicesCode,
EfiRuntimeServicesData,
EfiConventionalMemory,
EfiUnusableMemory,
EfiACPIReclaimMemory,
EfiACPIMemoryNVS,
EfiMemoryMappedIO,
EfiMemoryMappedIOPortSpace,
EfiPalCode,
EfiPersistentMemory,
EfiMaxMemoryType
}

We don't have to give the rest values because we've already given the first. The values of the rest will automatically be determined by the compiler.

This EFI_ALLOCATE_POOL function comes right after the GetMemoryMap:

#[repr(C)]
pub struct BootServices {
unneeded1: [u8; 56],
get_mem_map: extern "efiapi" fn(
mem_map_size: *mut usize,
mem_map: *mut MemDescriptor,
map_key: *mut usize,
descriptor_size: *mut usize,
descriptor_version: *mut u32
) -> Status,
// NEW:
// Allocates a chunk of memory
alloc_pool: extern "efiapi" fn(
pool_type: MemType,
size: usize,
buffer: *mut *mut u8
) -> Status,
// DELETED: unneeded2: [u8; 168],
unneeded2: [u8; 160], // NEW
exit_boot_services: extern "efiapi" fn(
image_handle: *const core::ffi::c_void,
map_key: usize
) -> Status,
unneeded: [u8; 80],
locate_protocol: extern "efiapi" fn(
protocol: *const Guid,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void,
) -> Status,
}

Before we go on to allocate memory for the map, from the spec:

The actual size of the buffer allocated for the consequent call to GetMemoryMap() should be bigger then the value returned in MemoryMapSize, since allocation of the new buffer may potentially increase memory map size.

So, the memory we allocate for the map has to be slightly bigger than what the firmware tells us.

Our breakdown of number 2 now looks like this:

  1. Call alloc_pool to allocate a little more memory than needed.
  2. Verify that alloc_pool executed successfully.

In code:

pub fn exit_boot_services(&self) -> Result<(), Status> {
// ... Others
if get_map_status != STATUS_BUFFER_TOO_SMALL {
return Err(get_map_status)
}

// Allocate memory to hold the memory map

let mem_type = MemType::EfiBootServicesData;
// A kilobytes is a 1024 bytes
let one_kib = 1024;

// Allocating an extra kilobyte because the spec
// recommends that we allocate extra memory
let mut new_mem_map_size = mem_map_size + one_kib;
let alloc_mem_status = (self.alloc_pool)(
mem_type,
new_mem_map_size,
// Casting the buffer as a pointer to bytes
&mut mem_map as *mut _ as *mut *mut u8
);
if alloc_mem_status != STATUS_SUCCESS {
return Err(alloc_mem_status);
}
}

Now that we have the memory required to hold the map, we have to get the memory map:

pub fn exit_boot_services(&self) -> Result<(), Status> {
// ... Others
if alloc_mem_status != STATUS_SUCCESS {
return Err(alloc_mem_status);
}

// Get the actual memory map
let get_map_status = (self.get_mem_map)(
&mut new_mem_map_size,
mem_map,
&mut map_key,
&mut descriptor_size,
&mut descriptor_version
);
if get_map_status != STATUS_SUCCESS {
return Err(get_map_status);
}
// At this point, map_key holds the key of the latest
// memory map

}

The final step now is to exit the Boot Services with the map key.

pub fn exit_boot_services(&self) -> Result<(), Status> {
// ... Others
if get_map_status != STATUS_SUCCESS {
return Err(get_map_status);
}

// Exit the Boot Services using the map key
let exit_status = (self.exit_boot_services)(
image_handle,
map_key
);
if exit_status != STATUS_SUCCESS {
return Err(exit_status);
}
return Ok(());
}

We also need to include the dependency on the image handle:

// DELETED: pub fn exit_boot_services(&self) -> Result<(), Status> {
pub fn exit_boot_services(&self, image_handle: *const core::ffi::c_void) -> Result<(), Status> { // NEW
// ... Others
}

And that's our exit_boot_services function.

The function now requires an argument of image handle to be passed:

In main.rs:

#[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();

// DELETED: boot_services.exit_boot_services();
boot_services.exit_boot_services(handle).unwrap(); // NEW

game::blasterball(screen);

// Returning 0 because the function expects it
0
}

Running the game now will result in the game scene being drawn on the screen, as expected.

We can now get to setting up interrupts and event handling.

Take Away

For the full code up to this point, go to the repo

In The Next Post

We'll start looking at interrupts.

References

  • UEFI spec, version 2.7, section 2.3.4, for stuff the firmware sets up for you
  • UEFI spec, version 2.7, section 4.4, for the Boot Services table
  • UEFI spec, version 2.7, section 7.2, for memory allocation services in the Boot Services table
  • UEFI spec, version 2.7, section 7.4, for image handling services

You can download the UEFI spec here