Rust development for UEFI

When developing any application, my first step is to create a setup that enables me to rapidly debug the written code. This is of course trivial for regular applications, UEFI apps need a little bit more attention, a simple cargo run won't do it here.

So this blog documents my attempts at compiling an EFI Application in rust as well as creating a suitable build pipeline to deploy the application on a virtual machine. Since compilation for UEFI is a nightly feature of the rust ecosystem the usual issues arise and i wanted to document what it takes to set up an UEFI app for compilation. Since the nightly builds are getting updated somewhat frequently, your experience may vary.

The first rust code i wanted to build is the "smallest #![no_std] Program" (that still compiles) and can be found in the Embedonomicon.

1
2
3
4
5
6
7
8
9
#![no_main]
#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

Additionally i added a .cargo/config.toml and added some options to it. I would really like to explain why i did that, but i do not remember for the life of me. Anyway it works, so i just left it as it is ¯_(ツ)_/¯

[unstable]
build-std = ["core", "compiler_builtins", "alloc"]

So with the code good to go, we just have to compile it! The uefi-rs crate lists the following 3 commands to build an EFI Application:

rustup toolchain install nightly
rustup component add --toolchain nightly rust-src
cargo +nightly build --target x86_64-unknown-uefi

But of course, nothing is ever this easy when dealing with experimental features. The first thing is that the compiler complains that no precompiled standard library for this target exists, so we have to add -Z build-std, making the command cargo +nightly build -Z build-std --target x86_64-unknown-uefi. This makes the really juice error occur! See below for an example of the failing build.

First compilation error

This issue is also mentioned on github (https://github.com/rust-lang/rust/issues/101071) and seems to be related to the debug information. But to fix it we don't really need to know why it happens, just how to fix it. Thankfully the github issue also provides a solution, some options need to be added to the target configuration. In order to do that i dumped the normal x86_64-unknown-uefi target with the following command.

rustc +nightly -Z unstable-options --print target-spec-json --target x86_64-unknown-uefi

This dumps the following configuration.

{
"abi-return-struct-as-int": true,
"allows-weak-linkage": false,
"arch": "x86_64",
"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",
"features": "-mmx,-sse,+soft-float",
"is-builtin": true,
"is-like-msvc": true,
"is-like-windows": true,
"linker": "rust-lld",
"linker-flavor": "lld-link",
"linker-is-gnu": false,
"lld-flavor": "link",
"llvm-target": "x86_64-unknown-windows",
"max-atomic-width": 64,
"os": "uefi",
"panic-strategy": "abort",
"pre-link-args": {
    "lld-link": [
    "/NOLOGO",
    "/entry:efi_main",
    "/subsystem:efi_application"
    ],
    "msvc": [
    "/NOLOGO",
    "/entry:efi_main",
    "/subsystem:efi_application"
    ]
},
"singlethread": true,
"split-debuginfo": "packed",
"stack-probes": {
    "kind": "call"
},
"supported-split-debuginfo": [
    "packed"
],
"target-pointer-width": "64"
}

Now we just have to copy this output to a file, i named it "x86_64-none-efi.json". To implement the fix mentioned in the github issue, remove "is-builtin" and add "debuginfo-kind": "pdb". The former might not be strictly necessary, but since our modified configuration surely is not a "builtin" it should be removed nontheless.

Now we have to tweak our build command to use the new target file: cargo +nightly build -Z build-std --target x86_64-none-efi.json. This promptly leads to the next compilation error.

Second compilation error

But here another github issue comes to our aid: https://github.com/rust-lang/rust/pull/98457. If we manually add all the fixes from this pull request, it should be fixed. So locate the correct folder and apply the proposed fixes. Note that these files reside in ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library on a regular linux install of rust.

Now finally, the build works without any compilation errors!

Note that i now added an efi-main function with an endless loop to the rust source file, so we could observe the VM freezing when the executable runs properly. (Note that this is not entirely true, because the panic handler also loops infinitely, but it will suffice for the moment)

#[no_mangle]
extern "C" fn efi_main() {
    loop{}
}

So, with our finished efi executable we just need to copy it to an iso file and execute it from the efi shell. Any UEFI VM will suffice for that, in fact a standard windows install on VMWare fulfills this condition.

The following commands will build an iso image from the compiled file (adjust the paths for your project).

dd if=/dev/zero of=/tmp/iso.bin bs=1k count=2880 2>/dev/null
mformat -i /tmp/iso.bin -f 2880 ::  2>/dev/null
mcopy -o -i /tmp/iso.bin target/x86_64-none-efi/debug/uefi-test.efi ::/  2>/dev/null
mkisofs -eltorito-boot iso.bin -no-emul-boot -o iso/uefi-test.iso /tmp/iso.bin  2>/dev/null

Now we just mount this iso in the VM. This can be achieved by selecting the CD/DVD entry in the virtual machine settings and configuring it as shown below.

VM preferences for iso mounting

The next step is to enter the boot menu to actually launch our uefi binary. To do this just repeatedly press the escape key while the VM is booting. This should bring you to the menu which should look similar to the following image.

VMware boot menu

The right entry is already pre-selected in the screenshot (EFI Internal Shell). Just hit enter and enter the following commands when the shell is ready.

fs1:             # to change to the ISO, this assumes that only one iso is mounted, otherwise it might be fs2, or higher
ls               # to show the contents of the iso and validate we are in the right place
./uefi-test.efi  # note that you will have to type that out, as there is no tab completion in this shell

The result should look something like this:

Uefi shell

If everything worked, the machine should not be returning to the UEFI shell and rather be stuck executing the program. I know that this result is kinda underwhelming, but keep in mind that we just compiled an UEFI binary from scratch and it runs flawlessly, thats an achievement in itself. Filling this binary with actual functionality will likely be the topic of a future article.