Demilade Sonuga's blog

All posts

The Graphics Output Protocol III

2022-11-10 · 46 min read

Now that we've modeled the GOP, we need to continue with our game. Our aim right now is to check out what's there, see what the GOP has available for us, then use that to come up with a way to draw graphics on the screen effectively.

In a previous post, we looked at the GOP and what it had to offer. It can set different video modes but the one it defaults to is a grayscale mode, where all the bits in the framebuffer are interpreted as color variants of black and white.

To draw graphics, we need to set the GOP to a mode where the framebuffer bits are interpreted as colored pixels.

Before we get to this, we first need the actual GOP instance. When we wanted to output text the first time, we used the Simple Text Output Protocol, which is in the System Table. But the GOP is not in the System Table, so we need another way of getting an instance of it.

The Boot Services, another table with a bunch of useful functions is also provided by the firmware and is accessible through the System Table. This Boot Services table has a function called LocateProtocol that is defined like this:

typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL) (
    IN EFI_GUID *Protocol,
    IN VOID *Registration OPTIONAL,
    OUT VOID **Interface
);

Where

Protocol is a pointer to the GUID of the protocol, a 128 bit value that uniquely identifies a protocol.

Registration is some field we need not worry about.

Interface is a pointer to the first interface found that has GUID Protocol. (Remember that all protocols have a unique 128-bit GUID associated with them.)

Our steps to get graphics now looks like this:

  1. Get boot services table from system table.
  2. Call the LocateProtocol function in the boot services table with the GOP's GUID.
  3. Figure out how to change the mode to a graphics mode with the GOP.
  4. Figure out how to draw on screen.

Okay. At this point, your main.rs should be looking like this:

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
let mut string_u16 = [0u16; 14];
let string = "Hello World!\n";
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
let simple_text_output = unsafe { (*sys_table).simple_text_output };
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
0
}

#[repr(C)]
struct SystemTable {
unneeded: [u8; 60],
simple_text_output: *mut SimpleTextOutput
}

#[repr(C)]
struct SimpleTextOutput {
unneeded: [u8; 8],
output_string: extern "efiapi" fn (this: *mut SimpleTextOutput, *mut u16)
}

#[repr(C)]
struct GraphicsOutput {
query_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32,
size_of_info: *const usize,
info: *mut *const GraphicsModeInfo
) -> usize,
set_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32
) -> usize,
unneeded: [u8; 8],
mode: *const GraphicsMode
}

#[repr(C)]
struct GraphicsModeInfo {
version: u32,
horizontal_resolution: u32,
vertical_resolution: u32,
pixel_format: PixelFormat,
pixel_info: PixelBitmask,
pixels_per_scan_line: u32
}

#[repr(u32)]
enum PixelFormat {
RedGreenBlueReserved = 0,
BlueGreenRedReserved = 1,
BitMask = 2,
BltOnly = 3
}

#[repr(C)]
struct Pixel {
blue: u8,
green: u8,
red: u8,
reserved: u8
}

#[repr(C)]
struct PixelBitmask {
red_mask: u32,
green_mask: u32,
blue_mask: u32,
reserved: u32
}

type PhysAddr = u64;

#[repr(C)]
struct GraphicsMode {
max_mode: u32,
mode: u32,
info: *const GraphicsModeInfo,
size_of_info: usize,
framebuffer_base: PhysAddr,
framebuffer_size: usize
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
loop {}
}

Our SystemTable's only 2 fields are unneeded and simple_text_output. The pointer to the Boot Services is 24 bytes after the simple_text_output field. We now add the boot_services field as follows:

#[repr(C)]
struct SystemTable {
unneeded1: [u8; 60], // NEW
// DELETED: unneeded: [u8; 60],
simple_text_output: *mut SimpleTextOutput,
unneeded2: [u8; 24], // NEW
boot_services: *const BootServices // NEW
}

And the LocateProtocol function pointer is at an offset of 320 bytes from the Boot Services's start address.

// NEW:
// A bunch of other useful functions provided by the firmware
// and accessible from the `SystemTable`
#[repr(C)]
struct BootServices {
// We don't need these other fields
unneeded: [u8; 320],
// A function that can be used to find a protocol by its
// unique GUID
locate_protocol: extern "efiapi" fn(
protocol: *const u128,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void
) -> usize
}

