User:Xenos/UEFI Bare Bones: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
Content added Content deleted
(Added QEMU / OVMF instructions.)
(→‎Source files: Added minimal elf.h include file.)
Line 63: Line 63:
<source lang="c">#include "efi.h"
<source lang="c">#include "efi.h"


EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable)
{
{
(void)ImageHandle;
EFI_STATUS Status;
EFI_STATUS Status;
EFI_INPUT_KEY Key;
EFI_INPUT_KEY Key;


/* Print message. */
/* Print message. */
Status = SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World\n\r");
Status = SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World\n\r");
if(EFI_ERROR(Status))
if(EFI_ERROR(Status))
return Status;
return Status;


/* Empty the console input buffer to flush out any keystrokes entered before this point. */
/* Empty the console input buffer to flush out any keystrokes entered before this point. */
Status = SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
Status = SystemTable->ConIn->Reset(SystemTable->ConIn, false);
if(EFI_ERROR(Status))
if(EFI_ERROR(Status))
return Status;
return Status;


/* Wait for keypress. */
/* Wait for keypress. */
while((Status = SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key)) == EFI_NOT_READY) ;
while((Status = SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key)) == EFI_NOT_READY) ;


return Status;
return Status;
}</source>
}</source>


=== efi.h ===
=== 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.

<source lang="c">#include <stdint.h>
#include <stdbool.h>

#ifndef EFI_H
#define EFI_H

typedef void* EFI_PVOID;
typedef void* EFI_HANDLE;

#ifdef __i386__
typedef uint32_t EFI_STATUS;
#endif
#ifdef __amd64__
typedef uint64_t EFI_STATUS;
#endif

#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, uint64_t Attribute);
typedef EFI_STATUS (*EFI_TEXT_STRING)(void *This, const void *String);

typedef EFI_STATUS (*EFI_TEXT_QUERY_MODE)(
void *This,
uint64_t ModeNumber,
uint64_t *Columns,
uint64_t *Rows);

typedef EFI_STATUS (*EFI_TEXT_SET_CURSOR_POSITION)(
void *This,
uint64_t Column,
uint64_t 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 {
uint16_t ScanCode;
uint16_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;
uint64_t NumberOfTableEntries;
void *ConfigurationTable;
} EFI_SYSTEM_TABLE;

#endif</source>


=== Linker script ===
=== Linker script ===

Revision as of 16:42, 11 April 2020

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:

i686
TARGET=i686-elf
TARGETS=$TARGET,i686-pe
x86_64
TARGET=x86_64-elf
TARGETS=$TARGET,x86_84-pe

Build environment

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");
	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>

#ifndef EFI_H
#define EFI_H

typedef void* EFI_PVOID;
typedef void* EFI_HANDLE;

#ifdef __i386__
typedef uint32_t EFI_STATUS;
#endif
#ifdef __amd64__
typedef uint64_t EFI_STATUS;
#endif

#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, uint64_t Attribute);
typedef EFI_STATUS (*EFI_TEXT_STRING)(void *This, const void *String);

typedef EFI_STATUS (*EFI_TEXT_QUERY_MODE)(
	void     *This,
	uint64_t ModeNumber,
	uint64_t *Columns,
	uint64_t *Rows);

typedef EFI_STATUS (*EFI_TEXT_SET_CURSOR_POSITION)(
	void     *This,
	uint64_t Column,
	uint64_t 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 {
	uint16_t ScanCode;
	uint16_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;
	uint64_t                        NumberOfTableEntries;
	void                            *ConfigurationTable;
} EFI_SYSTEM_TABLE;

#endif

Linker script

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
use 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
disable the red zone.
-mgeneral-regs-only
use only general purpose registers (since SSE etc. registers need operating system support).
-mabi=ms
used 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