UEFI

From OSDev.wiki
Jump to navigation Jump to search

(U)EFI or (Unified) Extensible Firmware Interface is a specification for x86, x86-64, ARM, and Itanium platforms that defines a software interface between the operating system and the platform firmware/BIOS. The original EFI was developed in the mid-1990s by Intel for use developing firmware/BIOS for Itanium platforms. In 2005 Intel transitioned the specification to a new working group called the Unified EFI Forum, consisting of companies such as AMD, Microsoft, Apple, and Intel itself. All modern PCs ship with UEFI firmware and UEFI is widely supported by both commercial and open source operating systems. Backwards compatibility is provided for legacy operating systems.

UEFI basics

UEFI vs. legacy BIOS

A common misconception is that UEFI is a replacement for BIOS. In reality, both legacy motherboards and UEFI-based motherboards come with BIOS ROMs, which contain firmware that performs the initial power-on configuration of the system before loading some third-party code into memory and jumping to it. The differences between legacy BIOS firmware and UEFI BIOS firmware are where they find that code, how they prepare the system before jumping to it, and what convenience functions they provide for the code to call while running.

Platform initialization

On a legacy system, BIOS performs all the usual platform initialization (memory controller configuration, PCI bus configuration and BAR mapping, graphics card initialization, etc.), but then drops into a backwards-compatible real mode environment. The bootloader must enable the A20 gate, configure a GDT and an IDT, switch to protected mode, and for x86-64 CPUs, configure paging and switch to long mode.

UEFI firmware performs those same steps, but also prepares a protected mode environment with flat segmentation and for x86-64 CPUs, a long mode environment with identity-mapped paging. The A20 gate is enabled as well.

Additionally, the platform initialization procedure of UEFI firmware is standardized. This allows UEFI firmware to be extended in a vendor-neutral way.

Boot mechanism

A legacy BIOS loads a 512 byte flat binary blob from the MBR of the boot device into memory at physical address 7C00 and jumps to it. The bootloader cannot return back to BIOS. UEFI firmware loads an arbitrary sized UEFI application (a relocatable PE executable file) from a FAT partition on a GPT-partitioned boot device to some address selected at run-time. Then it calls that application's main entry point. The application can return control to the firmware, which will continue searching for another boot device or bring up a diagnostic menu.

System discovery

