User:Xenos/UEFI Bare Bones

From OSDev Wiki
Jump to: navigation, search

Contents

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:

  1. Print "Hello world" to the standard EFI console.
  2. Flush the input buffer, to remove any possibly pending key strokes.
  3. Wait for a key to be pressed.
  4. 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:

  1. Select the output file format (ELF and its specific flavor).
  2. Select the output architecture.
  3. Set the entry point to the function efi_main.
  4. Select the necessary sections and align them on page boundaries.
  5. Create a fake relocation entry, since EFI loaders check for the presence of a relocation table.
  6. 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
Personal tools
Namespaces
Variants
Actions
Navigation
About
Toolbox