User:Xenos/UEFI Bare Bones
Introduction
This tutorial shows how to build a simple "Hello world" bare bones EFI application. It is largely based on the UEFI App Bare Bones article, but instead of using (parts of) gnu-efi and a Windows targeted toolchain, it uses a bare metal targeted GCC cross compiler and no foreign sources.
Prerequisites
Bare metal GCC cross compiler
There are two approaches using the GNU (GCC / binutils) toolchain which are most common for building EFI applications:
- gnu-efi uses native tools, provided that the given version of objcopy can create EFI applications.
- Various tutorials use Windows (MinGW, MSYS) targeted toolchains.
This tutorial uses a different approach, which is closer to other, non-UEFI bare bones tutorials on OS development, by using a bare metal ELF targeted GCC cross compiler. However, a few minor modifications are necessary in order to create EFI applications with the created toolchain.
Target system selection
Before starting, one needs to decide on a target architecture, which will determine the further compiler options. For this tutorial, a bare metal ELF targeted cross compiler will be used; however, for binutils to be able to create EFI applications in the PE format, an additional PE target is needed. Depending on the chosen target architecture, one may use one of the following:
i386
export TARGET=i686-elf
export TARGETS=$TARGET,i686-pe
x86_64
export TARGET=x86_64-elf
export TARGETS=$TARGET,x86_64-pe
Build environment
export PREFIX=/opt/cross/$TARGET
export PATH="$PREFIX/bin:$PATH"
Building binutils
mkdir build-binutils
cd build-binutils
../binutils-x.y.z/configure --target=$TARGET --enable-targets=$TARGETS --prefix="$PREFIX" --with-sysroot --disable-nls
make
make install
Building GCC
mkdir build-gcc
cd build-gcc
../gcc-x.y.z/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers
make all-gcc
make all-target-libgcc
make install-gcc
make install-target-libgcc
MTools
The disk image is created using MTools.
Source files
main.c
This tutorial creates a simple "Hello world" EFI application, which does the following:
- Print "Hello world" to the standard EFI console.
- Flush the input buffer, to remove any possibly pending key strokes.
- Wait for a key to be pressed.
- Exit and return to the environment.
This is done in the following C code:
#include "efi.h"
EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable)
{
(void)ImageHandle;
EFI_STATUS Status;
EFI_INPUT_KEY Key;
/* Print message. */
Status = SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World\n\r" MESSAGE);
if(EFI_ERROR(Status))
return Status;
/* Empty the console input buffer to flush out any keystrokes entered before this point. */
Status = SystemTable->ConIn->Reset(SystemTable->ConIn, false);
if(EFI_ERROR(Status))
return Status;
/* Wait for keypress. */
while((Status = SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key)) == EFI_NOT_READY) ;
return Status;
}
efi.h
Most EFI applications make use of a large number of data types, structs, constants, function prototypes etc. which are declared in a common header file or set of header files. For this simple bare bones application, only a subset of these is used, and so a minimal set of declarations may be used. The following file declares only those parts which are necessary for simple console output and input.
#include <stdint.h>
#include <stdbool.h>
#include <wchar.h>
#ifndef EFI_H
#define EFI_H
typedef void* EFI_PVOID;
typedef void* EFI_HANDLE;
#ifdef __i386__
#define MESSAGE L"i386\r\n"
typedef uint32_t UINTN;
#endif
#ifdef __amd64__
#define MESSAGE L"x86_64\r\n"
typedef uint64_t UINTN;
#endif
typedef UINTN EFI_STATUS;
#define EFIERR(a) (a | ~(((EFI_STATUS)-1) >> 1))
#define EFI_ERROR(a) (a & ~(((EFI_STATUS)-1) >> 1))
#define EFI_NOT_READY EFIERR(6)
typedef struct {
uint64_t Signature;
uint32_t Revision;
uint32_t HeaderSize;
uint32_t CRC32;
uint32_t Reserved;
} EFI_TABLE_HEADER;
typedef struct {
uint32_t MaxMode;
uint32_t Mode;
uint32_t Attribute;
uint32_t CursorColumn;
uint32_t CursorRow;
uint8_t CursorVisible;
} SIMPLE_TEXT_OUTPUT_MODE;
typedef EFI_STATUS (*EFI_TEXT_CLEAR_SCREEN)(void *This);
typedef EFI_STATUS (*EFI_TEXT_ENABLE_CURSOR)(void *This, uint8_t Visible);
typedef EFI_STATUS (*EFI_TEXT_SET_ATTRIBUTE)(void *This, UINTN Attribute);
typedef EFI_STATUS (*EFI_TEXT_STRING)(void *This, const wchar_t *String);
typedef EFI_STATUS (*EFI_TEXT_QUERY_MODE)(
void *This,
UINTN ModeNumber,
UINTN *Columns,
UINTN *Rows);
typedef EFI_STATUS (*EFI_TEXT_SET_CURSOR_POSITION)(
void *This,
UINTN Column,
UINTN Row);
typedef struct {
EFI_PVOID Reset;
EFI_TEXT_STRING OutputString;
EFI_PVOID TestString;
EFI_TEXT_QUERY_MODE QueryMode;
EFI_PVOID SetMode;
EFI_TEXT_SET_ATTRIBUTE SetAttribute;
EFI_TEXT_CLEAR_SCREEN ClearScreen;
EFI_TEXT_SET_CURSOR_POSITION SetCursorPosition;
EFI_TEXT_ENABLE_CURSOR EnableCursor;
SIMPLE_TEXT_OUTPUT_MODE *Mode;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
typedef struct {
wchar_t ScanCode;
wchar_t UnicodeChar;
} EFI_INPUT_KEY;
typedef EFI_STATUS (*EFI_INPUT_RESET)(void *This, bool ExtendedVerification);
typedef EFI_STATUS (*EFI_INPUT_READ_KEY)(void *This, EFI_INPUT_KEY *Key);
typedef struct {
EFI_INPUT_RESET Reset;
EFI_INPUT_READ_KEY ReadKeyStroke;
EFI_PVOID WaitForKey;
} EFI_SIMPLE_TEXT_INPUT_PROTOCOL;
typedef struct {
EFI_TABLE_HEADER Hdr;
EFI_PVOID FirmwareVendor;
uint32_t FirmwareRevision;
EFI_PVOID ConsoleInHandle;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn;
EFI_PVOID ConsoleOutHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
EFI_PVOID StandardErrorHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
void *RuntimeServices;
void *BootServices;
UINTN NumberOfTableEntries;
void *ConfigurationTable;
} EFI_SYSTEM_TABLE;
#endif
Linker script
To link the final binary and keep the sections which are needed, a linker script is used. The following linker scripts, for different target architectures, do the following:
- Select the output file format (ELF and its specific flavor).
- Select the output architecture.
- Set the entry point to the function efi_main.
- Select the necessary sections and align them on page boundaries.
- Create a fake relocation entry, since EFI loaders check for the presence of a relocation table.
- Discard unnecessary sections.
For linking, one must make sure that the correct target architecture is selected.
i386
OUTPUT_FORMAT("elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(efi_main)
SECTIONS
{
. = 4096;
ImageBase = .;
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
. = ALIGN(4096);
.text :
{
_text = .;
*(.text)
*(.text.*)
*(.gnu.linkonce.t.*)
. = ALIGN(16);
}
_etext = .;
_text_size = . - _text;
. = ALIGN(4096);
.rdata :
{
_data = .;
*(.got.plt)
*(.got)
*(.rodata*)
*(.srodata)
*(.gnu.linkonce.r.*)
}
. = ALIGN(4096);
.data :
{
*(.data*)
*(.sdata*)
*(.gnu.linkonce.d.*)
}
. = ALIGN(4096);
.bss :
{
*(.sbss)
*(.scommon)
*(.dynbss)
*(.bss)
*(.gnu.linkonce.b.*)
*(COMMON)
}
. = ALIGN(4096);
.dynamic : { *(.dynamic) }
. = ALIGN(4096);
.rel :
{
*(.rel.data)
*(.rel.data.*)
*(.rel.got)
*(.rel.stab)
}
_edata = .;
_data_size = . - _etext;
. = ALIGN(4096);
.reloc :
{
LONG(_data);
LONG(10);
SHORT(0);
*(.reloc)
}
. = ALIGN(4096);
.dynsym : { *(.dynsym) }
. = ALIGN(4096);
.dynstr : { *(.dynstr) }
. = ALIGN(4096);
/DISCARD/ :
{
*(.rel.reloc)
*(.eh_frame)
*(.note*)
*(.comment*)
}
}
x86_64
OUTPUT_FORMAT("elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(efi_main)
SECTIONS
{
. = 4096;
ImageBase = .;
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
. = ALIGN(4096);
.text :
{
_text = .;
*(.text)
*(.text.*)
*(.gnu.linkonce.t.*)
. = ALIGN(16);
}
_etext = .;
_text_size = . - _text;
. = ALIGN(4096);
.rdata :
{
_data = .;
*(.got.plt)
*(.got)
*(.rodata*)
*(.srodata)
*(.gnu.linkonce.r.*)
}
. = ALIGN(4096);
.data :
{
*(.data*)
*(.sdata*)
*(.gnu.linkonce.d.*)
}
. = ALIGN(4096);
.bss :
{
*(.sbss)
*(.scommon)
*(.dynbss)
*(.bss)
*(.gnu.linkonce.b.*)
*(COMMON)
}
. = ALIGN(4096);
.dynamic : { *(.dynamic) }
. = ALIGN(4096);
.rel :
{
*(.rel.data)
*(.rel.data.*)
*(.rel.got)
*(.rel.stab)
}
_edata = .;
_data_size = . - _etext;
. = ALIGN(4096);
.reloc :
{
LONG(_data);
LONG(10);
SHORT(0);
*(.reloc)
}
. = ALIGN(4096);
.dynsym : { *(.dynsym) }
. = ALIGN(4096);
.dynstr : { *(.dynstr) }
. = ALIGN(4096);
/DISCARD/ :
{
*(.rel.reloc)
*(.eh_frame)
*(.note*)
*(.comment*)
}
}
Building
The build process consists of three steps. First, the main input (C) file is compiled using GCC. This produces an ELF object file. Note that for compiling, a freestanding environment must be used, since there is no C library used in this tutorial. The following flags are applied:
- -ffreestanding
- enables the freestanding environment, where no C library is used.
- -fpic
- produces position-independent code, which can be loaded anywhere in memory.
- -fno-stack-protector
- disables a stack protector.
- -fshort-wchar
- sets wide character strings to use 16 bit, since EFI uses 16 bit unicode.
- -Wall -Wextra -Wpedantic
- enables a number of helpful warnings.
- -O3
- enables optimization.
In the next step, the object file is linked using the appropriate linker script, which selects the emitted sections and their memory layout. This will create an ELF shared library file. The following flags are used:
- -nostdlib
- disables standard library search paths.
- -shared
- creates a shared library file.
- -Wl,-T,linker.lds
- makes use of the supplied linker script.
- -Wl,-Bsymbolic
- binds references to global symbols to their definition within the shared library.
- -Wl,-znocombreloc
- do not combine multiple relocation sections.
Finally, objcopy is used to build an EFI application out of the ELF library created in the previous step. The output file name is chosen so that the file is booted automatically in the EFI boot process, and can be found in the following table, depending on the target architecture.
architecture | boot file name | PE executable machine type |
---|---|---|
i386 / IA32 | BOOTIA32.EFI | 0x14c |
x86_64 / AMD64 | BOOTX64.EFI | 0x8664 |
IA64 / Itanium | BOOTIA64.EFI | 0x200 |
ARM / AArch32 | BOOTARM.EFI | 0x1c2 |
AArch64 | BOOTAA64.EFI | 0xaa64 |
In principle, it is also possible to omit the last step and to link directly to an EFI application, without using the intermediate ELF format. However, for this to work also the libgcc and object files must be present in a suitable (COFF) format. The GCC compiler used here produces ELF files, and so the intermediate step is used.
Depending on the target architecture, different further adaptations may be necessary.
i386
The following additional compiler options are used on the i686 target:
- -mgeneral-regs-only
- uses only general purpose registers (since SSE etc. registers need operating system support).
The following commands are used:
i686-elf-gcc -ffreestanding -fpic -fno-stack-protector -fshort-wchar -mgeneral-regs-only -Wall -Wextra -Wpedantic -O3 -o main.o -c main.c
i686-elf-gcc -nostdlib -shared -Wl,-T,i386.lds -Wl,-Bsymbolic -Wl,-znocombreloc -o kernel_ia32.elf main.o -lgcc
i686-elf-objcopy -I elf32-i386 -O efi-app-ia32 kernel_ia32.elf BOOTIA32.EFI
x86_64
The following additional compiler options are used on the x86_64 target:
- -mno-red-zone
- disables the red zone.
- -mgeneral-regs-only
- uses only general purpose registers (since SSE etc. registers need operating system support).
- -mabi=ms
- uses the Microsoft ABI for all function calls (which is needed for all calls to and from the EFI API).
The following commands are used:
x86_64-elf-gcc -ffreestanding -fpic -fno-stack-protector -fshort-wchar -mno-red-zone -mgeneral-regs-only -mabi=ms -Wall -Wextra -Wpedantic -O3 -o main64.o -c main.c
x86_64-elf-gcc -nostdlib -shared -Wl,-T,x86_64.lds -Wl,-Bsymbolic -Wl,-znocombreloc -o kernel_x64.elf main64.o -lgcc
x86_64-elf-objcopy -I elf64-x86-64 -O efi-app-x86_64 kernel_x64.elf BOOTX64.EFI
Creating disk images
There are different possibilities how to boot an EFI application. One possibility is to create a disk image and to copy the file into the directory /EFI/BOOT. The following commands create a floppy image, format it and copy the file(s) into the correct directory using MTools.
dd if=/dev/zero of=fat.img bs=1k count=1440
mformat -i fat.img -f 1440 ::
mmd -i fat.img ::/EFI
mmd -i fat.img ::/EFI/BOOT
mcopy -i fat.img BOOT*.EFI ::/EFI/BOOT
Running
QEMU with OVMF
One possibility to run EFI applications is to use QEMU with OVMF. Compiled OVMF images can be downloaded for different target architectures, and one must make sure to choose the correct files. The following examples will directly boot the bare bones kernel, wait for a key press and drop into the BIOS.
i386
qemu-system-i386 -machine q35 -m 256 -smp 2 -net none \
-global driver=cfi.pflash01,property=secure,value=on \
-drive if=pflash,format=raw,unit=0,file=OVMF_CODE-pure-efi.fd,readonly=on \
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS-pure-efi.fd \
-drive if=ide,format=raw,file=fat.img
x86_64
qemu-system-x86_64 -machine q35 -m 256 -smp 2 -net none \
-global driver=cfi.pflash01,property=secure,value=on \
-drive if=pflash,format=raw,unit=0,file=OVMF_CODE-pure-efi.fd,readonly=on \
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS-pure-efi.fd \
-drive if=ide,format=raw,file=fat.img
VirtualBox
VirtualBox can boot in UEFI mode and run the "Hello world" application created in this tutorial in both 32 and 64 bit mode. A virtual machine with the appropriate settings can be created with the following commands.
i386
Create the virtual machine:
VBoxManage createvm --name UEFI32 --ostype "Other" --register
VBoxManage modifyvm UEFI32 --ioapic on
VBoxManage modifyvm UEFI32 --boot1 floppy
VBoxManage modifyvm UEFI32 --memory 256 --vram 16
VBoxManage modifyvm UEFI32 --firmware efi32
VBoxManage storagectl UEFI32 --name "Floppy" --add floppy
VBoxManage storageattach UEFI32 --storagectl "Floppy" --port 0 --device 0 --type fdd --medium "fat.img"
Run:
VBoxManage startvm UEFI32
x86_64
Create the virtual machine:
VBoxManage createvm --name UEFI64 --ostype "Other_64" --register
VBoxManage modifyvm UEFI64 --ioapic on
VBoxManage modifyvm UEFI64 --boot1 floppy
VBoxManage modifyvm UEFI64 --memory 256 --vram 16
VBoxManage modifyvm UEFI64 --firmware efi64
VBoxManage storagectl UEFI64 --name "Floppy" --add floppy
VBoxManage storageattach UEFI64 --storagectl "Floppy" --port 0 --device 0 --type fdd --medium "fat.img"
Run:
VBoxManage startvm UEFI64