OS Development Notes #2: UEFI GOP
2023-07-31 | osdev rust programming uefi gop framebufferGraphics 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.