And there's our BootServices. The structure has the #[repr(C)] attribute because field order matters. The first 320 bytes in the table are unneeded at the moment, so we just mark them as unneeded. Then comes our locate_protocol function, which takes a pointer to a 128 bit number, the protocol's unique GUID, an optional registration field which we don't have to worry about, and a pointer interface which will point to the protocol on the function's successful return. The function returns a usize which tells whether or not the function executed successfully.

There's one more thing that is still unexplained, that is the core::ffi::c_void. In the UEFI definition of the function, Registration and Interface are both void pointers.

In C, void pointers are just pointers to anything. Usually, a pointer is always associated with a data type, like a pointer to an integer, or a pointer to a character. But a void pointer doesn't have any type associated with it. It's a pointer to anything.

Because C, the lingua franca of codeworld, is everywhere, a language like Rust will need some way to interface with it. This is done with the core::ffi module, and a pointer to core::ffi::c_void in Rust, is equivalent to C's void pointer.

If you sit back and take a look at our locate_protocol function, you'll notice that it's not very clear from the code what the protocol's u128 is supposed to be. The type definition tells us that it's a pointer to an unsigned 128 bit number, but it doesn't tell us anything else, it doesn't even hint at the meaning of the 128 bit number.

We change that like so:


// NEW:
// A number that uniquely identifies a protocol
type Guid = u128;

#[repr(C)]
struct BootServices {
// We don't need these other fields
unneeded: [u8; 320],
// A function that can be used to find a protocol by its
// unique GUID
locate_protocol: extern "efiapi" fn(
// DELETED: protocol: *const u128,
protocol: *const Guid,
registration: *const core::ffi::c_void,
interface: *mut *mut core::ffi::c_void
) -> usize
}

Okay, now that looks so much better. Guid is still just a u128, but by naming it, we have effectively bestowed it with some higher meaning that will make it easier to understand and reason about the code.

As a matter of fact, we should do the same for the status being returned by the functions:

// NEW:
// Returned by the UEFI functions to indicate the success
// or failure of the function
type Status = usize;

#[repr(C)]
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
// DELETED: ) -> usize
) -> Status
}

And for our GraphicsOutput:

#[repr(C)]
struct GraphicsOutput {
query_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32,
size_of_info: *const usize,
info: *mut *const GraphicsModeInfo
// DELETED: ) -> usize,
) -> Status,
set_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32
// DELETED: ) -> usize,
) -> Status,
unneeded: [u8; 8],
mode: *const GraphicsMode
}

That is so much better now. Before, when we looked at the code, we knew the functions returned usizes, but we didn't know what the usizes meant. With Rust's type system, we can encode a whole lot of high level meaning in our code that makes it easier to write and reason about, without performance penalties, since the Guid is still just a u128 and the Status is still just a usize ---- zero cost abstractions.

Okay, that was all wonderful.

Now we need to actually locate the GOP. In the UEFI spec, the GOP's GUID is given as:

#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID \
{0x9042a9de,0x23dc,0x4a38,\
{0x96,0xfb,0x7a,0xde,0xd0,0x80,0x51,0x6a}}

The number is broken down into groups of hexadecimal numbers. If you're not familiar with hexadecimals, all you need to know for now is that hexadecimal numbers are just another way of representing numbers using digits 1..=9 and letters a..=f, where a, b, c, d, e, f correspond to 10, 11, 12, 13, 14, 15. The 0x in front of the digits is to indicate that what comes after are hex digits. Each digit is 4 bits in size. If you do the math, this number defined is 128 bits.

All GUIDs in the UEFI spec are defined like this. Because of the way it's defined, translating it to our code is not that straightforward. In our code, GUIDs are modeled as blobs of u128s, not chunks of u32s, u16s and u8s.

To make it easier to translate to code, we remodel the GUID in our code as:

// DELETED: type Guid = u128;
// NEW:
// A number that uniquely identifies a protocol
#[repr(C)]
struct Guid {
first_chunk: u32,
second_chunk: u16,
third_chunk: u16,
other_chunks: [u8; 8]
}

And now, we have a model of the GUID in our code which makes it much easier for us to work with. Since each hex digit is 4 bits in size, the first 0x9042a9de of the GOP GUID is 32 bits, the second and third 0x23dc and 0x4a38 are 16 bits each and the remaining 0x96,0xfb,0x7a,0xde,0xd0,0x80,0x51,0x6a are 8 bits each.

