Software by mistakes

OS Development Notes #2: UEFI GOP

|

Graphics Output Protocol

Boot Services

The UEFI Boot Services defines quite a few services that we can access for various tasks and information. We can set timers and event handlers, getting free memory, allocate memory, handle loading and running EFI images. What we are interested in at this point is loading protocols. Remember that any usage of these services are invalidated after calling ExitBootServices, so we are only interested in using persistant changes and getting information for further usage.

First let's define the BootServices part of our UEFI System Table, mentioned in the last article:

struct SystemTable {
    ...
    boot_services: *const BootServices,
    ...
}

The BootServices struct is quite large, and for now we only need to access one function pointer to open a protocol, as defined in the UEFI specification Chapter 7.3:

#[repr(C)]
struct BootServices {
    hdr: EFIHeader,

    // Task Priority Services
    raise_tpl: Handle,
    restore_tpl: Handle,

    // Memory Services
    allocate_pages: Handle,
    free_pages: Handle,
    get_memory_map: Handle,
    allocate_pool: Handle,
    free_pool: Handle,

    // Event & Time Services
    create_event: Handle,
    set_timer: Handle,
    wait_for_event: Handle,
    signal_event: Handle,
    close_event: Handle,
    check_event: Handle,

    // Protocol Handler Services
    install_protocol_interface: Handle,
    reinstall_protocol_interface: Handle,
    uninstall_protocol_interface: Handle,
    handle_protocol: Handle,
    _reserved: Handle,
    register_protocol_notify: Handle,
    locate_handle: Handle,
    locate_device_path: Handle,
    install_configuration_table: Handle,

    // Image Services
    load_image: Handle,
    start_image: Handle,
    exit: Handle,
    unload_image: Handle,
    exit_boot_services: Handle,

    // Misc Services
    get_next_monotonic_count: Handle,
    stall: Handle,
    set_watchdog_timer: Handle,

    // DriverSupport Services
    connect_controller: Handle,
    disconnect_controller: Handle,

    // Adds elements to the list of agents consuming a protocol interface.
    open_protocol: extern "efiapi" fn(
        handle: Handle,
        protocol: &Guid,
        interface: &mut *mut c_void,
        agent_handle: Handle,
        controller_handle: Option<Handle>,
        attributes: u32,
    ) -> usize,
    close_protocol: Handle,
    open_protocol_information: Handle,

    // Library Services
    protocols_per_handle: Handle,
    locate_handle_buffer: Handle,
    locate_protocol: Handle,
    install_multiple_protocol_interfaces: Handle,
    uninstall_multiple_protocol_interfaces: Handle,

    // 32-bit CRC Services
    calculate_crc32: Handle,

    // Misc Services
    copy_mem: Handle,
    set_mem: Handle,
    create_event_ex: Handle,
}

For opening an protocol, we need an identifier:

In the abstract, a protocol consists of a 128-bit globally unique identifier (GUID) and a Protocol Interface structure.

We define the GUID by how specification typically represent them as a 32-bit integer, followed by two 16-bit integers and eight 8-bit integers (total 128 bit):

#[repr(C)]
pub struct Guid {
    data1: u32,
    data2: u16,
    data3: u16,
    data4: [u8; 8],
}

For accessing these functions easier, we use a decorative pattern giving us a rusty way implementation:

use core::{ffi::c_void, ptr};

impl BootServices {
    pub fn open_protocol(
        &self,
        handle: Handle,
        protocol: Guid,
        agent: Handle,
        controller: Option<Handle>,
        attributes: OpenProtocolAttribute,
    ) -> Result<*mut c_void, usize> {
        let mut interface = ptr::null_mut();
        let status = (self.open_protocol)(
            handle,
            &protocol,
            &mut interface,
            agent,
            controller,
            attributes as u32,
        );

        if status != 0 {
            return Err(status);
        }

        Ok(interface)
    }
}

As we now use Handle as a parameter, it needs to be public, and we also need to derive Copy as we are going to implement it using it on several parameters:

#[derive(Clone, Copy)]
#[repr(C)]
pub struct Handle(NonNull<c_void>);

The attributes can be set as a 32-bit represented enum by the definitions:

#[repr(u32)]
pub enum OpenProtocolAttribute {
    ByHandleProtocol = 0x01,
    GetProtocol = 0x02,
    TestProtocol = 0x04,
    ByChildController = 0x08,
    ByDriver = 0x10,
    ByExclusive = 0x20,
}

Graphics Output Protocol

