Fun and games with gnu-efi

gnu-efi is a set of scripts, libraries, header files and code examples to make it possible to write applications and drivers for the UEFI environment directly from your POSIX world. It supports i386, Ia64, X64, ARM and AArch64 targets ... but it would be dishonest to say it is beginner friendly in its current state. So let's do something about that.

Rough Edges

gnu-efi comes packaged for most Linux distributions, so you can simply run

$ sudo apt-get install gnu-efi

or

$ sudo dnf install gnu-efi gnu-efi-devel

to install it. However, there is a bunch of Makefile boilerplate that is not covered by said packaging, meaning that getting from "hey, let's check this thing out" to "hello, world" involves a fair bit of tedious makefile hacking.

... serrated?

Also, the whole packaging story here is a bit ... special. It means installing headers and libraries into /usr/lib and /usr/include solely for the inclusion into images to be executed by the UEFI firmware during Boot Services, before the operating system is running. And don't get me started on multi-arch support.

Simplification

Like most other programming languages, Make supports including other source files into the current context. The gnu-efi codebase makes use of this, but not in a way that's useful to a packaging system.

Now, at least GNU Make looks in /usr/include and /usr/local/include as well as the current working directory and any directories specified on the command line with -L. This means we can stuff most of the boilerplate in makefile fragments and include where we need them.

Hello World

So, let's start with the (almost) most trivial application imaginable:

#include <efi/efi.h>
#include <efi/efilib.h>

EFI_STATUS
efi_main(
    EFI_HANDLE image_handle,
    EFI_SYSTEM_TABLE *systab
    )
{
    InitializeLib(image_handle, systab);

    Print(L"Hello, world!\n");

    return EFI_SUCCESS;
}

Save that as hello.c.

Reducing the boiler-plate

Now grab Make.defaults and Make.rules from the gnu-efi source directory and stick them in a subdirectory called efi/.

Then download this gnuefi.mk I prepared earlier, and include it in your Makefile:

include gnuefi.mk

ifeq ($(HAVE_EFI_OBJCOPY), y)
FORMAT := --target efi-app-$(ARCH)      # Boot time application
#FORMAT := --target efi-bsdrv-$(ARCH)   # Boot services driver
#FORMAT := --target efi-rtdrv-$(ARCH)   # Runtime driver
else
SUBSYSTEM=$(EFI_SUBSYSTEM_APPLICATION)  # Boot time application
#SUBSYSTEM=$(EFI_SUBSYSTEM_BSDRIVER)    # Boot services driver
#SUBSYSTEM=$(EFI_SUBSYSTEM_RTDRIVER)    # Runtime driver
endif

all: hello.efi

clean:
    rm -f *.o *.so *.efi *~

The hello.efi dependency for the all target invokes implicit rules (defined in Make.rules) to generate hello.efi from hello.so, which is generated by an implicit rule from hello.o, which is generated by an implicit rule from hello.c.

NOTE: there are two bits of boiler-plate that still need addressing.

First of all, in gnuefi.mk, GNUEFI_LIBDIR needs to be manually adjusted to fit the layout implemented by your distribution. Template entries for Debian and Fedora are provided.

Secondly, the bit of boiler-plate we cannot easily get rid of - we need to inform the toolchain about whether the desired output is an application, a boot-time driver or a runtime driver. Templates for this is included in the Makefile snippet above - but note that different options must currently be set for toolchains where objcopy supports efi- targets directly and ones where it does not.

Building and running

Once the build environment has been completed, build the project as you would with any regular codebase.

$ make
gcc -I/usr/include/efi -I/usr/include/efi/x86_64 -I/usr/include/protocol -mno-red-zone -fpic  -g -O2 -Wall -Wextra -Werror -fshort-wchar -fno-strict-aliasing -fno-merge-constants -ffreestanding -fno-stack-protector -fno-stack-check -DCONFIG_x86_64 -DGNU_EFI_USE_MS_ABI -maccumulate-outgoing-args --std=c11 -c hello.c -o hello.o
ld -nostdlib --warn-common --no-undefined --fatal-warnings --build-id=sha1 -shared -Bsymbolic /usr/lib/crt0-efi-x86_64.o -L /usr/lib64 -L /usr/lib /usr/lib/gcc/x86_64-linux-gnu/6/libgcc.a -T /usr/lib/elf_x86_64_efi.lds hello.o -o hello.so -lefi -lgnuefi
objcopy -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel \
        -j .rela -j .rel.* -j .rela.* -j .rel* -j .rela* \
        -j .reloc --target efi-app-x86_64       hello.so hello.efi
rm hello.o hello.so
$ 

Then get the resulting application (hello.efi) over to a filesystem accessible from UEFI and run it.

UEFI Interactive Shell v2.2
EDK II
UEFI v2.60 (EDK II, 0x00010000)
Mapping table
FS0: Alias(s):HD1a1:;BLK3:
     PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)
BLK2: Alias(s):
     PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)
BLK4: Alias(s):
     PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)
BLK0: Alias(s):
     PciRoot(0x0)/Pci(0x1,0x0)/Floppy(0x0)
BLK1: Alias(s):
     PciRoot(0x0)/Pci(0x1,0x0)/Floppy(0x1)
Press ESC in 5 seconds to skip startup.nsh or any other key to continue.
Shell> fs0:
FS0:\> hello
Hello, world!
FS0:\>

Wohoo, it worked! (I hope.)

Summary

gnu-efi provides a way to easily develop drivers and applications for UEFI inside your POSIX environment, but it comes with some unnecessarily rough edges. Hopefully this post makes it easier for you to get started with developing real applications and drivers using gnu-efi quickly.

Clearly, we should be working towards getting this sort of thing included in upstream and installed with distribution packages.