OS Development Notes #4: Intermezzo
2023-10-22 | osdev rust programming uefi boot hardwareBefore 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.