Now that we have defined how to get a protocol, we look for the Graphical Output Protocol which is defined under Chapter 12 Console Support with the following GUID:

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

And in our Rust struct format:

const EFI_GRAPHICS_OUTPUT_PROTOCOL: Guid = Guid {
    data1: 0x9042a9de,
    data2: 0x23dc,
    data3: 0x4a38,
    data4: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a],
};

To retrieve the GOP, we will define the structure of it as well:

#[repr(C)]
pub struct GraphicsOutputProtocol<'open_protocol> {
    query_mode: extern "efiapi" fn(
        &GraphicsOutputProtocol,
        mode_number: u32,
        size_of_info: &mut usize,
        info: &mut *const ModeInfo,
    ) -> usize,
    set_mode: extern "efiapi" fn(&mut GraphicsOutputProtocol, mode_number: u32) -> usize,
    blt: extern "efiapi" fn(
        this: &mut GraphicsOutputProtocol,
        blt_buffer: *mut BltPixel,
        blt_operation: u32,
        source_x: usize,
        source_y: usize,
        destination_x: usize,
        destination_y: usize,
        width: usize,
        height: usize,
        delta: Option<usize>,
    ) -> usize,
    mode: &'open_protocol Mode<'open_protocol>,
}

The four parameters are defined as:

QueryMode

Returns information for an available graphics mode that the graphics device and the set of active video output devices supports.

We define ModeInfo as:

#[derive(Copy, Clone)]
#[repr(C)]
pub struct ModeInfo {
    version: u32,
    horizontal_resolution: u32,
    vertical_resolution: u32,
    pixel_format: PixelFormat,
    pixel_information: PixelMask,
    pixels_per_scan_line: u32,
}

#[derive(Copy, Clone)]
#[repr(u32)]
pub enum PixelFormat {
    RedGreenBlueReserved8BitPerColor = 0,
    BlueGreenRedReserved8BitPerColor,
    PixelBitMask,
    PixelBltOnly,
    PixelFormatMax,
}

#[derive(Copy, Clone)]
#[repr(C)]
pub struct PixelMask {
    red: u32,
    green: u32,
    blue: u32,
    _reserved: u32,
}

The Pixel Mask represents each pixels drawn on the screen with an intensity of red, green and blue, and the Pixel Format is a given definition on how the GOP works (RGB, BGR, BLT ...).

As we later on query for modes, we have to set these as Copy derived.

SetMode

Set the video device into the specified mode and clears the visible portions of the output display to black.

Blt

Software abstraction to draw on the video device’s frame buffer.

We define a BLT Pixel as BGR moded with only 8-bit color intensities:

#[repr(C)]
pub struct BltPixel {
    blue: u8,
    green: u8,
    red: u8,
    _reserved: u8,
}

As we will use BLT for clearing the screen, we also define the operation needed:

pub enum BltOperation {
    VideoFill {
        color: BltPixel,
        destination: (usize, usize),
        dimensions: (usize, usize),
    },
    _VideoToBltBuffer,
    _BufferToVideo,
    _VideoToVideo,
    _GOPBltOperationMax,
}

Mode

Pointer to EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE data.

The data from the mode we define as

#[derive(Copy, Clone)]
#[repr(C)]
pub struct Mode<'open_protocol> {
    max_mode: u32,
    mode: u32,
    info: &'open_protocol ModeInfo,
    size_of_info: usize,
    frame_buffer_base: usize,
    frame_buffer_size: usize,
}

where we also get the information of the framebuffer's address frame_buffer_base and the length frame_buffer_size. Mode also derives Copy as ModeInfo as we see later on when using the protocol.

Retrieving the protocol

Now that we have set the basics for getting a protocol, and defined the GOP protocol, we can create a function to open the protocol and giving us the struct:

pub fn get_gop(
    bs: &BootServices,
    console_out_handle: Handle,
) -> Result<&mut GraphicsOutputProtocol, usize> {
    let found = bs.open_protocol(
        console_out_handle,
        EFI_GRAPHICS_OUTPUT_PROTOCOL,
        console_out_handle,
        None,
        OpenProtocolAttribute::GetProtocol,
    )?;
    let interface = found.cast::<GraphicsOutputProtocol>();
    let gop = unsafe { &mut *interface };
    return Ok(gop);
}

We borrow the boot services and take in the console_out_handle from the System Table as defined in the last article, and the changes mentioned earlier to the type.

As we have the same error type on Result we just use error propagation (question mark), that will return Err(status) from its own result. If we get an Ok(found) we continue the match.