A legacy bootloader scans memory for structures like the EBDA, SMBIOS, and ACPI tables. It uses PIO to talk to the root PCI controller and scan the PCI bus. It is possible that redundant tables may be present in memory (for example, the MP table in the SMBIOS contains information that's also present in the ACPI DSDT) and the bootloader can choose which to use.

When UEFI firmware calls a UEFI application's entry point function, it passes a "System Table" structure, which contains pointers to all of the system's ACPI tables, memory map, and other information relevant to an OS. Legacy tables (like SMBIOS) may not be present in memory.

Convenience functions

A legacy BIOS hooks a variety of interrupts which a bootloader can trigger to access system resources like disks and the screen. These interrupts are not standardized, except by historical convention. Each interrupt uses a different register passing convention.

UEFI firmware establishes many callable functions in memory, which are grouped into sets called "protocols" and are discoverable through the System Table. The behavior of each function in each protocol is defined by specification. UEFI applications can define their own protocols and persist them in memory for other UEFI applications to use. Functions are called with a standardized, modern calling convention supported by many C compilers.

Development environment

Legacy bootloaders can be developed in any environment that can generate flat binary images: nasm, gcc, etc. UEFI applications can be developed in any language that can be compiled and linked into a PE executable file and supports the calling convention used to access functions established in memory by the UEFI firmware. In practice this means one of two development environments: Intel's TianoCore EDK2 or GNU-EFI.

TianoCore is a large, complex environment with its own build system. It can be configured to use GCC, MinGW, Microsoft Visual C++, etc. as a cross-compiler. Not only can it be used to compile UEFI applications, but it can also be used to compile UEFI firmware to be flashed to a BIOS ROM.

GNU-EFI is a set of libraries and headers for compiling UEFI applications with a system's native GCC. It can't be used to compile UEFI firmware. Since it's just a couple of libraries against which a UEFI application can be linked, it is much easier to use than TianoCore.

Emulation

Bochs ships with a default open-source legacy BIOS. Additionally, SeaBIOS, a popular open-source legacy BIOS, has been ported to both the Bochs and QEMU emulated machines. Both of these BIOSs implement most of the legacy BIOS features you would expect. However, they vary quite significantly in operation from commercial legacy BIOSs on real machines.

OVMF, a popular open source UEFI firmware, has been ported to the QEMU (but not Bochs) emulated machine. Because it implements the UEFI specification, it behaves very similarly to commercial UEFI firmware on real machines. (OVMF itself is built with TianoCore, but pre-built images are available.)

Legacy bootloader or UEFI application?

If you are targeting legacy systems for which UEFI is not available or is not reliable, you should develop a legacy bootloader. This requires intimate knowledge of 16-bit addressing and the backwards-compatibility features of an x86 or x86-64 CPU. If you are targeting modern systems you should develop a UEFI application. Many UEFI firmwares can be configured to emulate a legacy BIOS, but there is even more variation among these emulated environments than among real legacy BIOS.

Although there is a slight learning curve to become familiar with the UEFI development environments, using the System Table, and accessing UEFI-provided protocols (functions), there are far fewer "gotchas" than trying to remain compatible with the wide variety of quickly-becoming-obsolete legacy BIOSs on real machines. UEFI is the standard for all modern PCs.

UEFI class 0-3 and CSM

PCs are categorized as UEFI class 0, 1, 2, or 3. A class 0 machine is a legacy system with a legacy BIOS; i.e. not a UEFI system at all.

A class 1 machine is a UEFI system that runs exclusively in Compatibility Support Module (CSM) mode. CSM is a specification for how UEFI firmware can emulate a legacy BIOS. UEFI firmware in CSM mode loads legacy bootloaders. A class 1 UEFI system may not advertise UEFI support at all, since it isn't exposed to the bootloader. It's only UEFI "within" the BIOS.

A class 2 machine is a UEFI system that can launch UEFI applications but also includes the option to run in CSM mode. The majority of modern PCs are UEFI class 2 machines. Sometimes the choice to run UEFI applications vs. CSM is a one-or-the-other setting in the BIOS configuration, and other times the BIOS will decide which to use after selecting the boot device and checking whether it has a legacy bootloader or a UEFI application.

A class 3 machine is a UEFI system that does not support CSM. UEFI class 3 machines only run UEFI applications and do not implement CSM for backwards compatibility with legacy bootloaders.

Secure Boot

Secure Boot is a digital signature scheme for UEFI applications that consists of four components:

  • PK: Platform Key
  • KEK: Key Exchange Keys
  • db: Whitelist database
  • dbx: Blacklist database

UEFI firmware that supports Secure Boot is always in one of three states:

  • Setup mode, Secure Boot off
  • User mode, Secure Boot off
  • User mode, Secure Boot on

In setup mode, any UEFI application can change or delete the PK, add/remove keys from the KEK, and add/remove whitelist or blacklist entries from the db or dbx.

In user mode, regardless of whether Secure Boot is on or off:

  • The PK may only be changed or deleted by a UEFI application that already has the current PK.
  • Keys can only be added/removed from the KEK by a UEFI application that has the PK.
  • Whitelist and blacklist entries can only be added/removed from the db and dbx by a UEFI application that has any one of the keys in the KEK.

Finally, in user mode with Secure Boot on, UEFI applications must meet one of the following four requirements to be launched:

  • Signed, with signature in db and not in dbx
  • Signed by a key in db and not in dbx
  • Signed by a key in the KEK
  • Unsigned, but a hash of the application is in db and not in dbx

Note that UEFI applications are not signed by the PK, unless the PK also happens to be in the KEK.

Not all UEFI firmwares support Secure Boot, although it is a requirement for Windows 10. Some UEFI firmwares support Secure Boot and do not allow it to be disabled, which poses a problem for independent developers that do not have access to the PK or any of the keys in the KEK, and therefore can't install their own key or application signature or hash to the whitelist database. Independent developers should develop on systems that either do not support Secure Boot or allow Secure Boot to be turned off.

How to use UEFI

Traditional operating systems like Windows and Linux have an existing software architecture and a large code base to perform system configuration and device discovery. With their sophisticated layers of abstraction they don't directly benefit from UEFI. As a result, their UEFI bootloaders do little but prepare the environment for them to run.

An independent developer may find more value in using UEFI to write feature-full UEFI applications, rather than viewing UEFI as a temporary start-up environment to be jettisoned during the boot process. Unlike legacy bootloaders, which typically interact with BIOS only enough to bring up the OS, a UEFI application can implement sophisticated behavior with the help of UEFI. In other words, an independent developer shouldn't be in a rush to leave "UEFI-land".

A good starting point is writing a UEFI application that uses the System Table to fetch a memory map, and uses the "File" protocol to read files from FAT-formatted disks. The next step might be to use the System Table to locate ACPI tables.

Developing with GNU-EFI

GNU-EFI can be used to develop both 32-bit and 64-bit UEFI applications. This section will address 64-bit UEFI applications only, and assumes that the development environment itself is running on an x86_64 system, so that no cross-compiler is needed. For a more thorough walk-through of a proper development environment, see UEFI Bare Bones.

GNU-EFI includes four things:

  • crt0-efi-x86_64.o: A CRT0 (C runtime initialization code) that provides an entry point that UEFI firmware will call when launching the application, which will in turn call the "efi_main" function that the developer writes.
  • libgnuefi.a: A library containing a single function (_relocate) that is used by the CRT0.
  • elf_x86_64_efi.lds: A linker script used to link UEFI applications.
  • efi.h and other headers: Convenience headers that provide structures, typedefs, and constants improve readability when accessing the System Table and other UEFI resources.
  • libefi.a: A library containing convenience functions like CRC computation, string length calculation, and easy text printing.
  • efilib.h: Header for libefi.a.

At a bare minimum, a 64-bit UEFI application will need to link against crt0-efi-x86_64.o and libgnuefi.a using the elf_x86_64_efi.lds linker script. Most likely you will want to use the provided headers and the convenience library as well, and this section will assume that going forward.

The traditional "Hello, world" UEFI program is shown below.

#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;
}

