UEFI: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
Line 359: Line 359:
* '''Microsoft's 64-bit calling convention''' for x86-64 UEFI functions
* '''Microsoft's 64-bit calling convention''' for x86-64 UEFI functions
* TODO for Itanium UEFI functions
* TODO for Itanium UEFI functions
* TODO for ARM UEFI functions
* '''SMC''' for ARM UEFI functions


This has two impacts on UEFI application developers:
This has two impacts on UEFI application developers:

Revision as of 15:19, 24 November 2015

(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+.

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
  • SMC 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

SystemTable->BootServices->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