From the result of opening the protocol, we cast the mut_ptr to our GraphicsOutputProtocol struct, and cast it as mutable before returning. This is considered unsafe but we know at this point that the interface exists and that it's mutable.

Setting it up from the kernel

We have now the structure to start using the BLT for clearing the screen and getting the framebuffer information:

For system table, let's access the boot_services and console_out_handle:

impl SystemTable {
    pub fn console_out_handle(&self) -> Handle {
        self.console_out_handle
    }

    pub fn boot_services(&self) -> &BootServices {
        unsafe { &*self.boot_services }
    }
}

We can now use the system table in the efi_main for accessing them for the usage of getting the GOP:

#[no_mangle]
extern "C" fn efi_main(_image_handle: Handle, system_table: *mut SystemTable) -> usize {
    unsafe {
        let bs = (*system_table).boot_services();
        let console_out_handle = (*system_table).console_out_handle();
        match get_gop(&bs, console_out_handle) {
            Ok(gop) => {
            
            },
            Err(status) => return status,
        }
    }

    loop {}
}

As a dereference of a raw pointer is considered unsafe, we have to wrap our logic in unsafe. As it's the base of the EFI Main we can assume that we are able to derefence it, just like we dereferenced the boot_services in the getter.

Using the Graphics Output Protcol

In the Ok(gop) match we now want to do four things:

  • Find the resolution we want
  • Setting the mode
  • Clearing the screen with a background color
  • Getting the framebuffer information for later usage

Finding the resolution

By definition, the minimum required resolution by Microsoft Windows for UEFI is 1024x768, so we set that as a fallback:

let wanted_resolution = (1920, 1080);
let graphics_mode = &match gop
    .modes()
    .find(|mode| mode.info().resolution() == wanted_resolution)
{
    Some(mode) => mode,
    None => {
        gop.modes()
            .find(|mode| mode.info().resolution() == (1024, 768))
            .expect("UEFI required resolution not found")
    }
};

For this we create the iterator function modes() and query_mode() in the GOP implementation:

impl<'open_protocol> GraphicsOutputProtocol<'open_protocol> {
    pub fn query_mode(&self, index: u32) -> Option<GraphicsMode> {
        let mut info_size = 0;
        let mut info = ptr::null();
        let status = (self.query_mode)(self, index, &mut info_size, &mut info);
        if status == 0 {
            let info = unsafe { *info };
            return Some(GraphicsMode { index, info });
        } else {
            return None;
        }
    }

    pub fn modes(&'_ self) -> impl ExactSizeIterator<Item = GraphicsMode> + '_ {
        GraphicsModeIterator {
            gop: self,
            current_index: 0,
            max_mode: self.mode.max_mode,
        }
    }
}

For the types, we define a graphics mode returned with the index to lookup:

pub struct GraphicsMode {
    index: u32,
    info: ModeInfo,
}

impl GraphicsMode {
    pub fn info(&self) -> ModeInfo {
        self.info
    }
}

impl ModeInfo {
    pub fn resolution(&self) -> (usize, usize) {
        (
            self.horizontal_resolution as usize,
            self.vertical_resolution as usize,
        )
    }
}

and define an iterator for modes, increasing the index, with having the GraphicsMode as data:

struct GraphicsModeIterator<'a> {
    gop: &'a GraphicsOutputProtocol<'a>,
    current_index: u32,
    max_mode: u32,
}

impl<'a> Iterator for GraphicsModeIterator<'a> {
    type Item = GraphicsMode;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current_index < self.max_mode {
            let mode = self.gop.query_mode(self.current_index);
            self.current_index += 1;
            return mode;
        }
        None
    }
}

impl ExactSizeIterator for GraphicsModeIterator<'_> {}

For an iterator we can now use the find functional-style lambda on the mode found, and check if there is a match

find(|mode| mode.info().resolution() == wanted_resolution)

Setting the mode

We set the mode, and if there is any errors, we return it instead of continuing the kernel loop:

let status = gop.set_mode(graphics_mode);
if status != 0 {
    return status;
}

We define the set_mode() function in the GOP as:

pub fn set_mode(&mut self, mode: &GraphicsMode) -> usize {
    (self.set_mode)(self, mode.index)
}

The resolution should change for the screen and be cleared as black, and we are now ready for using the framebuffer or using the BLT.

Clearing the screen

