Demilade Sonuga's blog

All posts

The Graphics Output Protocol II

2022-11-07

In the previous post, we took our first steps to modeling the GOP. At this point, your code should look 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,
}

#[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
}

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

Remember our GOP is specified like this:

typedef struct EFI_GRAPHICS_OUTPUT_PROTCOL {
    EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE QueryMode;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE SetMode;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT Blt;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL;

So we're not done yet. We still need to get SetMode, Blt and Mode in our code.

In the spec, the definition of EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE is

typedef
EFI_STATUS
(EFIAPI *EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE) (
    IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
    IN UINT32 ModeNumber
);

The SetMode function just takes a pointer to the GOP in This and the mode number associated with the mode which you want set the video device to in ModeNumber. The function returns a status which tells if it was successful.

In Rust:

#[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,
    // NEW:
    // Sets the video device into the mode associated with `mode_number` and clears
    // the visible portions of the output display to black
    set_mode: extern "efiapi" fn(
        this: *mut GraphicsOutput,
        mode_number: u32
    ) -> usize
}

The EFI_GRAPHICS_OUTPUT_PROTOCOL maps to GraphicsOutput and UINT32 (unsigned 32 bit integer) maps to u32 in our code. And the EFI_STATUS is just a usize.

Next on our list is the Blt. This function is used to draw rectangles on the video screen. But we really won't be needing it in this project, so we just place an unneeded field in our GOP representation:

#[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,
    // NEW:
    // The Blt function pointer, which we don't need
    unneeded: [u8; 8]
}

In our GraphicsOutput struct, instead of the function, we place an array of 8 bytes and mark the field as unneeded. 8 bytes because the fn is just a pointer to a function, a pointer is an address and addresses on x86_64, which is the platform we're developing for, are 8 bytes in size.

Lastly, we have the Mode field. Mode is just a pointer to a EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE structure, a read-only structure which gives information about the current and available video modes. This structure is defined as:

typedef struct {
    UINT32 MaxMode;
    UINT32 Mode;
    EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
    UINTN SizeOfInfo;
    EFI_PHYSICAL_ADDRESS FrameBufferBase;
    UINTN FrameBufferSize;
} EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE;

The MaxMode field gives the number of modes supported by the GOP's QueryMode and SetMode.

The Mode field gives the currently set mode of the graphics device, where the only allowed values are 0..=MaxMode-1. Info is a pointer to a read-only instance of EFI_GRAPHICS_OUTPUT_MODE_INFORMATION which is modelled as GraphicsModeInfo in our code.

SizeOfInfo gives the size of the GraphicsModeInfo strucure in bytes. It may seem like this field isn't needed, since the size of our struct is fixed but it is because the definition of the structure can change in future UEFI specs, but this isn't really relevant to us right now, so I think we can forget about it.

FrameBufferBase gives the beginning address of the graphics framebuffer, the segment of memory whose bit contents are interpreted as color intensities of pixels to be displayed on the screen.

FrameBufferSize is the amount of memory in bytes needed to support the active mode, that is, number of pixels in one horizontal line * number of pixels in one vertical line * the size of a pixel in bytes.

The EFI_PHYSICAL_ADDRESS, which is the type of the FrameBufferBase field is just an alias for UINT64, which is a 64 bit unsigned integer (8 bytes).

In Rust:

// The physical address on x86_64 is 8 bytes (64 bits)
type PhysAddr = u64;

// Gives info about the currently set and other available graphics modes
#[repr(C)]
struct GraphicsMode {
    // The number of modes supported by `GraphicsOutput::set_mode`
    // and `GraphicsOutput::query_mode`
    max_mode: u32,
    // The number associated with the current mode of the graphics
    // device. Valid values are always in the range 0..=`max_mode`-1
    mode: u32,
    // Pointer to a read only GraphicsModeInfo
    info: *const GraphicsModeInfo,
    // Size of the `GraphicsModeInfo` structure
    size_of_info: usize,
    // The starting address of the graphics framebuffer
    framebuffer_base: PhysAddr,
    // The size of the framebuffer in bytes
    framebuffer_size: usize
}

Our GraphicsOutput now:

#[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,
    // The Blt function pointer, which we don't need
    unneeded: [u8; 8],
    // NEW:
    // Gives information about the current graphics mode
    // and the other available modes
    mode: *const GraphicsMode
}

And that's it, we now have GOP in our Rust code.

Take Away

  • Addresses on x86_64 are 64 bit numbers.

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 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 {}
}

In the Next Post

We'll be going through the steps to locating the GOP

References

  • UEFI spec, version 2.7, section 12.9 (https://uefi.org/specifications)