A few notes:

  • efi.h is included so we can use types like EFI_STATUS, EFI_HANDLE, and EFI_SYSTEM_TABLE.
  • When creating a 32-bit UEFI application, EFIAPI is empty; GCC will compile the "efi_main" function using the standard C calling convention. When creating a 64-bit UEFI application, EFIAPI expands to "__attribute__((ms_abi))" and GCC will compile the "efi_main" function using Microsoft's x64 calling convention, as specified by UEFI. Only functions that will be called directly from UEFI (i.e. main) need to use the UEFI calling convention.
  • "InitializeLib" and "Print" are convenience functions provided by libefi.a with prototypes in efilib.h. "InitializeLib" lets libefi.a store a reference to the ImageHandle and SystemTable provided by BIOS. "Print" uses those stored references to print a string by reaching out to UEFI-provided functions in memory. (Later on we will see how to find and call UEFI-provided functions manually.)

This program is compiled and linked as below.

$ gcc main.c                             \
      -c                                 \
      -fno-stack-protector               \
      -fpic                              \
      -fshort-wchar                      \
      -mno-red-zone                      \
      -I /path/to/gnu-efi/headers        \
      -I /path/to/gnu-efi/headers/x86_64 \
      -DEFI_FUNCTION_WRAPPER             \
      -o main.o

$ ld main.o                         \
     /path/to/crt0-efi-x86_64.o     \
     -nostdlib                      \
     -znocombreloc                  \
     -T /path/to/elf_x86_64_efi.lds \
     -shared                        \
     -Bsymbolic                     \
     -l:/path/to/libgnuefi.a        \
     -l:/path/to/libefi.a           \
     -o main.so

$ objcopy -j .text                \
          -j .sdata               \
          -j .data                \
          -j .dynamic             \
          -j .dynsym              \
          -j .rel                 \
          -j .rela                \
          -j .reloc               \
          --target=efi-app-x86_64 \
          main.so                 \
          main.efi

The result of this process is a 44 kB PE executable file main.efi. On a real project you'll probably want to use make or another build tool, and may need to build a cross-compiler.

Emulation with QEMU and OVMF

Any recent version of QEMU with a recent version of OVMF will be sufficient to run a UEFI application. QEMU binaries are available for many platforms, and a binary OVMF image (OVMF.fd) can be found on the TianoCore website. QEMU (without any boot disk) can be invoked as below.

$ qemu-system-x86_64 -cpu qemu64 -bios /path/to/OVMF.fd

If you prefer to work on a terminal without X, or via SSH/telnet, you will want to run QEMU without graphics support.

$ qemu-system-x86_64 -cpu qemu64 -bios /path/to/OVMF.fd -nographic

If OVMF does not find a boot disk with a properly named UEFI application (more on this later) it will drop into a UEFI shell.

You can find a list of shell commands here or you can type help at the shell.

Creating disk images

To launch a UEFI application you will need to create a disk image and present it to QEMU. UEFI firmware expects UEFI applications to be stored in a FAT12, FAT16, or FAT32 file system on a GPT-partitioned disk. Many firmwares only support FAT32, so that's what you'll want to use. Depending on your platform, there are several different ways to create a disk image containing your UEFI application, but they all start by creating a zeroed disk image file. The minimum FAT32 partition size is 33,548,800 bytes, plus you will need space for the primary and secondary GPT tables, plus some slack space so the partition can be aligned correctly. Throughout these examples we will be creating a 34,000,384 byte (66407 512-byte sectors, or ~34 MB) disk image.

$ dd if=/dev/zero of=/path/to/uefi.img bs=512 count=66407

Linux, root required

TODO - going to rework the wording here.

Select some reasonable size of your disk. Below I am assuming that $BYTES_PER_SECTOR is number of bytes per sector on your disk (typically 512) and $NUM_SECTORS is total disk size expressed in sectors. Firstly create disk image initial file filled with zeroes:

export filename=$PREFIX/share/qemu/myOS/myOS.disk
dd if=/dev/zero of=$filename bs=$BYTES_PER_SECTOR count=$NUM_SECTORS

After that create partition table by gdisk:

gdisk $filename

It has interface similar to fdisk utility. Use "o" command to create new partition table, "n" for new partition with default parameters to use the whole disk (partition type "ef00" for EFI system partition), "w" to write all changes and exit. Now you have disk image with GUID partition table on it but the partition is still unformatted. We will use Linux loopback device to access the file as block device. We need to know exact position of the partition on the disk:

gdisk -l $filename

This command will output list of partitions and their first and last sectors indexes. If you have several partitions then just use numbers for your one. Let's assume $START is the first sector index * bytes per sector and $SIZE size is the partition size in bytes. Associate your image file with the loopback device:

losetup --offset $start --sizelimit $size /dev/loop0 $filename

So now we can create filesystem there. We need FAT for EFI system partition. You can use FAT12 if your partition size is small to prevent from big space overhead from larger FAT filesystems:

mkdosfs -F 12 /dev/loop0

Now you can mount your partition to some mount point:

mkdir /tmp/myOsDisk
mount /dev/loop0 /tmp/myOsDisk

Just copy all files you need there (e.g. your EFI application executable image). You can create "stratup.nsh" script which will be executed automatically if no other options are configured in EFI built-in boot-manager. Script could contain just your file launching command with required parameters, e.g. "fs0:\\efi\\boot\\myOsLoader some parameters here". According to specification you can create "/EFI/BOOT/BOOTx64.EFI" file which will be loaded automatically. Finally unmount the partition and release the loopback device:

umount /tmp/myOsDisk
losetup -d /dev/loop0

Your disk image is ready and you can finally launch emulation. Obviously creating disk image could be easily automated in order to not execute these actions manually each time. Automation example can be found there.

