Introduction
Welcome to the Rust UEFI Book. The focus of this book is how to use
uefi-rs
to build UEFI applications in Rust, but it also describes
some general UEFI concepts, as well as relevant tools such as QEMU.
Tutorial
This tutorial describes the process of creating and running a simple x86_64 UEFI application in Rust. The application will print "Hello World", pause for 10 seconds, then exit.
Creating a UEFI application
Install dependencies
Follow the Rust installation instructions to set up Rust.
Create a minimal application
Create an empty application and change to that directory:
cargo new my-uefi-app
cd my-uefi-app
Add a few dependencies:
cargo add log uefi
Replace the contents of src/main.rs
with this:
#![no_main] #![no_std] use log::info; use uefi::prelude::*; #[entry] fn main(_image_handle: Handle, mut system_table: SystemTable<Boot>) -> Status { uefi::helpers::init(&mut system_table).unwrap(); info!("Hello world!"); system_table.boot_services().stall(10_000_000); Status::SUCCESS }
Walkthrough
Let's look a quick look at what each part of the program is doing,
starting with the #![...]
lines at the top:
#![allow(unused)] #![no_main] #![no_std] fn main() { }
This is some boilerplate that all Rust UEFI applications will
need. no_main
is needed because the UEFI application entry point is
different from the standard Rust main
function. no_std
is needed to
turn off the std
library; the core
and alloc
crates can still be
used.
Next up are some use
lines. Nothing too exciting here; the
uefi::prelude
module is intended to be glob-imported, and exports a
number of commonly-used types.
#![allow(unused)] fn main() { use log::info; use uefi::prelude::*; }
Now we get to the UEFI application main
function, and here things look
a little different from a standard Rust program.
#[entry] fn main(_image_handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
The main
function in a Uefi application always takes two arguments,
the image handle and the system table. The image handle represents the
currently-running executable, and the system table provides access to
many different UEFI services. The main
function returns a Status
,
which is essentially a numeric error (or success) code defined by UEFI.
The first thing we do inside of main
is initialize uefi_services
:
#![allow(unused)] fn main() { uefi::helpers::init(&mut system_table).unwrap(); }
The uefi_services
crate is not strictly required to make a UEFI
application with the uefi
crate, but it makes things much simpler by
setting a simple memory allocator, initializing the logger, and
providing a panic handler.
Next we use the standard log
crate to output "Hello world!". Then we
call stall
to make the system pause for 10 seconds. This just ensures
you have enough time to see the output.
#![allow(unused)] fn main() { info!("Hello world!"); system_table.boot_services().stall(10_000_000); }
Finally we return Status::SUCCESS
indicating that everything completed
successfully:
#![allow(unused)] fn main() { Status::SUCCESS } }
Building
Toolchain
In order to compile for UEFI, an appropriate target must be installed. The
easiest way to set this up is using a rustup toolchain file. In the root of
your repository, add rust-toolchain.toml
:
[toolchain]
targets = ["aarch64-unknown-uefi", "i686-unknown-uefi", "x86_64-unknown-uefi"]
Here we have specified all three of the currently-supported UEFI targets; you can remove some if you don't need them.
Build the application
Run this command to build the application:
cargo build --target x86_64-unknown-uefi
This will produce an x86-64 executable:
target/x86_64-unknown-uefi/debug/my-uefi-app.efi
.
Running in a VM
Install dependencies
Two dependencies are needed: QEMU, which implements the virtual machine itself, and OVMF, which provides UEFI firmware that QEMU can run.
The details of how to install QEMU and OVMF will vary depending on your operating system.
Debian/Ubuntu:
sudo apt-get install qemu ovmf
Fedora:
sudo dnf install qemu-kvm edk2-ovmf
Firmware files
The OVMF package provides two firmware files, one for the executable code and one for variable storage. (The package may provide multiple variations of these files; refer to the package's documentation for details of the files it includes.)
For ease of access we'll copy the OVMF code and vars files to the
project directory. The location where OVMF is installed depends on your
operating system; for Debian, Ubuntu and Fedora the files are under
/usr/share/OVMF
.
Copy the files to your project directory:
cp /usr/share/OVMF/OVMF_CODE.fd .
cp /usr/share/OVMF/OVMF_VARS.fd .
System partition
Now create a directory structure containing the executable to imitate a UEFI System Partition:
mkdir -p esp/efi/boot
cp target/x86_64-unknown-uefi/debug/my-uefi-app.efi esp/efi/boot/bootx64.efi
Launch the VM
Now we can launch QEMU, using VVFAT to access the esp
directory created above.
qemu-system-x86_64 -enable-kvm \
-drive if=pflash,format=raw,readonly=on,file=OVMF_CODE.fd \
-drive if=pflash,format=raw,readonly=on,file=OVMF_VARS.fd \
-drive format=raw,file=fat:rw:esp
A QEMU window should appear, and after a few seconds you should see the log message:
[ INFO]: src/main.rs@011: Hello world!
Running on Hardware
To run on real hardware you'll need a specially-prepared USB drive.
Preparation
The general steps to prepare the drive are:
- Partition the drive using GPT.
- Create a partition.
- Set the partition type GUID to
C12A7328-F81F-11D2-BA4B-00A0C93EC93B
. That marks it as an EFI System partition. (On many UEFI implementations this is not strictly necessary, see note below.) - Format the partition as FAT.
- Mount the partition.
- Create the directory path
EFI/BOOT
on the partition. (FAT is case insensitive, so capitalization doesn't matter.) - Copy your EFI application to a file under
EFI/BOOT
. The file name is specific to the architecture. For example, on x86_64 the file name must beBOOTX64.EFI
. See the boot files table for other architectures.
The details of exactly how to do these steps will vary depending on your OS.
Note that most UEFI implementations do not strictly require GPT partitioning or the EFI System partition GUID; they will look for any FAT partition with the appropriate directory structure. This is not required however; the UEFI Specification says "UEFI implementations may allow the use of conforming FAT partitions which do not use the ESP GUID."
Example on Linux
Warning: these operations are destructive! Do not run these commands on a disk if you care about the data it contains.
# Create the GPT, create a 9MB partition starting at 1MB, and set the
# partition type to EFI System.
sgdisk \
--clear \
--new=1:1M:10M \
--typecode=1:C12A7328-F81F-11D2-BA4B-00A0C93EC93B \
/path/to/disk
# Format the partition as FAT.
mkfs.fat /path/to/disk_partition
# Mount the partition.
mkdir esp
mount /path/to/disk_partition esp
# Create the boot directory.
mkdir esp/EFI/BOOT
# Copy in the boot executable.
cp /path/to/your-executable.efi esp/EFI/BOOT/BOOTX64.EFI
Booting the USB
Insert the USB into the target computer. Reboot the machine, then press the one-time boot key. Which key to press depends on the vendor. For example, Dell uses F12, HP uses F9, and on Macs you hold down the Option key.
Once the one-time boot menu appears, select your USB drive and press enter.
How-to
This chapter contains practical how-to guides.
Using Protocols
The open a protocol, you must first get a handle, then open a protocol on that handle. See Handles and Protocols for an overview of what these terms mean.
To get a handle you can use:
BootServices::locate_handle_buffer
: this can be used to get all available handles, or just the handles that support a particular protocol.BootServices::locate_handle
: the same aslocate_handle_buffer
, but you provide the slice that stores the handles.BootServices::locate_device_path
: find a handle by Device Path.
Once you have obtained a handle, use
BootServices::open_protocol_exclusive
to open a protocol on that
handle. This returns a ScopedProtocol
, which automatically closes
the protocol when dropped.
Using BootServices::open_protocol_exclusive
is the safest way to
open a protocol, but in some cases a protocol cannot be opened in
exclusive mode. The unsafe
BootServices::open_protocol
can be used
in that case.
Example
For this example we'll look at a program that opens a couple different
protocols. This program opens the LoadedImage
protocol to get
information about an executable (the currently-running program in this
case). It also opens the DevicePathToText
protocol to get the file
system path that the program was launched from.
We'll walk through the details of this program shortly, but first here's the whole thing:
#![no_main] #![no_std] use log::info; use uefi::prelude::*; use uefi::proto::device_path::text::{ AllowShortcuts, DevicePathToText, DisplayOnly, }; use uefi::proto::loaded_image::LoadedImage; use uefi::table::boot::SearchType; use uefi::{Identify, Result}; #[entry] fn main(image_handle: Handle, mut system_table: SystemTable<Boot>) -> Status { uefi::helpers::init(&mut system_table).unwrap(); let boot_services = system_table.boot_services(); print_image_path(boot_services).unwrap(); boot_services.stall(10_000_000); Status::SUCCESS } fn print_image_path(boot_services: &BootServices) -> Result { let loaded_image = boot_services .open_protocol_exclusive::<LoadedImage>(boot_services.image_handle())?; let device_path_to_text_handle = *boot_services .locate_handle_buffer(SearchType::ByProtocol(&DevicePathToText::GUID))? .first() .expect("DevicePathToText is missing"); let device_path_to_text = boot_services .open_protocol_exclusive::<DevicePathToText>( device_path_to_text_handle, )?; let image_device_path = loaded_image.file_path().expect("File path is not set"); let image_device_path_text = device_path_to_text .convert_device_path_to_text( boot_services, image_device_path, DisplayOnly(true), AllowShortcuts(false), ) .expect("convert_device_path_to_text failed"); info!("Image path: {}", &*image_device_path_text); Ok(()) }
When the program is run it will print something like this:
[ INFO]: example.rs@058: Image path: \EFI\BOOT\BOOTX64.EFI
Walkthrough
The main
function looks much like the "Hello world!" example. It
sets up logging, calls print_image_path
, and pauses for ten seconds to
give you time to read the output. Let's look at print_image_path
:
#![allow(unused)] fn main() { fn print_image_path(boot_services: &BootServices) -> Result { }
The return type is a uefi::Result
, which is a Result
alias that
combines uefi::Status
with the error data. Both the success and
error data types are ()
by default.
The function starts by opening the LoadedImage
protocol:
#![allow(unused)] fn main() { let loaded_image = boot_services .open_protocol_exclusive::<LoadedImage>(boot_services.image_handle())?; }
The open_protocol_exclusive
method takes a type parameter, which is
the type of Protocol
you want to open (LoadedImage
in this
case). It also takes one regular argument of type Handle
. For this
example we want the handle of the currently-running image, which was
passed in as the first argument to main
. The handle is conveniently
accessible through BootServices::image_handle
, so we use that here.
Next the program opens the DevicePathToText
protocol:
#![allow(unused)] fn main() { let device_path_to_text_handle = *boot_services .locate_handle_buffer(SearchType::ByProtocol(&DevicePathToText::GUID))? .first() .expect("DevicePathToText is missing"); let device_path_to_text = boot_services .open_protocol_exclusive::<DevicePathToText>( device_path_to_text_handle, )?; }
This protocol isn't available for the image_handle
, so we start by
using locate_handle_buffer
to find all handles that support
DevicePathToText
. We only need one handle though, so we call first()
and discard the rest. Then we call open_protocol_exclusive
again. It
looks more or less like the previous time, but with DevicePathToText
as the type parameter and device_path_to_text_handle
as the handle.
Now that we have both protocols open, we can use them together to get the program's path and convert it to text:
#![allow(unused)] fn main() { let image_device_path = loaded_image.file_path().expect("File path is not set"); let image_device_path_text = device_path_to_text .convert_device_path_to_text( boot_services, image_device_path, DisplayOnly(true), AllowShortcuts(false), ) .expect("convert_device_path_to_text failed"); info!("Image path: {}", &*image_device_path_text); Ok(()) } }
Since protocols do a wide range of different things, the methods available to call are very specific to each individual protocol. The best places to find out what each protocol can do are the uefi-rs reference documentation and the UEFI Specification.
Drawing to the Screen
This example shows how to draw to the screen using the graphics output protocol. The code will a SierpiĆski triangle using the "chaos game" method.
The core abstraction used here is a linear buffer:
#![allow(unused)] fn main() { struct Buffer { width: usize, height: usize, pixels: Vec<BltPixel>, } impl Buffer { /// Create a new `Buffer`. fn new(width: usize, height: usize) -> Self { Buffer { width, height, pixels: vec![BltPixel::new(0, 0, 0); width * height], } } /// Get a single pixel. fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { self.pixels.get_mut(y * self.width + x) } /// Blit the buffer to the framebuffer. fn blit(&self, gop: &mut GraphicsOutput) -> Result { gop.blt(BltOp::BufferToVideo { buffer: &self.pixels, src: BltRegion::Full, dest: (0, 0), dims: (self.width, self.height), }) } } }
This Buffer
type stores a Vec
of BltPixel
s, which are BGRX
32-bit pixels (8 bites each for blue, green, and red, followed by 8
unused bits of padding). We use the pixel
method to alter a single
pixel at a time. This is often not an efficient method; for more complex
graphics you could use a crate like embedded-graphics
.
The Buffer::blit
method calls the graphics output protocol's blt
method to copy the buffer to the screen.
Most of the rest of the code is just implementing the algorithm for drawing the fractal. Here's the full example:
#![no_main] #![no_std] extern crate alloc; use alloc::vec; use alloc::vec::Vec; use core::mem; use uefi::prelude::*; use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput}; use uefi::proto::rng::Rng; use uefi::Result; #[derive(Clone, Copy)] struct Point { x: f32, y: f32, } impl Point { fn new(x: f32, y: f32) -> Self { Self { x, y } } } struct Buffer { width: usize, height: usize, pixels: Vec<BltPixel>, } impl Buffer { /// Create a new `Buffer`. fn new(width: usize, height: usize) -> Self { Buffer { width, height, pixels: vec![BltPixel::new(0, 0, 0); width * height], } } /// Get a single pixel. fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { self.pixels.get_mut(y * self.width + x) } /// Blit the buffer to the framebuffer. fn blit(&self, gop: &mut GraphicsOutput) -> Result { gop.blt(BltOp::BufferToVideo { buffer: &self.pixels, src: BltRegion::Full, dest: (0, 0), dims: (self.width, self.height), }) } } /// Get a random `usize` value. fn get_random_usize(rng: &mut Rng) -> usize { let mut buf = [0; mem::size_of::<usize>()]; rng.get_rng(None, &mut buf).expect("get_rng failed"); usize::from_le_bytes(buf) } fn draw_sierpinski(bt: &BootServices) -> Result { // Open graphics output protocol. let gop_handle = bt.get_handle_for_protocol::<GraphicsOutput>()?; let mut gop = bt.open_protocol_exclusive::<GraphicsOutput>(gop_handle)?; // Open random number generator protocol. let rng_handle = bt.get_handle_for_protocol::<Rng>()?; let mut rng = bt.open_protocol_exclusive::<Rng>(rng_handle)?; // Create a buffer to draw into. let (width, height) = gop.current_mode_info().resolution(); let mut buffer = Buffer::new(width, height); // Initialize the buffer with a simple gradient background. for y in 0..height { let r = ((y as f32) / ((height - 1) as f32)) * 255.0; for x in 0..width { let g = ((x as f32) / ((width - 1) as f32)) * 255.0; let pixel = buffer.pixel(x, y).unwrap(); pixel.red = r as u8; pixel.green = g as u8; pixel.blue = 255; } } let size = Point::new(width as f32, height as f32); // Define the vertices of a big triangle. let border = 20.0; let triangle = [ Point::new(size.x / 2.0, border), Point::new(border, size.y - border), Point::new(size.x - border, size.y - border), ]; // `p` is the point to draw. Start at the center of the triangle. let mut p = Point::new(size.x / 2.0, size.y / 2.0); // Loop forever, drawing the frame after each new point is changed. loop { // Choose one of the triangle's vertices at random. let v = triangle[get_random_usize(&mut rng) % 3]; // Move `p` halfway to the chosen vertex. p.x = (p.x + v.x) * 0.5; p.y = (p.y + v.y) * 0.5; // Set `p` to black. let pixel = buffer.pixel(p.x as usize, p.y as usize).unwrap(); pixel.red = 0; pixel.green = 100; pixel.blue = 0; // Draw the buffer to the screen. buffer.blit(&mut gop)?; } } #[entry] fn main(_handle: Handle, mut system_table: SystemTable<Boot>) -> Status { uefi::helpers::init(&mut system_table).unwrap(); let bt = system_table.boot_services(); draw_sierpinski(bt).unwrap(); Status::SUCCESS }
You can run this example from the uefi-rs repository with:
cargo xtask run --example sierpinski
Building drivers
There are three types of UEFI images:
- Application
- Boot service driver
- Runtime driver
By default, Rust's UEFI targets produce applications. This can be
changed by passing a subsystem
linker flag in rustflags
and setting the
value to efi_boot_service_driver
or efi_runtime_driver
.
Example:
# In .cargo/config.toml:
[build]
rustflags = ["-C", "link-args=/subsystem:efi_runtime_driver"]
Concepts
The canonical source of information about UEFI is the UEFI specification.
The specification is huge (currently nearly 2500 pages). Much of that
content relates to optional services, understanding of which is not
critical to understanding UEFI as a whole. This chapter summarizes some
of the more important UEFI concepts and links to the relevant uefi-rs
documentation.
Boot Stages
A UEFI system goes through several distinct phases during the boot process.
- Platform Initialization. This early-boot phase is mostly outside
the scope of
uefi-rs
. It is described by the UEFI Platform Initialization Specification, which is separate from the main UEFI Specification. - Boot Services. This is when UEFI drivers and applications are
loaded. Both the
BootServices
andRuntimeServices
tables are accessible. This stage typically culminates in running a bootloader that loads an operating system. The stage ends whenSystemTable::exit_boot_services
is called, putting the system in Runtime mode. - Runtime. This stage is typically active when running an operating
system such as Linux or Windows. UEFI functionality is much more
limited in the Runtime mode. The
BootServices
table is no longer accessible, but theRuntimeServices
table is still available. Once the system is in Runtime mode, it cannot return to the Boot Services stage until after a system reset.
Tables
UEFI has a few table structures. These tables are how you get access to UEFI services.
SystemTable
(EFI_SYSTEM_TABLE
in the specification) is the
top-level table that provides access to the other tables.
BootServices
(EFI_BOOT_SERVICES
in the specification) provides
access to a wide array of services such as memory allocation, executable
loading, and optional extension interfaces called protocols. This table
is only accessible while in the Boot Services stage.
RuntimeServices
(EFI_RUNTIME_SERVICES
in the specification)
provides access to a fairly limited set of services, including variable
storage, system time, and virtual-memory mapping. This table is
accessible during both the Boot Services and Runtime stages.
When writing a UEFI application, you get access to the system table from
one of the arguments to the main
entry point:
fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status;
Then use SystemTable::boot_services
and
SystemTable::runtime_services
to get access to the other
tables. Once SystemTable::exit_boot_services
is called, the original
system table is consumed and a new system table is returned that only
provides access to the RuntimeServices
table.
GUID
GUID is short for Globally Unique Identifier. A GUID is always 16 bytes,
and has a standard string representation format that looks like this:
313b0d7c-fed4-4de7-99ed-2fe48874a410
. The details of the GUID format
aren't too important, but be aware that the actual byte representation
is not in the same order as the string representation because the first
three fields are little-endian. For the most part you can treat GUIDs as
opaque identifiers.
The UEFI specification uses GUIDs all over the place. GUIDs are used to
identify protocols, disk partitions, variable groupings, and much
more. In uefi-rs
, GUIDs are represented by the Guid
type.
Handles and Protocols
Handles and protocols are at the core of what makes UEFI extensible. Together they are the mechanism by which UEFI can adapt to a wide array of hardware and boot conditions, while still providing a consistent interface to drivers and applications.
Handles
Handles represent resources. A resource might be a physical device such as a disk drive or USB device, or something less tangible like a loaded executable.
A Handle is an opaque pointer, so you can't do anything with it directly. To operate on a handle you have to open a protocol.
Protocols
Protocols are interfaces that provide functions to interact with a resource. For example, the BlockIO protocol provides functions to read and write to block IO devices.
Protocols are only available during the Boot Services stage; you can't access them during the Runtime stage.
The UEFI Specification defines a very large number of protocols. Because
protocols are inherently very diverse, the best place to learn about
individual protocols is the UEFI Specification. There are many
chapters covering various protocols. Not all of these protocols are
wrapped by uefi-rs
yet (contributions welcome!) but many of the most
commonly useful ones are.
See the Using Protocols how-to for details of the uefi-rs
API for
interacting with protocols.
Device Paths
A device path is a very flexible packed data structure for storing paths to many kinds of device. Note that these device paths are not the same thing as file system paths, although they can include file system paths. Like handles, device paths can be used to uniquely identify resources such as consoles, mice, disks, partitions, and more. Unlike handles, which are essentially opaque pointers, device paths are variable-length structures that contain parseable information.
The uefi::proto::device_path
module documentation describes the
details of how device paths are encoded.
Device paths can also be converted to and from human-readable text representations that look like this:
PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)
See uefi::proto::device_path::text
for details.
Variables
UEFI provides fairly flexible key/value variable storage.
Each variable is identified by a key consisting of a UCS-2 null-terminated name plus a vendor GUID. The vendor GUID serves as a namespace for variables so that different vendors don't accidentally overwrite or misinterpret another vendor's variable if they happen to have the same name.
The data stored in each variable is an arbitrary byte array.
Attributes
Each variable has attributes (represented as bit flags) associated with it that affect how it is stored and how it can be accessed.
If the BOOTSERVICE_ACCESS
and RUNTIME_ACCESS
bits are set, the
variable can be accessed during both the Boot Services and Runtime
stages. If only BOOTSERVICE_ACCESS
is set then the variable can
neither be read nor written to after exiting boot services.
Another important attribute is the NON_VOLATILE
bit. If this bit is
not set, the variable will be stored in normal memory and will not
persist across a power cycle. If this bit is set, the variable will be
stored in special non-volatile memory. You should be careful about
writing variables of this type, because the non-volatile storage can be
very limited in size. There have been cases where a vendor's poor UEFI
implementation caused the machine not too boot once the storage became
too full. Even figuring out how much space is in use can be tricky due
to deletion being implemented via garbage collection. Matthew Garret's
article "Dealing with UEFI non-volatile memory quirks" has more details.
Most of the other attributes relate to authenticated variables, which can be used to prevent changes to a variable by unauthorized programs.
GPT
GPT is short for GUID Partition Table. It's a more modern alternative to MBR (master boot record) partition tables. Although it's defined in the UEFI specification, it often gets used on non-UEFI systems too. There are a couple big advantages of using GPT over MBR:
- It has a relatively clear and precise standard, unlike MBR where implementations often just try to match what other implementations do.
- It supports very large disks and very large numbers of partitions.
A GPT disk contains a primary header near the beginning of the disk, followed by a partition entry array. The header and partition entry array have a secondary copy at the end of the disk for redundancy. The partition entry arrays contain structures that describe each partition, including a GUID to identify the individual partition, a partition type GUID to indicate the purpose of the partition, and start/end block addresses. In between the entry arrays is the actual partition data.
System partition
The system partition is UEFI's version of a bootable partition. The
system partition is sometimes called the ESP, or EFI System
Partition. It is identified by a partition type of
c12a7328-f81f-11d2-ba4b-00a0c93ec93b
. The system partition always
contains a FAT file system. There are various standardized paths that
can exist within the file system, and of particular importance are the
boot files. These are the files that UEFI will try to boot from by
default (in the absence of a different boot configuration set through
special UEFI variables).
Boot files are under \EFI\BOOT
, and are named BOOT<ARCH>.efi
, where
<ARCH>
is a short architecture name.
Architecture | File name |
---|---|
Intel 32-bit | BOOTIA32.EFI |
X86_64 | BOOTX64.EFI |
Itanium | BOOTIA64.EFI |
AArch32 | BOOTARM.EFI |
AArch64 | BOOTAA64.EFI |
RISC-V 32-bit | BOOTRISCV32.EFI |
RISC-V 64-bit | BOOTRISCV64.EFI |
RISC-V 128-bit | BOOTRISCV128.EFI |