A couple nights ago I was looking over the UEFI spec, and I realized it shouldn’t be too hard to write UEFI applications in Rust. It turns out, you can, and here I will tell you how.
The thing that surprises me most about UEFI is that it now appears possible to boot your machine without ever writing a single line of assembly language. Booting used to require this tedious process of starting out in 16-bit real mode, then transitioning into 32-bit protected mode and then doing it all over again to get into 64-bit mode. UEFI firmwares, on the other hand, will happily load an executable file that you give it and run your code in 64-bit mode from the start. Your startup function receives a pointer to some functions that give you basic console support, as well as an API to access richer features. From a productivity standpoint, this seems like a win, but I also miss the sorcery you used to have to do when you were programming at this level.
Booting to Rust is a lot like writing bindings to any C library, except that the linking process is a bit more involved.
An Overview of the UEFI Boot Process
I was trying to find the simplest way to get my code running. As such, I’m not sure I did this in the most official way, or that my terminology is totally correct, but here’s something that worked.
UEFI firmware works around this idea of applications. These could be things like a shell to help you troubleshoot a broken computer, but most of the apps seem to be bootloaders. As far as I can tell, a bootloader is just an app that transfers control over to an operating system rather than returning control to the firmware.
One of the things that confused me is that all of the example code I could find was written in C. I always thought code that runs this early in the boot process should be written in assembly language. The difference is that now a lot of this early boot code is handled by the firmware. Thus, you just have to run a program that looks kind of like this example from over on the OSDev wiki
1 2 3 4 5 6 7 8 9 10 11 12 13
I’m not super wild about all of the extra code this example uses from an EFI library, but fortunately the spec tells us what we need to know to recreate this. Now we can look at how to do a similar program in Rust.
UEFI in Rust
Much of the UEFI spec looks like reading C header files, since the API that UEFI exposes to applications is basically a C API. This is fantastic, because Rust has always had very good C interoperability. Basically, we just have to transcribe the various structs and function pointers and start writing our application. Here’s what the system table struct looks like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
It’s basically a transliteration of the C definition. However, we do start to see the advantages of using a higher level language like Rust. For example, we can mark the
Reserved field in
EFI_TABLE_HEADER as private to make sure nothing uses this when it’s not supposed to.
You’ll notice I left out a lot of fields from the structure. For now I’m just trying to get a simple Hello, World program running, which means all I need is text output. For the same reason, I leave the
EFI_SIMPLE_TEXT_INPUT_PROTOCOL struct completely unspecified:
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL, we define just enough to output a string.
1 2 3 4 5 6 7 8 9 10
One thing I like about this code is that all you have to do to access functions provided by the UEFI firmware is declare a function pointer. You can then call this function just like any other, although you must call from an unsafe block.
Now we have enough to write our UEFI entry point. The skeleton of it looks like this:
1 2 3 4 5
This function just does an infinite loop. The return type is probably not exactly correct, but we don’t need to worry about this since our function doesn’t return anyway. Building from this, we can fill in the function to print “Hello, World!”
Ideally, we’d just do
SystemTable.ConOut.OutputString("Hello, World!"). Unfortunately, it’s not quite that simple. One problem is that Rust represents strings as UTF-8, while UEFI’s console output needs UTF-16 strings. We’ll deal with this for now by rather verbosely making an array of
u16s where each element is filled with a character casted to
u16, such as
'H' as u16. We also need to be sure we set the last element to 0, and we should add both the carriage return (
'\r') and linefeed (
'\n') characters. Once we have this array, we use
transmute to get a
*u16 that points to its contents.
We’re working with a lot of unsafe pointers, so we’ll put the whole body of
efi_main in an unsafe block. We’ll also have to be rather explicit about where we are dereferencing our pointers.
Here’s the code to print out hello world:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
We had to define
transmute as a Rust intrinsic directly, rather than using the version that’s in Rust’s standard library. The reason is that we can’t use Rust’s standard library because we don’t even have an operating system.
Now that we have our program, we need to figure out how to run it.
Rust generally assumes you are writing programs that you want to run on your own machine, meaning it generates binaries in the format that the host operating system expects. On Linux, for example, these are ELF files. According to the UEFI spec, we need a PE32+ file, and it needs to be marked as using subsystem 10, meaning it’s a UEFI Application. Fortunately, the Linux linker can generate nearly any binary format known to man.
We’ll have to tell Rust to only compile our program and not link it,
and then we’ll write the linker command ourselves. Let’s say our
program was in a file called
boot.rs. You can see my
compile it using this command:
This creates a file called
boot.o, which we can then link.
My linker didn’t support the right output format by default, so I had
to install a cross-linker. I was running Gentoo, so I got this by
sudo crossdev -s0 --target x86_64-efi-pe.
Once we have an appropriate linker, we build our PE file as follows:
That’s quite a mouthful. Let’s break down the options.
x86_64-efi-pe-ld– this is what
crossdevcalled my custom linker.
--oformat pei-x86-64– Generate a PE32+ file containing 64-bit code.
--subsystem 10– set the subsystem to 10. This tells the firmware that this is a UEFI application.
-pie– generate position independent code. This is important. This program runs before virtual memory is enabled, so the firmware might have to relocate your program if the addresses you requested were already in use.
-e efi_main– set the entry point to
boot.o– this is the file we are linking.
-o boot.efi– call the result
I also had to add the
#[no_mangle] attribute to
efi_main in the
Rust code so that Rust would not use a different symbol name in the
final object code.
The first time I ran this, the linker complained that
was missing. The easiest way to deal with this is to add the
#[no_split_stack] attribute to whatever functions report that symbol
as missing, which in our case is
efi_main. This attribute prevents
LLVM from inserting the stack safety checks.
When all is said and done,
file reports that we have the correct
kind of executable:
There’s one more issue we have to deal with, which is calling conventions. Calling conventions describe rules for where function arguments go when you call a procedure. For example, some calling conventions say all arguments go on the stack, while others pass some parameters through registers. Rust purposefully leaves its calling convention undefined for now, so we don’t really know what’s going to happen. Let’s take a look at some assembly and see what we get.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Let’s work backwards through this to see where this code expects its arguments to come from. We know our function made one procedure call, which seems to be the
callq instruction on line 16. Line 16 calls the procedure pointed to by
rax, and we see this value is assigned in line 6. This reads the value at 8 bytes away from the
rsi register, which in turn came form 32 bytes offset from the original value of
rsi. If you work out the field offsets from the system table struct and the console output struct, we see that Rust assumed the pointer to the system table was in
The UEFI spec, however, says that the system table will be in
when it calls your entry point. The calling convention described in
that document for x86_64 happen to match the convention used by
Microsoft Windows. Rust didn’t support this convention by default
(although it is probably the convention used when you run Rust on
Windows), but it was only a
few lines of code
to add support for it. Once we’ve made this change, we add
as the calling convention and see what the new assembly code looks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Line 5 shows us reading from
rdx, which is what we wanted.
Now the moment of truth.
The easiest way to boot this code is to stick it on a CD image. The EFI spec states that you can create a file called
/efi/boot/bootx64.efi and the firmware will try to use this file to boot from a removable media. I used
mkisofs to build an ISO image with the right format.
In theory I could burn this to a CD and boot an EFI-capable computer off it. I’m not quite brave enough to try this yet, so I used VirtualBox instead. I created a new VM, saying the operating system was an other/unknown 64-bit OS. In the system settings, I had to check the Enable EFI option as well. Then, I pointed the virtual CD-ROM device at my ISO image and booted the machine.
Okay, so it didn’t actually work on the first try. It took a while to get the PE file format right, the calling conventions correct, and all the pointers pointing to the right place. Fortunately, once that much is done, adding more functionality shouldn’t be hard.
So we’ve covered how to make a minimally interesting EFI application in Rust. The next step is to fill out the EFI API and start building more interesting applications, such as a full-fledged operating system. The initial working version of this code is available here. Since then, I’ve cleaned it up and added a few more pieces. You can see the latest version at the location below.