Writing UEFI application in Rust from scratch

This is an English version of my blog post. The original version is here (Japanese).

Recently, x86_64-unknown-uefi target was added into Rust mainline (https://github.com/rust-lang/rust/pull/56769).

So, I tried to write UEFI application with this update.

There exists an awesome crate, uefi-rs, which provides Rust interface for UEFI application.

However, this is my first time to write UEFI application, so to understand what happens in it, I didn’t use any existing crate.

What is UEFI?

UEFI is a software interface for platform firmware and replaces BIOS. For more detail, please refer Wikipedia, or there are a lot of existing examples developing in C.

The latest specification is available here: Unified Extensible Firmware Interface Forum
I followed version 2.7.

Preparation

The target architecture here is x86_64. First, we need to use nigtly Rust compiler including x86_64-unknown-uefi target. Also, to include sysroot libraries, cargo-xbuild is necessary.

$ rustup default nightly-2019-03-23
$ cargo install cargo-xbuild

To run your UEFI application in your local machine, you need to get qemu-system-x86_64 and its UEFI firmware OVMF.

To get OVMF, it is better to download the built image from https://www.kraxel.org/repos/. (You can build it from source code as well, but it will take a long time). You need to get OVMF_VARS-pure-efi.fd and OVMF_CODE-pure-efi.fd from the rpm for x64 in jenkins/edk2 directory.

Result

Here is my final result for hello world application on UEFI.

To run this application on qemu, you need to put OVMF_VARS.fd and OVMF_CODE.fd on the project root directory. Then, you can start the UEFI shell by following commands:

$ cargo xbuild --target x86_64-unknown-uefi
$ sh qemu-run.sh

And, you can start the application by following commands:

Shell > fs0:
FS0: \> uefi-practice.efi

Now, you will see “Hello World”.

How it works

efi_main function is the entry point. It correspond to EFI_IMAGE_ENTRY_POINT in the UEFI specification. This function takes two arguments and return EFI_STATUS. Please refer to the specification for more detail.

There are code snippets written in C to declare the interface types. We need to write down them in Rust. However, Rust and C ABI are different. So, we need to attach repr attribute to make sure the compatiblity. Rustonomicon provides the detail for it in Data Layout section.

My goal was to display “hello world”, so I use EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL to put text. This protocol includes reset ant output_string functions, so I used these. However, this protocol and other structs include more interface, actually. I declared only what I needed, and declared as usize for unused functions. It is hard to write down all interfaces.

In UEFI, we need to encode all string as UCS-2, however, in Rust the default string is encoded as UFT-8 and there is no support to convert it to UCS-2. Each character in UCS-2 is 16bit, so I prepared some u16 array buffer and convert the string on it (It is the same way as uefi-rs).

Finally, we need to provide panic_handler on no_std setting.

Conclusion

It is too hard to implement all UEFI specification. So, let’s use uefi-rs.