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
cargo add uefi --features logger,panic_handler
to your Cargo.toml
. The resulting Cargo.toml
should look like that:
[dependencies]
log = "0.4.21"
uefi = { version = "0.33.0", features = [ "panic_handler", "logger" ] }
Replace the contents of src/main.rs
with this:
#![no_main] #![no_std] use log::info; use uefi::prelude::*; #[entry] fn main() -> Status { uefi::helpers::init().unwrap(); info!("Hello world!"); boot::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 macros, modules, and 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() -> Status {
The main
function in a UEFI application takes no arguments and returns
a Status
, which is essentially a numeric error (or success) code
defined by UEFI. The main
function must be marked with the #[entry]
macro.
The first thing we do inside of main
is initialize the helpers
module, which initializes logging:
#![allow(unused)] fn main() { uefi::helpers::init().unwrap(); }
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!"); boot::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:
boot::locate_handle_buffer
: this can be used to get all available handles, or just the handles that support a particular protocol.boot::locate_handle
: the same aslocate_handle_buffer
, but you provide the slice that stores the handles.boot::locate_device_path
: find a handle by Device Path.
Once you have obtained a handle, use
boot::open_protocol_exclusive
to open a protocol on that
handle. This returns a ScopedProtocol
, which automatically closes
the protocol when dropped.
Using boot::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
boot::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::boot::{self, SearchType}; use uefi::prelude::*; use uefi::proto::device_path::text::{ AllowShortcuts, DevicePathToText, DisplayOnly, }; use uefi::proto::loaded_image::LoadedImage; use uefi::{Identify, Result}; #[entry] fn main() -> Status { uefi::helpers::init().unwrap(); print_image_path().unwrap(); boot::stall(10_000_000); Status::SUCCESS } fn print_image_path() -> Result { let loaded_image = boot::open_protocol_exclusive::<LoadedImage>(boot::image_handle())?; let device_path_to_text_handle = *boot::locate_handle_buffer( SearchType::ByProtocol(&DevicePathToText::GUID), )? .first() .expect("DevicePathToText is missing"); let device_path_to_text = boot::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( 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() -> 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::open_protocol_exclusive::<LoadedImage>(boot::image_handle())?; }
The boot::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, conveniently
accessible through boot::image_handle
.
Next the program opens the DevicePathToText
protocol:
#![allow(unused)] fn main() { let device_path_to_text_handle = *boot::locate_handle_buffer( SearchType::ByProtocol(&DevicePathToText::GUID), )? .first() .expect("DevicePathToText is missing"); let device_path_to_text = boot::open_protocol_exclusive::<DevicePathToText>( device_path_to_text_handle, )?; }
This protocol isn't available for the image_handle
, so we start by
using boot::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 boot::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( 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), }) } /// Update only a pixel to the framebuffer. fn blit_pixel( &self, gop: &mut GraphicsOutput, coords: (usize, usize), ) -> Result { gop.blt(BltOp::BufferToVideo { buffer: &self.pixels, src: BltRegion::SubRectangle { coords, px_stride: self.width, }, dest: coords, dims: (1, 1), }) } } }
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::{boot, 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), }) } /// Update only a pixel to the framebuffer. fn blit_pixel( &self, gop: &mut GraphicsOutput, coords: (usize, usize), ) -> Result { gop.blt(BltOp::BufferToVideo { buffer: &self.pixels, src: BltRegion::SubRectangle { coords, px_stride: self.width, }, dest: coords, dims: (1, 1), }) } } /// 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() -> Result { // Open graphics output protocol. let gop_handle = boot::get_handle_for_protocol::<GraphicsOutput>()?; let mut gop = boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)?; // Open random number generator protocol. let rng_handle = boot::get_handle_for_protocol::<Rng>()?; let mut rng = boot::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; } } // Draw background. buffer.blit(&mut gop)?; 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_pixel(&mut gop, (p.x as usize, p.y as usize))?; } } #[entry] fn main() -> Status { uefi::helpers::init().unwrap(); draw_sierpinski().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"]
Combining Rust std
with uefi
TL;DR
In Mid-2024, we recommend to stick to our normal guide. Use this document as guide and outlook for the future of UEFI and Rust.
About
Programs created with the uefi
crate are typically created with #![no_std]
and #![no_main]
. A #![no_std]
crate can use the core
and alloc
parts of
Rust's standard library, but not std
. A #![no_main]
executable does not use
the standard main entry point, and must define its own entry point; uefi
provides the #[entry]
macro for this purpose.
Rust has added partial support for building UEFI executables without
#![no_std]
and #![no_main]
, thus, the standard way. Some functionality
requires a nightly toolchain, they are gated by the uefi_std
feature (Rust
language feature, not uefi
crate feature). Follow the
tracking issue for details.
Code Example
Please refer to <repo>/uefi-std-example
to
see a specific example. The relevant main.rs
looks as follows:
// Note: In Rust 1.82.0-nightly and before, the `uefi_std` feature is // required for accessing `std::os::uefi::env::*`. The other default // functionality doesn't need a nightly toolchain (with Rust 1.80 and later), // but with that limited functionality you - currently - also can't integrate // the `uefi` crate. #![feature(uefi_std)] use std::os::uefi as uefi_std; use uefi::runtime::ResetType; use uefi::{Handle, Status}; /// Performs the necessary setup code for the `uefi` crate. fn setup_uefi_crate() { let st = uefi_std::env::system_table(); let ih = uefi_std::env::image_handle(); // Mandatory setup code for `uefi` crate. unsafe { uefi::table::set_system_table(st.as_ptr().cast()); let ih = Handle::from_ptr(ih.as_ptr().cast()).unwrap(); uefi::boot::set_image_handle(ih); } } fn main() { println!("Hello World from uefi_std"); setup_uefi_crate(); println!("UEFI-Version is {}", uefi::system::uefi_revision()); uefi::runtime::reset(ResetType::SHUTDOWN, Status::SUCCESS, None); }
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 the
uefi
crate. 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.
Functions in both the
boot
module andruntime
module can be used. This stage typically culminates in running a bootloader that loads an operating system. The stage ends whenboot::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. Functions in the
boot
module can no longer be used, but functions in theruntime
module are 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. In the specification and C API, EFI_SYSTEM_TABLE
is the top-level
table that provides access to the other tables, EFI_BOOT_SERVICES
and
EFI_RUNTIME_SERVICES
.
In the uefi
crate, these tables are modeled as modules rather than structs. The
functions in each module make use of a global pointer to the system table that
is set automatically by the entry
macro.
-
uefi::system
(EFI_SYSTEM_TABLE
in the specification) provides access to system information such as the firmware vendor and version. It can also be used to access stdout/stderr/stdin. -
uefi::boot
(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. Functions in this module can only be used while in the Boot Services stage. Afterexit_boot_services
has been called, these functions will panic. -
uefi::runtime
(EFI_RUNTIME_SERVICES
in the specification) provides access to a fairly limited set of services, including variable storage, system time, and virtual-memory mapping. Functions in this module are accessible during both the Boot Services and Runtime stages.
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 |