GNU-EFI

From OSDev.wiki
Jump to navigation Jump to search

GNU-EFI is a very lightweight developing environment to create UEFI applications. EDK2 is a large, complex environment with its own build system. GNU-EFI on the other hand is a set of libraries and headers for compiling UEFI applications with a system's native GCC.

This tutorial will guide you through to create a simple Hello World UEFI application using this library.

Basic Concept

The main concept is to use your host native compiler, then convert the resulting ELF into UEFI-compatible PE. This means you can use your native toolchain, and only one additional step is required.

Of course you can also use GCC Cross-Compiler generating PE directly as described in UEFI App Bare Bones. Whichever you prefer.

Requirements

Download and compile GNU-EFI.

$ git clone https://git.code.sf.net/p/gnu-efi/code gnu-efi
$ cd gnu-efi
$ make

Libraries

This "make" should create the following three object files:

  • crt0-efi-x86_64.o: A CRT0 (C runtime initialization code) that will call the "efi_main" function.
  • libgnuefi.a: A library containing a single function (_relocate) that is used by the CRT0.
  • libefi.a: A library containing convenience functions like CRC computation, string length calculation, and easy text printing.

Strictly speaking only the first two are needed, but it worth having the third.

Headers

You'll need some header files. You can use the ones shipped with your Linux distribution (/usr/include/efi), this is the preferable as they are updated to the latest. Then you can use the ones in the EDK2 package, or the minimal headers in GNU-EFI inc. This latter inc directory is installed with the git clone command, however it is just a minimal set of headers, only the essentials and not necessarily up-to-date.

Linker Script

This is pretty simple, we'll just use elf_x86_64_efi.lds that GNU-EFI provides.

Creating an EFI executable

The traditional "Hello, world" UEFI program is shown below, which we will compile using GNU-EFI in this tutorial.

main.c:

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

EFI_STATUS
EFIAPI
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  InitializeLib(ImageHandle, SystemTable);
  Print(L"Hello, world!\n");
  return EFI_SUCCESS;
}

Compilation

To compile the main.c above, you'll have to use some environment specific command line options. These are:

$ gcc -Ignu-efi-dir/inc -fpic -ffreestanding -fno-stack-protector -fno-stack-check -fshort-wchar -mno-red-zone -maccumulate-outgoing-args -c main.c -o main.o
  • -I: set this to the efi headers directory (see above)
  • -fpic: the UEFI PE executables must be relocatable
  • -ffreestanding: there's no hosted gcc environment, we don't have libc
  • -fno-stack-protector -fno-stack-check -mno-red-zone: stack must be strictly used, no additional canaries or pre-allocated local variable space allowed
  • -fshort-wchar: it is very important that UEFI uses 16 bit characters (wide-characters or wchar_t, defined as CHAR16 in efi headers)
  • -maccumulate-outgoing-args: function calls must include the number of arguments passed to the functions

Replace 'gnu-efi-dir' with your repository's path.

Linking

It is very simple, but there is a trick to it. To generate properly relocatable code, we should link it as a shared library.

$ ld -shared -Bsymbolic -Lgnu-efi-dir/x86_64/lib -Lgnu-efi-dir/x86_64/gnuefi -Tgnu-efi-dir/gnuefi/elf_x86_64_efi.lds gnu-efi-dir/x86_64/gnuefi/crt0-efi-x86_64.o main.o -o main.so -lgnuefi -lefi
  • -shared -Bsymbolic: tell GNU ld to create so
  • -L and -T: where to find the static GNU-EFI libraries (.a) and the linker script
  • .o: it is important to specify crt0 as the first. Should work as the last too, but I had problems
  • -l: linking with gnuefi is a must, as that contains the relocation code. Linking with efi is optional, but recommended

Replace 'gnu-efi-dir' with your repository's path.

Converting Shared Object to EFI executable

The last step is to convert our ELF shared object into an UEFI PE.

$ objcopy -j .text -j .sdata -j .data -j .rodata -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
  • -j: these specify which sections to keep during convertion
  • --target efi-app-x86_64: this tells objcopy to generate a PE32+ format, with architecture code 0x8664
  • --subsystem=10: this is the most important. It sets the file's type to UEFI executable in the PE header.

Now you can copy main.efi to your EFI System Partition, and after boot run it from the EFI Shell. Or you can rename it to EFI\BOOT\BOOTX64.EFI and it should be executed automatically on boot.

Calling Any Arbitrary UEFI Function

The libefi.a has wrappers for the most common UEFI functions, but you might need to call something not covered. For completeness, it provides:

uefi_call_wrapper(func, numarg, ...);

For example, the "Print" function used in our main.c and which accepts printf compatible arguments, is under the hood nothing else than a call to:

uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, buffer);

The biggest advantage of 'uefi_call_wrapper' is that doesn't matter what ABI your gcc is using, it will always correctly translate that into UEFI ABI. If, and only if you've used the correct gcc options, then you should be able to make the same call as:

ST->ConOut->OutputString(ST->ConOut, buffer);

It worth noting that GNU-EFI uses the host compiler, therefore you might need additional gcc options to get this working. Using 'uefi_call_wrapper' on the other hand will always work.

See also

Articles

External Links