Now, to locate the GOP, we go back to our efi_main function:

#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
/* DELETED:
let mut string_u16 = [0u16; 14];
let string = "Hello World!\n";
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
let simple_text_output = unsafe { (*sys_table).simple_text_output };
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
*/

// Getting the pointer to the Boot Services from the System Table
let boot_services = unsafe { (*sys_table).boot_services };
// The Graphics Output Protocol (GOP) GUID
let gop_guid = Guid {
first_chunk: 0x9042a9de,
second_chunk: 0x23dc,
third_chunk: 0x4a38,
other_chunks: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]
};
// This location which will hold a pointer to a GOP on a successful call to locate_protocol
let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
// The raw pointer to the GOP Guid
let guid_ptr = &gop_guid as *const Guid;
// An optional argument which we're just going to pass null into
let registration = core::ptr::null_mut();
// Location where the GOP pointer should be placed into on a successful locate_protocol invocation
let gop_ptr = &mut gop as *mut _;
// Invoking the Boot Services locate_protocol function to find the GOP
let locate_gop_status = unsafe { ((*boot_services).locate_protocol)(
guid_ptr,
registration,
gop_ptr
) };
// Returning 0 because the function expects it
0
}

In the code above, we first retrieve the pointer to the Boot Services table from the System Table into the variable boot_services. Then we instantiate the GOP's GUID in gop_guid.

locate_protocol's first argument, is a pointer to the GUID. We put &gop_guid as *const Guid into guid_ptr. The function expects a pointer to the GUID of a protocol to locate and that is exactly what we're putting here. A reference is actually just a pointer with super powers (which we'll get to later) and they can be casted into raw pointers.

The second argument, registration is an optional argument we don't need. Passing null to a UEFI function as an argument tells the function "I'm deciding not to supply this argument". So, we tell the firmware that we aren't supplying this argument by passing a null pointer to it.

The BootServices::locate_protocol function takes a pointer to a pointer to anything as the third argument. The reason is because locate_protocol needs to be able to return a pointer to any protocol (which is just a bunch of functions). Its way of returning that pointer is to place the pointer in a known location we specify. The last argument, is a pointer to a pointer to anything. We first declared gop as null pointer with the type pointer to anything (*mut core::ffi::c_void). We then took a mutable reference to this pointer and casted that to a pointer (&mut gop as *mut _). The newly casted pointer at this point, is now a pointer to pointer to anything. It's this that we now pass as the third argument to locate_protocol.

On a successful function execution, the value gop would be mutated (changed) by the firmware to hold the location of the GOP instance in memory, that is, a pointer to the GOP.

The locate_gop_status, at the end of the locate_protocol execution will hold a number that tells whether or not the function execution successfully, and if it failed, it will hint at the reason why.

In the UEFI spec, 0 is defined as the success status. So, before we can safely interpret the gop as a pointer to a GOP, we first need to check if the function executed successfully, otherwise we will have undefined behavior as gop was initialized to hold a null pointer.

We now modify our code as follows:


#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
let boot_services = unsafe { (*sys_table).boot_services };
let gop_guid = Guid {
first_chunk: 0x9042a9de,
second_chunk: 0x23dc,
third_chunk: 0x4a38,
other_chunks: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]
};
let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
let guid_ptr = &gop_guid as *const Guid;
let registration = core::ptr::null_mut();
let gop_ptr = &mut gop as *mut _;
let locate_gop_status = unsafe { ((*boot_services).locate_protocol)(
guid_ptr,
registration,
gop_ptr
) };
if locate_gop_status != 0 {
// Space to hold 16 bit numbers to be interpreted as characters by the Simple Text Output's
// output_string function
let mut string_u16 = [0u16; 22];
// The string as a string slice
let string = "Failed to locate GOP\n";
// Converting the string slice to UTF-16 characters and placing the characters
// in the array
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
// Getting the pointer to the Simple Text Output Protocol from the System Table
let simple_text_output = unsafe { (*sys_table).simple_text_output };
// Getting the output_string function from the Simple Text Output Protocol and
// calling it with the required parameters to print "Failed to locate GOP\n"
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
loop {}
}
// Space to hold 16 bit numbers to be interpreted as characters by the Simple Text Output's
// output_string function
let mut string_u16 = [0u16; 30];
// The string as a string slice
let string = "Successfully located the GOP\n";
// Converting the string slice to UTF-16 characters and placing the characters
// in the array
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
// Getting the pointer to the Simple Text Output Protocol from the System Table
let simple_text_output = unsafe { (*sys_table).simple_text_output };
// Getting the output_string function from the Simple Text Output Protocol and
// calling it with the required parameters to print "Successfully located the GOP\n"
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
// Returning 0 because the function expects it
0
}