Linux, root not required

You can also get a few tools that do not rely on having certain features in a Linux kernel. Instead, we make use of dd, mtools and parted rather than gdisk and loopback devices. These can be automated into a relatively brief makefile:

FAT_SECTORS:=65536
DISK_SECTORS:=69632
DISK_START:=2048
DISK_END:=67584

TEMP_IMG := temp.img
PARTED := /usr/sbin/parted
PARTED_PARAMS := -s -a minimal

partition.img: $(FILES)
        dd if=/dev/zero of=$(TEMP_IMG) bs=512 count=$(FAT_SECTORS)
        mformat -i $(TEMP_IMG) -h 32 -t 32 -n 64 -c 1 ::
        mcopy -i $(TEMP_IMG) $(FILES) ::
        cp $(TEMP_IMG) $@
        rm $(TEMP_IMG)

hd.img: partition.img
        dd if=/dev/zero of=$(TEMP_IMG) bs=512 count=$(DISK_SECTORS)
        $(PARTED) $(TEMP_IMG) $(PARTED_PARAMS) mklabel gpt
        $(PARTED) $(TEMP_IMG) $(PARTED_PARAMS) mkpart EFI FAT16 $(DISK_START)s $(DISK_END)s
        $(PARTED) $(TEMP_IMG) $(PARTED_PARAMS) toggle 1 boot
        dd if=$(BUILDROOT)/partition.img of=$(TEMP_IMG) bs=512 obs=512 count=$(FAT_SECTORS) seek=$(DISK_START) conv=notrunc
        cp $(TEMP_IMG) $@
        rm $(TEMP_IMG)

FreeBSD, root required

This approach uses mdconfig, gpart, newfs_msdos, and mount_msdosfs. First, create a device node that presents the zeroed disk image as a block device. This will let us work on it using standard partitioning and formatting tools.

$ mdconfig -f /path/to/uefi.img
md0

In this example the new block device is md0. Now create the empty primary and secondary GPT tables on the device.

$ gpart create -s GPT md0
md0 created

Now we can add a partition to the disk. We'll specify an "EFI" partition, which just means that GPT will set that partition's GUID to the special "EFI" type. Not all BIOSs require this, and the partition will still be able to be mounted and browsed normally on Linux, FreeBSD, and Windows.

$ gpart add -t efi md0
md0p1 added

Next, create a FAT16 file system on the new partition. You can specify various parameters for the file system if you'd like, but it isn't necessary. Ideally you would create a FAT32 partition, for best BIOS compatibility, but FreeBSD sometimes creates FAT32 partitions that OVMF can't read. TODO - specify the right parameters to work around this.

$ newfs_msdos -F 16 md0p1
newfs_msdos: trim 1 sectors to adjust to a multiple of 63
/dev/md0p1: 66240 sectors in 8280 FAT16 clusters (4096 bytes/cluster)
BytesPerSec=512 SecPerClust=8 ResSectors=1 FATs=2 RootDirEnts=512 Media=0xf0 FATsecs=33 SecPerTrack=63 Heads=2 HiddenSecs=0 HugeSectors=66339

The partition can now be mounted, so that we can copy files to it. In this example we use the /mnt directory, but you could also create a local directory for temporary use.

$ mount_msdosfs /dev/md0p1 /mnt

Copy any UEFI applications you want to test to the file system.

$ cp /path/to/main.efi /mnt/
$ ...

Finally, unmount the partition and free the block device.

$ umount /mnt
$ mdconfig -d -u md0

uefi.img is now a disk image containing primary and secondary GPT tables, containing a single partition of type EFI, containing a FAT16 file system, containing one or more UEFI applications.

Launching UEFI applications

Once your disk image is ready, you can invoke QEMU as below.

$ qemu-system-x86_64 -cpu qemu64 -bios /path/to/OVMF.fd -drive file=uefi.disk,if=ide

When OVMF drops into the UEFI shell, you will see an additional entry in the "Mapping table", labeled "FS0". This indicates that the firmware detected the disk, discovered the partition, and was able to mount the file system. You can explore the file system by switching to it using the DOS-style syntax "FS0:", as illustrated below.

You can launch a UEFI application by entering its name.

Notice that the UEFI shell resumed once the application terminated. Of course if this was a proper bootloader it would never resume, but rather launch an OS.

Some commercial UEFI firmware provides UEFI shells or the capability to launch user-selected UEFI applications, such as the firmware that ships with the HP EliteBook line of laptops. Most, however, do not expose this functionality to the end-user.

Debugging

See Debugging UEFI applications with GDB.

Running on real hardware

NVRAM variables

A UEFI firmware will present most of its configuration options through a text or graphical configuration menu, just like a legacy BIOS. Selections made in these menus are stored in the NVRAM chip between reboots. Unlike legacy BIOS, however, the firmware developer has the option to expose some or all of these "NVRAM variables" to the OS and end-user via convenience functions made resident in RAM by the firmware at boot.

The Linux efivarfs kernel module will use these functions to list NVRAM variables in the /sys/firmware/efi/efivars file. NVRAM variables can also be dumped from within the UEFI shell itself with the dmpstore command. Device boot order is always accessible via NVRAM variables. The Linux program efibootmgr works specifically with the boot order NVRAM variables. The UEFI shell offers the bcfg command for the same purpose.

Bootable UEFI applications

