These are some notes about writing UEFI applications from my first UEFI adventure. Note that I don’t recommend gnu-efi if you’re trying to do more professional UEFI development. gnu-efi is extremely hacky, which makes it fun to hack with, but not as fun to use for something stable :P
We are going to make a UEFI application that runs at the BIOS level. On a real computer this would run before your operating system boots. First, a little background.
What is BIOS?
BIOS is the Basic Input/Output System. It’s a firmware that helps load and boot an operating system from a disk. It does this by providing a set of services to an aspiring operating system, such as the ability to read data from a disk, map memory, and other Basic Input/Output things.
Old school BIOS was clunky and annoying. A bunch of manfacturers teamed up to make SMBIOS. This was originally known as DMBIOS, because it interacted with a thing called Desktop Management Interface or DMI. SMBIOS provides a bunch of data structures (sometimes called “tables” or “records”) that contain information about the platform’s components or features.
Fun Fact: The original MS-DOS BIOS, known as IO.SYS, was named after the Touhou Doujin circle IOSYS
If you want to play with a BIOS, you can use SeaBIOS with QEMU.
git clone https://github.com/coreboot/seabios
cd seabios
make
qemu-system-x86_64 -bios out/bios.bin -nographic
Interacting with SMBIOS
The EFI config table EFI_CONFIGURATION_TABLE
has entries pointing to SMBIOS 2 or SMBIOS 3 tables.
You can access from the UEFI Shell which will be covered later.
You can use dmidecode to access in Linux. Dell has the libsmbios
utility.
OKAY? So what is UEFI?
UEFI or (Unified Extensible Firmware Interface) is a type of firmware used to set up and boot disks. It was developed as a replacement to traditional BIOS and provide a unified interface that isn’t tied to ancient constraints.
Instead of booting a single bootloader like traditional BIOS, UEFI allows for running “Applications”. These applications can do various setup and verification tasks, as well as gather information about the state of the current hardware.
The default reference implementation for UEFI is TianoCore EDK II. There are several other UEFI implementations to explore. We will use gnu-efi
in this guide.
Further Reading:
Application Development
There are a lot of guides that are half complete or just very confusing. This is the most reliable way I’ve found to get started.
Some of the guides I referenced
- https://wiki.osdev.org/UEFI_App_Bare_Bones
- https://wiki.osdev.org/GNU-EFI
- https://krinkinmu.github.io/2020/10/11/efi-getting-started.html
Environment Setup
I did this on Ubuntu 20.04, but it should work on any debian based system.
To make it easier I’m going to assume you have this directory structure
uefi/
app/ -- Your application code will live here (mkdir app/)
gnu-efi/ -- This is the library we link against (You will clone this)
mkgpt/ -- This is a library we clone
root/ -- This will be the dummy file system passed to the emulator (mkdir root/)
First, install the pre-requisites. I may have forgotten something but this is generally what is going to be needed.
sudo apt-get install qemu ovmf gnu-efi binutils-mingw-w64 gcc-mingw-w64 xorriso mtools automake
You’ll also need to install mkgpt. If you see the autoconf error error: version mismatch
, it’s prolly fine.
git clone https://github.com/jncronin/mkgpt.git
cd mkgpt
automake --add-missing
autoreconf
./configure
make
sudo make install
Grab the source for gnu-efi as well. This should provide some of the direct header files to link against.
git clone https://git.code.sf.net/p/gnu-efi/code gnu-efi
cd gnu-efi
make
We will need a copy of OVMF or Open Virtual Machine Firmware, to run our application.
Copy the OVMF.fd file to the app/ directory. This is a UEFI firmware file that qemu will boot into.
cd app
cp /usr/share/ovmf/OVMF.fd .
Writing A UEFI Application
Let’s just use a small program to start. This example was taken from the gnu-efi
library and modified.
main.c
#include <efi.h>
#include <efilib.h>
EFI_STATUS
efi_main (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
EFI_INPUT_KEY efi_input_key;
EFI_STATUS efi_status;
InitializeLib(image, systab);
Print(L" (0w0)/ (^V^)/ (@.@)/ \n");
Print(L" Your boys just want to \n");
Print(L" dap you up before you boot.\n");
Print(L"\n\n");
Print(L" Press any key to dap.\n");
WaitForSingleEvent(ST->ConIn->WaitForKey, 0);
uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L"\n\n");
efi_status = uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, &efi_input_key);
Print(L"You dapped: ScanCode [%02xh] UnicodeChar [%02xh] CallRtStatus [%02xh]\n",
efi_input_key.ScanCode, efi_input_key.UnicodeChar, efi_status);
return EFI_SUCCESS;
}
Now we have our files in place, let’s build the app.
Building a UEFI Application
There are a bunch of different options that we don’t need to care about right now. A lot of guides will lead to various toolchains and image formats, but we can keep it fairly straightforward by following somewhat of what the OSDEV GNU-EFI page recommends. That is, compiling to an object file, linking it against the gnu-efi
library, and then building an EFI binary with objcopy.
This script will do just that (build.sh). Run it in the app/ dir:
#!/bin/bash
echo "Compiling"
gcc -I$(pwd)/../gnu-efi/inc \
-fpic \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fshort-wchar \
-mno-red-zone \
-maccumulate-outgoing-args \
-c main.c \
-o main.o
echo "Linking"
ld -shared \
-Bsymbolic \
-L$(pwd)/../gnu-efi/x86_64/lib \
-L$(pwd)/../gnu-efi/x86_64/gnuefi \
-T$(pwd)/../gnu-efi/gnuefi/elf_x86_64_efi.lds \
$(pwd)/../gnu-efi/x86_64/gnuefi/crt0-efi-x86_64.o \
main.o \
-o main.so \
-lgnuefi \
-lefi
echo "Building EFI binary"
objcopy -j .text \
-j .sdata \
-j .data \
-j .dynamic \
-j .dynsym \
-j .rel \
-j .rela \
-j .rel.* \
-j .rela.* \
-j .reloc \
--target efi-app-x86_64 \
--subsystem=10 \
main.so \
main.efi
echo "Cleaning up files"
rm main.o main.so
echo "Copying main.efi to ../root/"
cp main.efi ../root/
This will build the app for you and put it in the ../root/ directory where our applications will live when passed to the OVMF binary within QEMU.
Running The Application
Okay now the fun part. Put this into run.sh:
#!/bin/bash
qemu-system-x86_64 -drive if=pflash,format=raw,file=$(pwd)/OVMF.fd \
-drive format=raw,file=fat:rw:../root \
-net none #\
# -nographic
This runs OVMF and passes the ../root/ directory with our EFI binary to it as the EFI file system.
Inside the shell type
Shell> fs0:
FS0:\> main.efi
You should see the following output:
Shell> fs0:
FS0:\> main.efi
(0w0)/ (^V^)/ (@.@)/
Your boys just want to
dap you up before you boot.
Press any key to dap.
You dapped: ScanCode [00h] UnicodeChar [61h] CallRtStatus [00h]
FS0:\>
Type Ctrl-A X
to exit QEMU.
There are a bunch of cool commands in the list when you type help
. Check out memmap, dh, dmpstore, devices, devtree, drivers, smbiosview…
EDIT: The following thing was due to -nographic haha
I noticed a weird thing when running the apps in gnu-efi that were built with the make
command. I’m not sure what compiler issue caused this but it happens even with simple examples. You may be able to just copy an example and link it yourself using the above methods, but this should be looked into further to understand what the build process it actually expects is.
FS0:\> bltgrid.efi
!!!! X64 Exception Type - 06(#UD - Invalid Opcode) CPU Apic ID - 00000000 !!!!
RIP - 00000000000B0000, CS - 0000000000000038, RFLAGS - 0000000000000246
RAX - 0000000000000000, RCX - 0000000007B23DE0, RDX - 0000000000000010
RBX - 000000000622EEF0, RSP - 0000000007F1C458, RBP - 0000000006E0E418
RSI - 0000000006221018, RDI - 0000000006221018
R8 - 0000000006221000, R9 - 0000000000001000, R10 - 0000000007F334C8
R11 - 0000000007F30470, R12 - 0000000007BEE018, R13 - 000000000622EEF8
R14 - 0000000000000000, R15 - 0000000006E0D040
DS - 0000000000000030, ES - 0000000000000030, FS - 0000000000000030
GS - 0000000000000030, SS - 0000000000000030
CR0 - 0000000080010033, CR2 - 0000000000000000, CR3 - 0000000007C01000
CR4 - 0000000000000668, CR8 - 0000000000000000
DR0 - 0000000000000000, DR1 - 0000000000000000, DR2 - 0000000000000000
DR3 - 0000000000000000, DR6 - 00000000FFFF0FF0, DR7 - 0000000000000400
GDTR - 0000000007BEE698 0000000000000047, LDTR - 0000000000000000
IDTR - 000000000722C018 0000000000000FFF, TR - 0000000000000000
FXSAVE_STATE - 0000000007F1C0B0
!!!! Can't find image information. !!!!
Additional References
- https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/3_foundation/readme.7
- https://wiki.osdev.org/Loading_files_under_UEFI
- https://krinkinmu.github.io/2020/10/18/handles-guids-and-protocols.html