In the code above, after the call to locate_protocol, we simply check if the status is not a success. If the status is not a success, we use the Simple Text Output protocol to print "Failed to locate GOP\n". If the status is a success, then we output "Successfully located the GOP".

Upon running the code, your emulator should look like this:

Successfully located GOP

And that's it, we've found the GOP.

Take Away

  • A pointer to c_void is a pointer to anything.
  • Rust's type system gives us tools to go beyond ordinary types like usize to types with higher logical meaning like Status.
  • A reference is a pointer with super powers.

Your code should be looking like this now:

Directory view:

blasterball/
| .cargo/
| | config.toml
| src/
| | main.rs
| .gitignore
| Cargo.lock
| Cargo.toml

main.rs contents

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
let boot_services = unsafe { (*sys_table).boot_services };
let gop_guid = Guid {
first_chunk: 0x9042a9de,
second_chunk: 0x23dc,
third_chunk: 0x4a38,
other_chunks: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]
};
let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
let guid_ptr = &gop_guid as *const Guid;
let registration = core::ptr::null_mut();
let gop_ptr = &mut gop as *mut _;
let locate_gop_status = unsafe { ((*boot_services).locate_protocol)(
guid_ptr,
registration,
gop_ptr
) };

if locate_gop_status != 0 {
let mut string_u16 = [0u16; 22];
let string = "Failed to locate GOP\n";
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
let simple_text_output = unsafe { (*sys_table).simple_text_output };
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
loop {}
}
let mut string_u16 = [0u16; 30];
let string = "Successfully located the GOP\n";
string.encode_utf16()
.enumerate()
.for_each(|(i, letter)| string_u16[i] = letter);
let simple_text_output = unsafe { (*sys_table).simple_text_output };
unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
0
}

#[repr(C)]
struct SystemTable {
unneeded: [u8; 60],
simple_text_output: *mut SimpleTextOutput,
unneeded2: [u8; 24],
boot_services: *const BootServices
}

#[repr(C)]
struct Guid {
first_chunk: u32,
second_chunk: u16,
third_chunk: u16,
other_chunks: [u8; 8]
}

type Status = usize;

#[repr(C)]
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
}


#[repr(C)]
struct SimpleTextOutput {
unneeded: [u8; 8],
output_string: extern "efiapi" fn (this: *mut SimpleTextOutput, *mut u16)
}

#[repr(C)]
struct GraphicsOutput {
query_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32,
size_of_info: *const usize,
info: *mut *const GraphicsModeInfo
) -> Status,
set_mode: extern "efiapi" fn(
this: *mut GraphicsOutput,
mode_number: u32
) -> Status,
unneeded: [u8; 8],
mode: *const GraphicsMode
}

#[repr(C)]
struct GraphicsModeInfo {
version: u32,
horizontal_resolution: u32,
vertical_resolution: u32,
pixel_format: PixelFormat,
pixel_info: PixelBitmask,
pixels_per_scan_line: u32
}

#[repr(u32)]
enum PixelFormat {
RedGreenBlueReserved = 0,
BlueGreenRedReserved = 1,
BitMask = 2,
BltOnly = 3
}

#[repr(C)]
struct Pixel {
blue: u8,
green: u8,
red: u8,
reserved: u8
}

#[repr(C)]
struct PixelBitmask {
red_mask: u32,
green_mask: u32,
blue_mask: u32,
reserved: u32
}

type PhysAddr = u64;

#[repr(C)]
struct GraphicsMode {
max_mode: u32,
mode: u32,
info: *const GraphicsModeInfo,
size_of_info: usize,
framebuffer_base: PhysAddr,
framebuffer_size: usize
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
loop {}
}

In the Next Post

We'll be getting to the remaining steps on this list

  1. Get boot services table from system table.
  2. Call the LocateProtocol function in the boot services table.
  3. Figure out how to change the mode to a graphics mode with the GOP.
  4. Figure out how to draw on screen.