The boot order NVRAM variables determine where firmware will look for UEFI applications to be launched at boot. Although this can be changed (for example, an OS installer might customize the boot entry for the hard drive to which it was installed) firmware typically looks for a UEFI application named "BOOT.efi" (for 32-bit applications) or "BOOTX64.efi" (for 64-bit applications) stored in the "/EFI/BOOT" path in the boot device's file system. This is the default path and name for OVMF.

Unlike a UEFI application launched from the shell, if a bootable UEFI application returns BIOS will continue searching for other boot devices.

Exposed functionality

Real PCs vary in the amount of UEFI capability they expose to the user. For example, even a class 3 machine may not make any mention of UEFI in its BIOS configuration and may not offer a UEFI shell. Additionally, some BIOS vendors make their UEFI firmware configuration screens look identical to their legacy BIOS configuration screens. Class 2 machines may present somewhat confusing boot menus and configuration options. For example, one laptop manufacturer includes a configuration option to enable/disable UEFI (i.e. switch between UEFI and CSM behavior), named "OS: Windows 8". Another laptop, if it fails to find a bootable UEFI application on the selected boot device (or if that application returns a status other than EFI_SUCCESS) will fall back to CSM behavior and then complain that the drive has a corrupted MBR. With time, and with the emergence of class 3 machines, clarity of UEFI boot behavior will improve.

To make testing on real hardware easier, you can install a bootable UEFI application to the internal hard drive of the system that provides a boot menu, such as rEFInd. This may also be convenient for multi-boot scenarios.

PC firmware developers

On x86 and x86-64 platforms, the following BIOS developers offer UEFI firmware:

  • AMI (Aptio).
  • Phoenix (SecureCore, TrustedCore, AwardCore).
  • Insyde (InsydeH20).

Apple systems

Apple systems implement EFI 1.0, as opposed to UEFI, with the distinction that UEFI applications are loaded from HFS+ file systems instead of FAT12/16/32. Additionally, those UEFI applications must be "blessed" (either directly, or by residing in a blessed directory) to be loaded. Blessing sets flags within the HFS+ file system that Apple's firmware checks before loading an application. The open-source hfsutils package includes support for blessing files within HFS file systems, but not directories nor HFS+.

Using GNU toolchain for compiling and debugging EFI applications

TODO: Integrate the information about FASM into the sections above. Integrate the info on debugging into the sections above. The rest can go - it's covered more simply above, and in more detail on the UEFI Bare Bones page.

GNU development toolchain consist of binutils package (assembler, linker, various utilities for manipulating executable images), GCC compiler, GNU make and GDB debugger. Advantages are obvious - build system is very flexible, the tools have very rich functionality, they are free and open source, your EFI application code can be easily integrated to any project. Most of open source software prefers this way.

Building tools

The first step is to compile your tools with required options. Firstly you need to compile binutils package. Obtain the latest from http://ftp.gnu.org/gnu/binutils/. You will need to enable "i386-efi-pe" target to have support for EFI PE+ executable format. If you use the same toolchain for compiling something else in your project (e.g. OS kernel) you can also specify required target (e.g. x86_64-myOS-freebsd) Note that it is BFD target, not the target platform. If you want to develop for 64-bits platform add "--enable-64-bit-bfd" options to "configure" script.

../src/configure --prefix=$PREFIX --target=$TARGET --disable-nls --build=$BUILD --enable-64-bit-bfd \
    --enable-targets=i386-efi-pe,x86_64-phoenix-freebsd
make all
make install

Here and below $PREFIX variable points to your prefix where you want to install your build tools (e.g. "export PREFIX=/home/John/projects/myOS/build-tools") and $TARGET is you target platform (e.g. "x86_64-myOS-elf").

If your build machine has not the same architecture as target platform you will need a cross compiler. There are instructions about compiling GCC for cross platform development. Just use binutils compiled above with these instructions.

You will need a debugger for debugging your applications. Obtain it from http://ftp.gnu.org/gnu/gdb/ and compile:

./configure --prefix=$PREFIX --target=$TARGET --enable-64-bit-bfd

Now you are ready to compile your first EFI application. Download gnu-efi package from https://sourceforge.net/projects/gnu-efi/ and read its README files. Follow the instructions there, check Makefiles are pointing to your build tools and have correct architecture specified. Run "make" command and you will get several sample applications in "apps" directory. We will describe how to run them a bit later but for now you need to examine the build log and notice all commands which where executed and all required options for them. This will be a basis for your Makefile if you will wish to integrate EFI application to your project. Several pieces of advice:

  • If you build a single binary in your project (e.g. OS loader) you will not need to make static libraries like it is done in the gnu-efi package. Just compile all required C and Assembler file and link them together in the final executable file.
  • If you project has only one target platform you can throw away unnecessary source files. Just select for gnu-efi build log all files which where compiled and throw away all the others.

Here is an example of the package integration. Pay attention to the Makefile. We will touch some its aspects later.

Now we need to run the resulted application(s) and somehow debug it. Qemu virtual machine is a good choice because of its rich built-in debugging functionality.

Download and compile it. Specify your target platform. You can use "--enable-kvm" option to significantly increase emulation speed if you have Linux kernel and kvm package installed.

./configure --prefix=$PREFIX --target-list=x86_64-softmmu --enable-kvm
make
make install

