Software by mistakes

OS Development Notes #4: Intermezzo

|

Before going on, I wanted to see if it's actually possible to run this EFI file on my x86_64 home computer.

USB stick

My computer has boot from USB-stick in UEFI-mode, so it should able to boot from a standard FAT-32 formatted USB-stick with the folder EFI/BOOT and the EFI-file.

It failed. My motherboard claim not to support the GOP.

In case of GOP is not found in UEFI, we can try to scan the PCI for a graphics card and get the framebuffer from this.

Text on screen

At this point it's hard to actually debug what is going on, which was actually my next project. Unfortunately we already hit a wall before being able to use the framebuffer from the GOP.

For getting the GOP in the first place, we had to use the console_out_handle from the SystemTable. We can also use the next function console_out to print on the screen before exiting the boot services.

We define in SystemTable a new struct ConsoleOut that is a direct pointer to the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL, that we usually have to load with open_protocol:

#[repr(C)]
pub struct SystemTable {
    ...
    console_out_handle: Handle,
    console_out: *mut ConsoleOut,
    ...
}

#[repr(C)]
pub struct ConsoleOut {
    reset: extern "efiapi" fn(this: &ConsoleOut, extended: bool) -> usize,
    output_string: unsafe extern "efiapi" fn(this: &ConsoleOut, string: *const u16) -> usize,
    test_string: Handle,
    query_mode: Handle,
    set_mode: Handle,
    set_attribute: Handle,
    clear_streen: Handle,
    set_cursor_position: Handle,
    enable_cursor: Handle,
    output_mode: Handle,
}

We only need to use reset that is defined as:

The Reset() function resets the text output device hardware. The cursor position is set to (0, 0), and the screen is cleared to the default background color for the output device. As part of initialization process, the firmware/device will make a quick but reasonable attempt to verify that the device is functioning.

And output_string defined as using as string:

The Null-terminated string to be displayed on the output device(s). All output devices must also support the Unicode drawing character codes defined in “Related Definitions.”

We implement the functions, where rust already handles utf16 on &str very easily:

impl ConsoleOut {
    pub fn reset(&mut self, extended: bool) -> usize {
        let status = (self.reset)(self, extended);
        status
    }

    pub fn output_string(&mut self, string: &str) -> usize {
        let mut buf = [0u16; 129];
        for (i, ch) in string.encode_utf16().enumerate() {
            buf[i] = ch;
        }

        let status = unsafe { 
            (self.output_string)(self, &buf as *const [u16] as *const u16) 
        };

        status
    }
}

We can the use it as a writer for core::fmt::Write trait (important to import this where used):

use core::fmt::Write;

impl core::fmt::Write for ConsoleOut {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        self.output_string(s);
        Ok(())
    }
}

As we are only going to use it sparsely, we are not attaching it to a log::Log trait or print! macro. We can call it directly, using system table's implementation:

impl SystemTable {
    pub fn console_out(&mut self) -> &mut ConsoleOut {
        unsafe { &mut *self.console_out.cast() }
    }

    ...
}

pub fn get_boot_info(image_handle: Handle, system_table: *mut SystemTable) -> usize {
    unsafe {
        let con_out = (*system_table).console_out();
        con_out.reset(true);
        con_out.write_str("Test");
        
        ...
    }

    0
}

Using write_str and write_fmt(format_args("{:?}", variable)) on con_out, I was able to confirm that indeed, it was only an GOP error with status EFI_UNSUPPORTED. I had no error on getting ACPI RSDP, the memory map and exiting boot services.

Alternative to GOP

The old EFI specification for v. 1.10 specifies an EFI_UGA_DRAW_PROTOCOL, that my system might support instead. We can only use it to set the mode and clear the screen, but the framebuffer we have to find otherwise.

The protocol has an GUID:

#define EFI_UGA_DRAW_PROTOCOL_GUID
{ 0x982c298b,0xf4fa,0x41cb,0xb8,0x38,0x77,0xaa,0x68,0x8f,0xb8,0x39 }

