15 KiB
+++ title = "UEFI Booting" path = "booting/uefi" date = 0000-01-01 template = "edition-3/page.html"
[extra] hide_next_prev = true icon = '''
''' +++This post is an addendum to our main Booting post. It explains how to create a basic UEFI bootloader from scratch.
This blog is openly developed on GitHub. If you have any problems or questions, please open an issue there. You can also leave comments at the bottom.
Minimal UEFI App
We start by creating a new cargo project with a Cargo.toml and a src/main.rs:
# in Cargo.toml
[package]
name = "uefi_app"
version = "0.1.0"
authors = ["Your Name <your-email@example.com>"]
edition = "2018"
[dependencies]
This uefi_app project is independent of the OS kernel created in the Booting, so we use a separate directory.
In the src/main.rs, we create a minimal no_std executable as shown in the Minimal Kernel post:
// in src/main.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
The #![no_std] attribute disables the linking of the Rust standard library, which is not available on bare metal. The #![no_main] attribute, we disable the normal entry point function that based on the C runtime. The #[panic_handler] attribute specifies which function should be called when a panic occurs.
Next, we create an entry point function named efi_main:
// in src/main.rs
#![feature(abi_efiapi)]
use core::ffi::c_void;
#[no_mangle]
pub extern "efiapi" fn efi_main(
image: *mut c_void,
system_table: *const c_void,
) -> usize {
loop {}
}
This function signature is standardized by the UEFI specification, which is available in PDF form on uefi.org. You can find the signature of the entry point function in section 4.1. Since UEFI also defines a specific calling convention (in section 2.3), we set the efiapi calling convention for our function. Since support for this calling function is still unstable in Rust, we need to add #![feature(abi_efiapi)] at the very top of our file.
The function takes two arguments: an image handle and a system table. The image handle is a firmware-allocated handle that identifies the UEFI image. The system table contains some input and output handles and provides access to various functions provided by the UEFI firmware. The function returns an EFI_STATUS integer to signal whether the function was successful. It is normally only returned by UEFI apps that are not bootloaders, e.g. UEFI drivers or apps that are launched manually from the UEFI shell. Bootloaders typically pass control to a OS kernel and never return.
UEFI Target
For our minimal kernel, we needed to create a custom target because none of the officially supported targets was suitable. For our UEFI application we are more lucky: Rust has built-in support for a x86_64-unknown-uefi target, which we can use without problems.
If you're curious, you can query the JSON specification of the target with the following command:
rustc +nightly --print target-spec-json -Z unstable-options --target x86_64-unknown-uefi
This outputs looks something like the following:
{
"abi-return-struct-as-int": true,
"allows-weak-linkage": false,
"arch": "x86_64",
"code-model": "large",
"cpu": "x86-64",
"data-layout": "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
"disable-redzone": true,
"emit-debug-gdb-scripts": false,
"exe-suffix": ".efi",
"executables": true,
"features": "-mmx,-sse,+soft-float",
"is-builtin": true,
"is-like-msvc": true,
"is-like-windows": true,
"linker": "rust-lld",
"linker-flavor": "lld-link",
"lld-flavor": "link",
"llvm-target": "x86_64-unknown-windows",
"max-atomic-width": 64,
"os": "uefi",
"panic-strategy": "abort",
"pre-link-args": {
"lld-link": [
"/NOLOGO",
"/NXCOMPAT",
"/entry:efi_main",
"/subsystem:efi_application"
],
"msvc": [
"/NOLOGO",
"/NXCOMPAT",
"/entry:efi_main",
"/subsystem:efi_application"
]
},
"singlethread": true,
"split-debuginfo": "packed",
"stack-probes": {
"kind": "call"
},
"target-pointer-width": "64"
}
From the output we can derive multiple properties of the target:
- The
exe-suffixis.efi, which means that all executables compiled for this target have the suffix.efi. - As for our kernel target, both the redzone and SSE are disabled.
- The
is-like-windowsis an indicator that the target uses the conventions of Windows world, e.g. PE instead of ELF executables. - The LLD linker is used, which means that we don't have to install any additional linker even when compiling on non-Windows systems.
- Like for all (most?) bare-metal targets, the
panic-strategyis set toabortto disable unwinding. - Various linker arguments are specified. For example, the
/entryargument sets the name of the entry point function. This is the reason that we named our entry point functionefi_mainand applied the#[no_mangle]attribute above.
If you're interested in understanding all these fields, check out the docs for Rust's internal Target and TargetOptions types. These are the types that the above JSON is converted to.
Building
Even though the x86_64-unknown-uefi target is built-in, there are no precompiled versions of the core library available for it. This means that we need to use cargo's build-std feature as described in the Minimal Kernel post.
A nightly Rust compiler is required for building, so we need to set up a rustup override for the directory. We can do this either by running a [rustup ovrride command] or by adding a rust-toolchain file.
The full build command looks like this:
cargo build --target x86_64-unknown-uefi -Z build-std=core \
-Z build-std-features=compiler-builtins-mem
This results in a uefi_app.efi file in our x86_64-unknown-uefi/debug folder. Congratulations! We just created our own minimal UEFI app.
Bootable Disk Image
To make our minimal UEFI app bootable, we need to create a new GPT disk image with a EFI system partition. On that partition, we need to put our .efi file under efi\boot\bootx64.efi. Then the UEFI firmware should automatically detect and load it when we boot from the corresponding disk. See the section about the UEFI boot process in the Booting post for more details.
To create this disk image, we create a new disk_image executable:
> cargo new --bin disk_image
This creates a new cargo project in a disk_image subdirectory. To share the target folder and Cargo.lock file with our uefi_app project, we set up a cargo workspace:
# in Cargo.toml
[workspace]
members = ["disk_image"]
FAT Partition
The first step is to create an EFI system partition formatted with the FAT file system. The reason for using FAT is that this is the only file system that the UEFI standard requires. In practice, most UEFI firmware implementations also support the NTFS filesystem, but we can't rely on that since this is not required by the standard.
To create a new FAT file system, we use the fatfs crate:
# in disk_image/Cargo.toml
[dependencies]
fatfs = "0.3.5"
TODO
GPT Disk Image
Running
Now we can run our disk_image executable to create the bootable disk image:
TODO
This results in a .fat and a .img file next to our .efi executable. These files can be launched in QEMU and on real hardware as described in the main Booting post. However, we don't see anything on the screen yet since we only loop {} in our efi_main:
TODO screenshot
Let's fix this by using the uefi crate.
The uefi Crate
In order to print something to the screen, we need to call some functions provided by the UEFI firmware. These functions can be invoked through the system_table argument passed to our efi_main function. This table provides [function pointers] for all kinds of functionality, including access to the screen, disk, or network.
Since the system table has a standardized format that is identical on all systems, it makes sense to create an abstraction for it. This is what the uefi crate does. It provides a [SystemTable] type that abstracts the UEFI system table functions as normal Rust methods. It is not complete, but the most important functions are all available.
To use the crate, we first add it as a dependency in our Cargo.toml:
# TODO
Now we can change the types of the image and system_table arguments in our efi_main declaration:
// TODO
Since the Rust compiler is not able to typecheck the function signature of the entry point function, we could accidentally use the wrong signature here. To prevent this (and the resulting undefined behavior), the uefi crate provides an entry macro to enforce the correct signature. To use it, we change our main.rs like this:
// TODO
Now we can safely use the types provided by the uefi crate.
Printing to Screen
The UEFI standard supports multiple interfaces for printing to the screen. The most simple one is the text-based TODO. To use it, ... TODO.
The text-based output is only available before exiting UEFI boot services. TODO explain
The UEFI standard also supports a pixel-based framebuffer for screen output through the GOP protocol. This framebuffer also stays available after exiting boot services, so it makes sense to set it up before switching to the kernel. The protocol can be set up like this:
TODO
See the [TODO] post for how to draw and render text using this framebuffer.
Memory Allocation
Physical Memory Map
APIC Base
Loading the Kernel
We already saw how to set up a framebuffer for screen output and query the physical memory map and the APIC base register address. This is already all the system information that a basic kernel needs from the bootloader.
The next step is to load the kernel executable. This involves loading the kernel from disk into memory, allocating a stack for it, and setting up a new page table hierarchy to properly map it to virtual memory.
Loading it from Disk
One approach for including our kernel could be to place it in the FAT partition created by our disk_image crate. Then we could use the TODO protocol of the uefi crate to load it from disk into memory.
To keep things simple, we will use a different appoach here. Instead of loading the kernel separately, we place its bytes as a static variable inside our bootloader executable. This way, the UEFI firmware directly loads it into memory when launching the bootloader. To implement this, we can use the [include_bytes] macro of Rust's core library:
// TODO
Parsing the Kernel
Now that we have our kernel executable in memory, we need to parse it. In the following, we assume that the kernel uses the ELF executable format, which is popular in the Linux world. This is also the excutable format that the kernel created in this blog series uses.
The ELF format is structured like this:
TODO
The various headers are useful in different situations. For loading the executable into memory, the program header is most relevant. It looks like this:
TODO
TODO: mention readelf/objdump/etc for looking at program header
There are already a number of ELF parsing crates in the Rust ecosystem, so we don't need to create our own. In the following, we will use the [xmas_elf] crate, but other crates might work equally well.
TODO: load program segements and print them
TODO: .bss section -> mem_size might be larger than file_size
Page Table Mappings
TODO:
- create new page table - map each segment - special-case: mem_size > file_size
Create a Stack
Switching to Kernel
Challenges
Boot Information
- Physical Memory