Qemu does not have EFI firmware so you need to download it separately. You can use OVMF firmware based on TianoCore from http://sourceforge.net/apps/mediawiki/tianocore/index.php?title=OVMF. Download 32 or 64 bits version depending on your target platform. Create some directory $YOUR_PREFIX/share/qemu/myOS and place there "vgabios-cirrus.bin" and "OVMF.fd" binaries from the OVMF package.

Making a disk image manually

The last thing left is a disk image with EFI system partition and our application there. EFI requires GUID partitions table and FAT32 filesystem for EFI system partition. We will need gdisk utility from http://sourceforge.net/projects/gptfdisk/ for GUID partitions table creation.

Select some reasonable size of your disk. Below I am assuming that $BYTES_PER_SECTOR is number of bytes per sector on your disk (typically 512) and $NUM_SECTORS is total disk size expressed in sectors. Firstly create disk image initial file filled with zeroes:

export filename=$PREFIX/share/qemu/myOS/myOS.disk
dd if=/dev/zero of=$filename bs=$BYTES_PER_SECTOR count=$NUM_SECTORS

After that create partition table by gdisk:

gdisk $filename

It has interface similar to fdisk utility. Use "o" command to create new partition table, "n" for new partition with default parameters to use the whole disk (partition type "ef00" for EFI system partition), "w" to write all changes and exit. Now you have disk image with GUID partition table on it but the partition is still unformatted. We will use Linux loopback device to access the file as block device. We need to know exact position of the partition on the disk:

gdisk -l $filename

This command will output list of partitions and their first and last sectors indexes. If you have several partitions then just use numbers for your one. Let's assume $START is the first sector index * bytes per sector and $SIZE size is the partition size in bytes. Associate your image file with the loopback device:

losetup --offset $start --sizelimit $size /dev/loop0 $filename

So now we can create filesystem there. We need FAT for EFI system partition. You can use FAT12 if your partition size is small to prevent from big space overhead from larger FAT filesystems:

mkdosfs -F 12 /dev/loop0

Now you can mount your partition to some mount point:

mkdir /tmp/myOsDisk
mount /dev/loop0 /tmp/myOsDisk

Just copy all files you need there (e.g. your EFI application executable image). You can create "stratup.nsh" script which will be executed automatically if no other options are configured in EFI built-in boot-manager. Script could contain just your file launching command with required parameters, e.g. "fs0:\\efi\\boot\\myOsLoader some parameters here". According to specification you can create "/EFI/BOOT/BOOTx64.EFI" file which will be loaded automatically. Finally unmount the partition and release the loopback device:

umount /tmp/myOsDisk
losetup -d /dev/loop0

Your disk image is ready and you can finally launch emulation. Obviously creating disk image could be easily automated in order to not execute these actions manually each time. Automation example can be found there.

Making a disk image (alternative version without root)

You can also get a few tools that do not rely on having certain features in a Linux kernel. Instead, we make use of dd, mtools and parted rather than gdisk and loopback devices. These can be automated into a relatively brief makefile:

FAT_SECTORS:=65536
DISK_SECTORS:=69632
DISK_START:=2048
DISK_END:=67584

TEMP_IMG := temp.img
PARTED := /usr/sbin/parted
PARTED_PARAMS := -s -a minimal

partition.img: $(FILES)
        dd if=/dev/zero of=$(TEMP_IMG) bs=512 count=$(FAT_SECTORS)
        mformat -i $(TEMP_IMG) -h 32 -t 32 -n 64 -c 1 ::
        mcopy -i $(TEMP_IMG) $(FILES) ::
        cp $(TEMP_IMG) $@
        rm $(TEMP_IMG)

hd.img: partition.img
        dd if=/dev/zero of=$(TEMP_IMG) bs=512 count=$(DISK_SECTORS)
        $(PARTED) $(TEMP_IMG) $(PARTED_PARAMS) mklabel gpt
        $(PARTED) $(TEMP_IMG) $(PARTED_PARAMS) mkpart EFI FAT16 $(DISK_START)s $(DISK_END)s
        $(PARTED) $(TEMP_IMG) $(PARTED_PARAMS) toggle 1 boot
        dd if=$(BUILDROOT)/partition.img of=$(TEMP_IMG) bs=512 obs=512 count=$(FAT_SECTORS) seek=$(DISK_START) conv=notrunc
        cp $(TEMP_IMG) $@
        rm $(TEMP_IMG)

Some firmwares only work with FAT32, which means that our disk image should have more clusters than FAT16 allows. Therefore we pick 2¹⁶ sectors as it doesn't fit into a 16-bit number, and make clusters 1 sector in size, which makes the end result close to the shortest possible disk size. Since the bootblocks and partitioning structures need some space of their own, we reserve some space at either end for the total image, here set to 1MB on each end. All the interesting tools here won't create files, so we have to fill them in in advance. A simple dd command can do that.

mformat works perfectly fine on images, but needs manually passed geometry information for performing a format (as it ends up in the FAT structures) Heads, cylinders, and sectors per track are calculated so that they make the entire disk in total (32 * 32 * 64 = 2¹⁶), and we tell it to set the sectors per cluster to one.

Parted works perfectly fine on files, although it will often complain you're doing it wrong if you try. Commands normally performed on the built-in prompt can be passed on the command line. The sequence is to make a GPT disk, create a FAT partition on it (with the sizes specified), then set the bootable flag on the first partition. Note that the command actually toggles the flag, so repeating that command on the same image file is generally a bad idea.