And in our Rust struct format:

const EFI_UGA_DRAW_PROTOCOL: Guid = Guid {
    data1: 0x982c298b,
    data2: 0xf4fa,
    data3: 0x41cb,
    data4: [0xb8, 0x38, 0x77, 0xaa, 0x68, 0x8f, 0xb8, 0x39],
};

Also common for programs like GRUB and rEFInd is that they load the EFI_CONSOLE_CONTROL_HEADER as well, although not defined anywhere.

The GRUB source code tells us why:

The console control protocol is not a part of the EFI spec, but defined in Intel's Sample Implementation.

#define EFI_CONSOLE_CONTROL_GUID
{ 0xf42f7782, 0x12e, 0x4c12,
{ 0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21 }
}

const EFI_CONSOLE_CONTROL_GUID: Guid = Guid {
    data1: 0xf42f7782,
    data2: 0x12e,
    data3: 0x4c12,
    data4: [0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21],
};

We can use it to define if any UGA exists, or we are stuck in text mode:

#[repr(C)]
pub struct ConsoleControlProtocol {
    get_mode: extern "efiapi" fn(&ConsoleControlProtocol, mode: &mut ScreenMode, uga_exists: &mut bool, std_in_locked: &mut bool)e -> usize,
    set_mode: extern "efiapi" fn(&ConsoleControlProtocol, mode: ScreenMode) -> usize,
    lock_std_in: extern "efiapi" fn(&ConsoleControlProtocol, password: &mut u16) -> usize,
}

pub enum ScreenMode {
    Text = 0,
    Graphics = 1,
    MaxValue = 2,
}

and UGA:

#[repr(C)]
pub struct UniversalGraphicsProtocol {
    get_mode: extern "efiapi" fn(
        &UniversalGraphicsProtocol,
        horizontal_resolution: &mut u32,
        vertical_resolution: &mut u32,
        color_depth: &mut u32,
        refresh_rate: &mut u32,
    ) -> usize,
    set_mode: extern "efiapi" fn(
        &UniversalGraphicsProtocol,
        horizontal_resolution: u32,
        vertical_resolution: u32,
        color_depth: u32,
        refresh_rate: u32,
    ) -> usize,
    blt: extern "efiapi" fn(
        &UniversalGraphicsProtocol,
        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,
}

All the protocols!

Weirdly, ConsoleControlProtocol tells me that UGA is supported, but I am not able to open the protocol. Let's actually see what protocols are available, using protocols_per_handle in the BootServices struct.

The results for the image_handle:

0xBC62157E - EFI_LOADED_IMAGE_DEVICE_PATH_PROTOCOL_GUID
0x5B1B31A1 - EFI_LOADED_IMAGE_PROTOCOL_GUID

The results for the console_out_handle:

0xC7A7030C - MouseDriver
0x8D59D32B - EFI_ABSOLUTE_POINTER_PROTOCOL_GUID
0x31878C87 - EFI_SIMPLE_POINTER_PROTOCOL_GUID
0x0ADFB62D - AmiEfiKeycodeProtocolGuid 
0xDD9E7534 - EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID
0x387477C1 - EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID
0xF42F7782 - EFI_CONSOLE_CONTROL_PROTOCOL_GUID
0x387477C2 - EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID
0xB295BD1C - AmiMultiLanSupportProtocolGuid 

Some of these GUIDs I found in this CSV file. Kudos to LongSoft for compiling this list.

So it seems I have no UGA either. Maybe my NVIDIA graphics card doesn't support any standard GOP resolutions, since my CPU has no iGPU. I did read that some NVIDIA cards have to be flashed for using GOP. Not something I am interested in at this point.

What is curious is that my AMI BIOS are placing some of their keyboard, mouse and network protocols on the console_out_handle. I guess I experienced the beauty of the many UEFI ROM implementations, and have to wait for my next upgrade to hopefully have a better one.

A nice learning experience either way. Find the code and some refactorization on Gitlab in the osdev-4 branch.