gnu-efi integration: kernel.so or loader.so+kernel.elf

In this post, we’ll discuss the 2 possible methods of integrating gnu-efi into your project to make your kernel / OS “UEFI-aware” (i.e. capable of booting through UEFI firmware).

Other options (without using gnu-efi) are laid out in my previous blog post on the topic.

Relevant context

gnu-efi has one key constraint that requires our project’s files to be built as shared libraries when linked with libefi and libgnuefi. Namely, from the gnu-efi project’s README:

(2) EFI binaries should be relocatable.

Since EFI binaries are executed in physical mode, EFI cannot guarantee that a given binary can be loaded at its preferred address. EFI does try to load a binary at it’s preferred address, but if it can’t do so, it will load it at another address and then relocate the binary using the contents of the .reloc section.

And:

The approach to building relocatable binaries in the GNU EFI build environment is to:

(a) build an ELF shared object

(b) link it together with a self-relocator that takes care of applying the dynamic relocations that may be present in the ELF shared object

(c) convert the resulting image to an EFI binary

Prerequisite reading

If you’re unfamiliar with what load-time relocation is, or what PIC (position-independent code) is, read the following:

If you’re unsure of how gnu-efi works or how we convert its shared/dynamic library into a relocatable PE, read this section from my previous post:


The bundled approach

To be able to compile the entire RTEMS kernel as a shared library, we’ll need to handle a few issues:

  • RTEMS uses Newlib, which is currently compiled as a static libc.a archive - this will cause us problems at link-time (if we use the -shared flag) because it’ll include incompatible relocation entries such as R_X86_64_32.
  • GCC provides us with crtbegin.o and crtend.o, both of which also contain the incompatible R_X86_64_32 entries.
    • We can just ask GCC to build the shared variants of these files, crtbeginS.o and crtendS.o, and have GCC use them whenever the -shared flag is used.
    • Relevant patch to GCC to handle this.
  • We’ll need to figure out a way to have RTEMS itself compile itself with -fPIC and build shared libraries.
    • Fortunately, even this is fairly simple because RTEMS uses the idea of a bsp.cfg file which can customize compiler flags, and we can simply add the relevant flags to our port’s specific amd64.cfg file.
    • Relevant RTEMS patch.

Obviously, this approach is fairly simple and would let us just package all of RTEMS neatly into a relocatable PE very easily.

But what are the downsides of this approach?

  • We may be special-casing the build system for UEFI beyond recognition, tying ourselves in too deeply to easily adapt to a different one, such as Multiboot support. This may not be as big a deal, but it’s a concern to keep in mind.
  • We have no real reason to use -fPIC and the GOT/PLT it brings with it in RTEMS, since it will be fully resolved, and in theory, we could figure out a way to make the linker fill in the relative-addressing relocations without needing a runtime GOT/PLT method.
  • RTEMS is a kernel, involving interrupt-handling, context-switching, etc. It’s entirely possible that this method has unintended consequences on how such code is generated later, when we start to actually use it. Chris Johns (one of this project’s GSoC mentors) means to look into this.

The FreeBSD way

FreeBSD takes a different approach. They have a multi-stage loading process. In brief:

  • They build a two-stage bootloader for EFI, called boot1.efi and loader.efi.
  • loader.efi is an interactive prompt which may autoboot, or a boot kernelImg command can be used to load the actual kernel.
  • The kernel is loaded as an ELF through helper functions. The command_boot function drives this:
    • In brief, through calls go through:
    • command_boot -> mod_loadkld -> file_load -> file_formats[i]->l_load (actually the loadfile function in load_elf.c)
    • The loadfile function parses the program and section headers of the ELF file (through more function detours that are not really important).
    • Once the ELF has been loaded into memory at the correct entry_addr that it expects to be loaded at in memory, the l_exec function is called, which is actually elf64_exec in elf64_freebsd.c, at which hopefully through trampolining magic, the control flow will transfer to the kernel or ELF module.

TL;DR: FreeBSD’s kernel is loaded as an ELF file into memory and then executed through trampolining magic.

The benefits of this approach are:

  • We’d have a proper ELF loader in RTEMS, so the kernel changing over time doesn’t mean needing to be concerned with how the UEFI build system may break because of it (for eg. how is handwritten assembly handled in terms of a relocatable shared library?).
  • Our choice to support UEFI doesn’t affect how the entire system is built - it only affects the codebase in terms of needing to add the ELF loader.

Downsides:

  • The loader may be a lot of complicated code, increasing the size and complexity of RTEMS and its images for x86-64.
  • We’d need to figure out a way to have a loader.efi which uses whatever UEFI boot services it needs to, and then calls into an ELF loader to load the actual ELF - the location of this ELF could be read from a configuration file or be based on convention. This isn’t a downside as much as it is a thing worth noting.

Finito.