impl BltPixel {
    pub fn new(red: u8, green: u8, blue: u8) -> Self {
        Self {
            red,
            green,
            blue,
            _reserved: 0,
        }
    }
}

We create the BLT Operation, as we defined earlier, making a nice blue background, and run it with the GOP (remember to change the wanted_resolution to what the mode was actually set to):

let op = BltOperation::VideoFill {
    color: BltPixel::new(2, 81, 170),
    destination: (0, 0),
    dimensions: wanted_resolution,
};

let status = gop.blt(op);
if status != 0 {
    return status;
}

We define the blt() function in the GOP as following:

pub fn blt(&mut self, operation: BltOperation) -> usize {
    match operation {
        BltOperation::VideoFill {
            color,
            destination: (dest_x, dest_y),
            dimensions: (width, height),
        } => (self.blt)(
            self,
            &color as *const _ as *mut _,
            0,
            0,
            0,
            dest_x,
            dest_y,
            width,
            height,
            Some(0),
        ),
        _ => 0,
    }
}

We should now see that the screen has been cleared with the color of our choosing. This is a visual verification of that the framebuffer is working, and we can now retrieve the framebuffer information for future reference. Remember that GOP with its BLT functions are not available after we exit the boot services and use our kernel directly.

Getting framebuffer information

We want to get following information for further usage:

  • Physical address of the framebuffer
  • Length of framebuffer
  • The width and height of the resolution (chosen mode)
  • The pixel format to draw with (RGB or BGR)
  • The stride / scanline to define the correct pixel length of a line

For this we can create some helper functions from the GOP:

let (framebuffer_address, framebuffer_size) = gop.frame_buffer();
let mode_info = gop.mode_info();
let pixel_format = mode_info.pixel_format();
let (width, height) = mode_info.resolution();
let stride = mode_info.stride();

defined as in the GOP impl:

pub fn mode_info(&self) -> ModeInfo {
    *self.mode.info
}

pub fn frame_buffer(&mut self) -> (*mut c_void, usize) {
    (
        self.mode.frame_buffer_base as *mut c_void,
        self.mode.frame_buffer_size,
    )
}

and in ModeInfo impl:

pub fn pixel_format(&self) -> PixelFormat {
    self.pixel_format
}

pub fn stride(&self) -> usize {
    self.pixels_per_scan_line as usize
}

We can now define a FramebufferInfo struct to collect these data:

pub struct FrameBufferInfo {
    pub framebuffer_address: *mut c_void,
    pub framebuffer_size: usize,
    pub height: usize,
    pub width: usize,
    pub pixel_format: PixelFormat,
    pub stride: usize,
}

Next time

In the next article we look into getting the ACPI tables, that has been included in the system table as configuration_table and retrieving the pointer to start the process of using ACPI for understanding the system we are booted into. We will explore ACPI and the built-in rust CPUID later on.

We will also conclude UEFI by getting the memory map, which is required to exit the boot services, and verify that we can run our kernel on our own. Maybe in the future, we can look into using the Runtime Services as well, at least for being able to (soft) reset the computer.

Retrospective

This was a longer article, as there are many definitions in the UEFI that needs to pencilled out. On the other hand they are also make it easy to lean on instead of using BIOS int calls from assembly, especially when using Rust. This is something we are going to see with ACPI as well, while when we get further down the x86 rabbit hole, it starts getting more blurry.

At this point there is also many routes to take:

In my first iteration I took the time to play around with the framebuffer to draw fonts and rectangles on the screen. I also did UART for serial connection to debugging.

In my second iteration I was focused on the memory mapping, to understand the reasoning behind it, and because I haven't done system programming for a while, I followed an assembly OS for a while that gives a better persective on how memory works.

In my third iteration I spend time on ACPI and CPUID to see what information can be gotten from the system, and this is what I am at this point. As I am still tinkering around with system programming, I have still a lot to learn. There are probably better ways to do it, and you can surely get started faster with using the uefi and acpi Rust crates, together with boot. My purpose is to learn how the basics works, and also getting better at Rust at the same time.

I follow the notes my self to create a new project from scratch to verify that the article actually works.

I find it better to close off UEFI first, so get a verification of a self-served kernel. If you want to use a two-staged solution, you have to look into the specification on loading images and running them. These images has to be the EFI format anyways, so that's why we stay with an unikernel, that doesn't have the 512 byte limit, a legacy bootable image has.

As my UART implementation still uses the standard COM ports for legacy BIOS, I am not sure yet if I want to expose this for my notes before researching if the UEFI serial protocol is to be used instead.