For best use, parted needs a few extra parameters. Pass -s (script) so that it doesn't ask for confirmation on anything - we know what we're doing, and we also set -a (alignment) minimal so that it doesn't magically try to guess the cylinder size, leaving no complaints about our partition boundaries or attempts to shift them for a "better" fit, although the explicit passing of sectors as unit should prevent the latter behaviour from occurring just as well.

Finally, we use a typical dd command to drop the partition contents into the centre of the partitioned disk.

Note that both steps have an intermediate .img file. This covers the case where something goes wrong, and you are left with an file named as the output with the current timestamp, which would cause make to think it's up to date when you retry (when it's actually corrupt), giving you some weird bugs to hunt for later.

Running the emulator

There are some advices for emulation running:

  • Use "-serial" option to have serial console available for the virtual machine. You will have console logs in your terminal and a possibility to use simple ports writing to output debug tracing to serial console.
  • Use "-s" option to enable built-in GDB stab which will wait for connection on TCP port 1234.

Launch Qemu providing path to the directory where your firmware binaries are located:

$PREFIX/bin/qemu-system-x86_64 -L $PREFIX/share/qemu/myOS -bios OVMF.fd -m 768 -cpu kvm64 \
    -vga cirrus -monitor stdio -serial tcp::666,server -s -hdb $PREFIX/share/qemu/myOS/myOS.disk -enable-kvm

Qemu will start and wait for incoming connection to serial console. In the example above it waits on TCP port 666. You can use, for example, socat utility to connect:

socat -,raw,echo=0 tcp4:localhost:666

Once connected the emulation will start. You can use EFI shell command to navigate through filesystems, output system information or launch your application. It has help for all commands so refer to it for details.

Sample application

The next important question is the application debugging. The first moment is that the EFI application should be stopped at some point and wait for debugger. The simplest way to do this is to insert some endless loop in your application. The loop can be enclosed in the block which is executed, for example, when your application receives "--debug" option in its arguments. Let's assume you have inserted such code:

EFI_STATUS
efi_main (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
    EFI_LOADED_IMAGE *loaded_image = NULL;
    EFI_STATUS status;

    InitializeLib(image, systab);
    status = uefi_call_wrapper(systab->BootServices->HandleProtocol,
                               3,
                              image,
                              &LoadedImageProtocol,
                              (void **)&loaded_image);
    if (EFI_ERROR(status)) {
        Print(L"handleprotocol: %r\n", status);
    }

    Print(L"Image base: 0x%lx\n", loaded_image->ImageBase);

    int wait = 1;
    while (wait) {
        __asm__ __volatile__("pause");
    }

    return EFI_SUCCESS;
}

When this code will be executed "pause" instruction will be executed in the loop.

The next thing required for GDB is executable image with symbols. If you carefully examined build log and Makefiles you should note that when EFI executable is created from ELF shared object file only limited set of sections are copied to the resulted image:

.text .sdata .data .dynamic .dynsym .rel .rela .reloc

For having debug symbols we need additionally these sections (in case you have compiled files with "-ggdb" option):

.debug_info .debug_abbrev .debug_loc .debug_aranges .debug_line .debug_macinfo .debug_str

But if you create EFI binary which additionally contains these sections the EFI firmware will be unable to launch it. Fortunately, we do not need the file with debug symbols on the target machine since we will use remote debugging anyway. So what you need is to create two EFI binaries - one with only required sections to upload it to target system and another one with debug symbols to use it with GDB. Actually you just need to run objcopy utility twice with different set of sections to copy and different output files. See Makefile example there.

Now you can launch GDB. You need to specify some file to use as the target binary - you can specify EFI binary with debug symbols but it will have no sense for debugging because the image will be relocated to different address. Note that in the example code above the actual image base address is output. It is required to properly load file with symbols. Let's say after you have launched your application it provided this output:

Image base: 0x2EE30000

So now you need to start GDB, connect to local TCP port 1234 where Qemu is waiting for GDB connection and load image with symbols to relocated address. We need to specify relocated addresses for .text and .data sections. Their addresses in non-relocated binary should be added to image base which is provided in the output above:

# gdb myOS.efi 
GNU gdb (GDB) 7.3
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/John/myOS/source/kernel/boot/build/DEBUG/myOS.efi...(no debugging symbols found)...done.
(gdb) info files 
Symbols from "/home/John/myOS/source/kernel/boot/build/DEBUG/myOS.efi".
Local exec file:
	`/home/John/myOS/source/kernel/boot/build/DEBUG/myOS.efi', file type pei-x86-64.
	Entry point: 0x3000
	0x0000000000003000 - 0x000000000000b9ce is .text
	0x000000000000b9ce - 0x000000000000b9d8 is .reloc
	0x000000000000c000 - 0x000000000000e148 is .data
	0x000000000000f000 - 0x000000000000f0f0 is .dynamic
	0x0000000000010000 - 0x0000000000011098 is .rela
	0x0000000000012000 - 0x0000000000013788 is .dynsym
(gdb) file
No executable file now.
No symbol file now.
(gdb) add-symbol-file debug.myOS.efi 0x2EE33000 -s .data 0x2EE3c000
add symbol table from file "debug.myOS.efi" at
	.text_addr = 0x2ee33000
	.data_addr = 0x2ee3c000
(y or n) y
Reading symbols from /home/John/myOS/source/kernel/boot/build/DEBUG/debug.myOS.efi...done.
(gdb) set architecture i386:x86-64:intel
The target architecture is assumed to be i386:x86-64:intel
(gdb) target remote :1234
Remote debugging using :1234
WaitDebugger () at loader/main.c:80
80	    while (wait) {
(gdb) set variable wait = 0

We need to unload executable binary by "file" command after sections layout is displayed because otherwise its symbols will override debug symbols loaded by "add-symbol-file" command (at least for data section). You do not need to load it each time because sections addresses will change only after next recompilation. Alternatively "objdump" utility can be used to dump sections. As you can see after setup is done you can normally debug your application using whole power of the GDB. Set your "wait" variable to zero and you will exit from endless loop. Set breakpoints/watchpoints, continue execution, enjoy debugging!


UEFI applications in detail

Binary Format

UEFI uses the PE-executable format, with its very own subtypes. Every UEFI application is basically a DLL without symbol tables et al, and another subtypes:

  • UEFI application (10).
  • UEFI boot service driver (11).
  • UEFI run-time driver (12).

TODO - I'd like to show a breakdown of a PE file containing a UEFI application here.

Calling Conventions

UEFI specifies the following calling conventions:

  • cdecl for x86 UEFI functions
  • Microsoft's 64-bit calling convention for x86-64 UEFI functions
  • TODO for Itanium UEFI functions
  • TODO for ARM UEFI functions

This has two impacts on UEFI application developers:

  • A UEFI application's main entry point must expect to be called with the corresponding calling convention.
  • Any UEFI-provided functions that a UEFI application calls must be called with the corresponding calling convention.

Note that functions strictly internal to the application can use whatever calling convention the developer chooses.

GNU-EFI and GCC

cdecl is the standard calling convention used by GCC, so no special attributes or modifiers are needed for writing the main entry point or calling UEFI functions in an x86 UEFI application developed with GNU-EFI. For x86-64, however, the entry point function must be declared with the "___attribute___((ms_abi))" modifier and all calls to UEFI-provided functions must be made through the "uefi_call_wrapper" thunk. This thunk is called with cdecl, but then translates to the Microsoft x86-64 calling convention before calling the requested UEFI function. This is necessary because older releases of GCC do not support specifying calling conventions for function pointers.

For developer convenience, GNU-EFI provides the "EFIAPI" macro, which expands to "cdecl" when targeting x86 and "__attribute__(ms_abi))" when targeting x86-64. Additionally, the "uefi_call_wrapper" thunk will simply pass the call through on x86. This allows the same source code to target x86 and x86-64. For example, the following main function will compile with the correct calling convention on both x86 and x86-64 and the call through the "uefi_call_wrapper" thunk will select the correct calling convention to use when calling the UEFI function (in this case, printing a string).

EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  EFI_STATUS status = uefi_call_wrapper(SystemTable->ConOut->OutputString,
                                        2,
                                        SystemTable->ConOut,
                                        L"Hello, World!\n");
  return status;
}

Language binding

UEFI applications are typically written in C, although bindings could be written for any other language that compiles to machine code. Assembly is also an option; a uefi.inc is available for FASM that allows UEFI applications to be written as below.

format pe64 dll efi
entry main

section '.text' code executable readable

include 'uefi.inc'

main:
    ; initialize UEFI library
    InitializeLib
    jc @f

    ; call uefi function to print to screen
    uefi_call_wrapper ConOut, OutputString, ConOut, _hello

@@: mov eax, EFI_SUCCESS
    retn

section '.data' data readable writeable

_hello                                  db 'Hello World',13,10,0

section '.reloc' fixups data discardable

As a UEFI application contains normal x86 or x86-64 machine code, inline assembly is also an option in compilers that support it.

EFI Byte Code

TODO

Common Problems

My UEFI application hangs/resets after about 5 minutes

When control is handed to your UEFI application by firmware, it sets a watchdog timer for 5 minutes, after which the firmware is reinvoked as it assumes your application has hung. The firmware in this case will normally try to reset the system (although the OVMF firmware in VirtualBox simply causes the screen to go black and hang). To counteract this, you are required to refresh the watchdog timer before it times out. Alternatively, you can disable it completely with code like

BS->SetWatchdogTimer(0, 0, 0, NULL);

Obviously this is not a problem for most bootloaders, but can cause an issue if you have an interactive loader which waits for user input. Also note that you are required to disable the watchdog timer if you exit back to the firmware.

My bootloader hangs if I use user defined EFI_MEMORY_TYPE values

For the memory management functions in EFI, an OS is meant to be able to use "memory type" values above 0x80000000 for its own purposes. In the OVFM EFI firmware release "r11337" (for Qemu, etc) there is a bug where the firmware assumes the memory type is within the range of values defined for EFI's own use, and uses the memory type as an array index. The end result is an "array index out of bounds" bug; where the higher memory type values (e.g. perfectly legal values above 0x80000000) cause the 64-bit version of the firmware to crash (page fault), and cause incorrect "attribute" values to be reported by the 32-bit version of the firmware. This same bug is also present in whatever version of the EFI firmware VirtualBox uses (which looks like an older version of OVFM); and I suspect (but don't know) that the bug may be present in a wide variety of firmware that was derived from the tianocore project (not just OVFM). Brendan 15:30, 29 July 2011 (UTC)

See also

OSDEV

Wikipedia

External Links