UEFI Programming - First Steps

vid, 2009-03-31 Revision: 1.0

In this article, I will describe steps needed to start on with development of real UEFI applications on x86 PC, and share some practical experiences with problems doing so. I will focus on 64-bit version of UEFI, because the 32-bit version isn't much used in this area (most likely due to Microsoft decision not to support UEFI in 32-bit Vista). So, to follow some of my steps here, you'll need a 64-bit CPU (but not 64-bit OS, you can use any 32-bit OS as well). We will finish this article with EFI Hello World application.

This article is continuation of my previous article Introduction to UEFI. Make sure to understand things described there, before reading on.

Of course, anything you try according to this article, you are doing at your own risk.

Getting the hardware

To start UEFI development, first of all you need to get a motherboard whose BIOS has UEFI support. (more precisely we should probably say "whose firmware has UEFI support", but I will use this form). Finding whether particular BIOS has UEFI support often turns out to be quite complicated task. Motherboard manufacturers license BIOS from other companies, usually from AMI (Aptio, AMIBIOS), Phoenix (SecureCore, TrustedCore, AwardCore) or Insyde (InsydeH20). Forget about determining UEFI support just by end-user stats you see in most shops. Since UEFI support is still only in somewhat experimental state, in many cases it isn't even listed in motherboard technical specification. In such case you are left to googling and asking on forums, where you often get only internal brand name that is often hard to match with end-user product designation.

One trick that I found out to work for Intel boards (but it may very well work for other boards as well) is to look at BIOS Update Release Notes, e. g. the document which lists changes and fixes of BIOS. If board has UEFI support, you will probably find UEFI mentioned there (and only there in case of Intel).

In short, determining UEFI support is much harder than it may seem. Some machines that have this technology are listed here. I use Intel DG33BU board (it was marketed as Intel DG33BUC for some reason).

You will also need some place to boot from. In theory just USB pen should be enough, but in practice none of 4 brands I tried worked with my board's UEFI implementation. So you may have to use harddrive. I strongly suggest IDE drive, because SATA drives may need some tweaking of BIOS settings, or they may not work at all. Like the USB pens, USB keyboard might be a problem too. I wouldn't fear this that much, but if you can use PS/2 keyboard, do so.

Getting the software

To go on with UEFI development, you will need two development packages: EFI Development Kit (EDK) and EFI Toolkit.

First package, the EFI Development Kit contains the TianoCore (public part of reference UEFI implementation by Intel) source code, along with many examples and binaries of EFI Shell (we'll talk about this later), all ready to be built with nice make system. It even can be built into a Win32 UEFI emulator for more convinient development and testing, but I won't cover that here. I also won't demonstrate usage of EDK build system in this article. Even though it might be a good idea for real world project, I want to give you bit more insight about what goes "under the hood" here.

Second package, the EFI Toolkit is set of extra UEFI applications like FTP client, Python port, text editor, etc. Strictly speaking, we don't really need this, but it has a set of C headers that is somewhat easier to use than those of EDK (unless you take advantage of EDK build system for your project). However, note that it doesn't contain all headers yet - you will quickly run into this problem if you try some real development with it.

Along with headers, you also will need documentation of the UEFI interface. That can be downloaded after filling simple form at http://www.uefi.org/specs. This is the official UEFI specification.

Except for UEFI specification, there is also another document called Platform Initialization Specification. This describes implementation of UEFI initialization stages (before drivers are loaded and applications can be executed), and more importantly for us, it also describes interface of routines used to implement UEFI. We can understand this Platform Initialization Specification as description of Tiano UEFI implementation. It provides more lowlevel control than UEFI, in cases when such control is needed. Strictly speaking, someone may implement UEFI Specification without implementing anything from Platform Initialization Specification, but that's not very likely to happen in real world.

Last but not least, you will need 64-bit C compiler. I suggest Microsoft Visual C++, whose 64-bit version is freely available in Windows DDK. You can get it here.

Booting into EFI Shell

Up to this point, you may have lacked any visual idea about UEFI. It was just a programatic interface, after all. Purpose of this chapter would be to overcome this, by booting the so-called EFI Shell. EFI shell is much like any other shell you know: you have a command line, through which you can enter commands to browse disk, run applications, configure system, etc. Only difference is that EFI shell is only built using UEFI services, and as such it doesn't require any operating system to run. Working in EFI shell feels much like working in some very simple operating system. From this shell you will also run your applications (later in this article).

Actual steps of booting EFI shell might vary a lot among different BIOS brands. Some BIOSes (mostly on MACs where EFI is the primary standard) have extra options for specifying file to boot, or can even have EFI shell built-in inside ROM. However, I only have experience with non-MAC Intel UEFI implementation, that only seems to support the very minimum of features required to install EFI versions of Windows. Microsoft requirements on UEFI implementation, among other things, specify that UEFI boot loader must use fixed path to file to boot, and if it can't boot UEFI for some reason, it must silently switch to default boot. That causes lot of headache when your UEFI doesn't boot as it should, and you must find out what the problem is without any information from loader.

For 64-bit UEFI implementations, the path to file that is booted is \EFI\BOOT\BOOTX64.EFI. UEFI boot loader searches all filesystems it can access for this file, and when it finds it, it is executed. As I already said, if the file isn't found, booting continues with legacy BIOS. UEFI boot loader can read MBR or GPT partition tables, and can only read FAT32 partitions. This includes USB drives, so it is possible to boot EFI shell from FAT32-formatted USB pen. Unfortunately, in my tests 3 of 4 USB pens didn't work, and the 4th stopped working too after BIOS update. I had similar problem with one of two SATA drives not working with UEFI. Therefore, I strongly suggest to use IDE drive, if you have any problems. If you already have at least one FAT32 partition on your drive, you can use it, otherwise you need to create fresh new one.

Now you need to copy binary of EFI shell to that partition. You can find 64-bit binary of EFI shell in EFI Development Kit: {EDK}\Other\Maintained\Application\UefiShell\bin\x64\Shell_full.efi. Copy it to your FAT32 partition as \EFI\BOOT\BOOTX64.EFI.

Now reboot, and enter into BIOS settings. In the Boot tab, you should see an UEFI Boot option, enable it. If everything is allright (likely not), now after reboot you should end up in the EFI Shell. If you did, congratulations. If you didn't, and instead your regular OS booted as usual, it most likely means that UEFI boot manager wasn't able to access the drive.

First try to enter Boot Menu during BIOS screen (F10 on my machine). If the EFI Shell on FAT32 partition was detected, but didn't boot, you will see it as one of option (in my case [Internal EFI Shell--Harddrive]). If you see it, just run it. This might possibly happen if you already have some EFI operating system installed, and it has written itself as default EFI boot entry to NVRAM.

If you don't see EFI Shell in the Boot Menu, it means UEFI Boot Loader wasn't able to find any FAT32 drive with \EFI\BOOT\BOOTX64.EFI. If you are trying to boot EFI shell from USB key, try tweaking USB emulation settings in BIOS. Same applies to SATA disks and SATA/IDE settings. If none of settings work, or your machine failed to boot from IDE drive, I can't help you any more than to doublecheck everything I wrote so far (especially typos in path to EFI shell).

If you have some experience with booting EFI shell not covered in this chapter, please drop me a message, so I can update this article.

Building an UEFI application

So, we are in EFI shell. That means we can finally test any (64-bit) EFI application we write. Time to start writing one. We will of course write and compile applications in normal operating system, not in EFI shell.

EFI application or driver is just a plain Windows PE DLL file, just with different subsystem value in header. There are 3 new values: EFI application = 10, EFI boot service driver = 11, efi runtime driver = 12 (numbers are decimal). Question how to set subsystem will be answered later, for now let's focus on the DLL file.

EFI PE application doesn't have any fancies we have in Windows PEs, like symbol tables, exports, static exception handling data, etc. It does not even have imports - all you will ever need in EFI application is passed as argument to entry point function. Only thing needed apart from data and code are relocations. So, this is simplest EFI application:

#include <efi.h>

EFI_STATUS main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  return EFI_SUCCESS;
}

Compiling it with MS Visual C (supply your own path to EFI toolkit):

   
cl /c /Zl /I"{EFI_Toolkit}\include\efi" /I"{EFI_Toolkit}\include\efi\em64t" hello.c

Here we set path to common EFI headers, and to platform-specific EFI headers. The /c switch disables linking (we will link separately for better readability), and /Zl disables dependency on default libc libraries.

Linking:

   
link /entry:main /dll /IGNORE:4086 hello.obj

The /entry:main overrides default libc entry point to our main() function. The /dll forces creation of relocations. And the IGNORE:4086 disables warning LNK4086, that is caused by nonstandard main() arguments.

Now we have windows DLL, we just need to change PE subsystem value to EFI. For that, EFI Toolkit contains simple (buggy) utility that changes subsystem to one of 3 EFI values. We find this utility in EFI_Toolkit\build\tools\bin\fwimage.exe. To set subsystem to EFI app, we'll use it like this:

   
fwimage app hello.dll hello.efi

Produced file hello.efi should now be functional empty EFI application. We just need to copy it to our FAT32 partition, reboot to EFI shell, and test it:

Shell> fs0:
fs0:> .\hello.efi

If we don't get any error message, application worked.

UEFI Programming

Now we finally can get deeper into UEFI programming. Your main source for this information should be the UEFI specification, I will only sum up most basic points.

First of all, we should know something about environemnt of UEFI applications. I will describe only environment of 64-bit x86 UEFI here (other to be found in UEFI specification).

UEFI runs in uniprocessor flat 64-bit long mode. Usually UEFI runs with memory mapping disabled (physical RAM address = virtual RAM address), but since 64-bit x86 mode requires mapping to be enabled, UEFI maps entire memory so that virtual address is same as physical (i. e. mapping is transparent). Calling convention is usual 64-bit fastcall (first 4 arguments in RCX, RDX, R8, R9 with space reserved on stack; rest of arguments passed by stack; RAX, R10, R11 and XMM0-XMM5 not preserved by called function), so you don't need to worry about special compiler settings. Notable feature of EFI is that for every supported architecture, it defines exact binary interface (ABI).

Now let's look at how our application interacts with UEFI services. First, UEFI provides set of services, called Boot Services. These are available to EFI drivers, applications, and to OS boot loader during boot. At some point during OS booting, OS loader can decide to drop them, and after that point those services become unavailable. There is also a little number of services that always remain available, called "Runtime Services". Apart from these two sets of services, all that UEFI offers is available through so-called protocols. Protocol is very much like a class in object oriented programming. UEFI defines set of protocols itself (for example protocols handling USB, filesystem, compression, network, …), and application can define its own protocols (hence the "Extensible" in "Unified Extensible Firmware Interface"). Protocols are identified by GUID (Global Unique Identifier, google it if you don't know what it is). Only very few protocols are mandatory in UEFI specification, and all the rest may or may not be implemented in particular firmware. If protocol isn't implemented in firmware, you can load a driver that implements it, or even write such driver yourself.

Now let's look at how to access these services. As I already explained, all you ever need is passed as argument to entry point function. Prototype of entry point function looks like this:

EFI_STATUS main(
  EFI_HANDLE ImageHandle, 
  EFI_SYSTEM_TABLE *SystemTable
)

First argument is handle of our process, nothing extra to say about it. Second is pointer to EFI_SYSTEM_TABLE, the top-level EFI structure, which keeps references to everything there is: boot/runtime services, drivers, protocol implementations, memory maps, etc. It is good idea to always save both these arguments in a global variable, so you can access them from anywhere in source code. You can find detailed description of EFI System Table in UEFI Specification chapter 4 - EFI System Table. Its C definition looks like this:

typedef struct _EFI_SYSTEM_TABLE {
  EFI_TABLE_HEADER                Hdr;
  CHAR16                          *FirmwareVendor;
  UINT32                          FirmwareRevision;

  EFI_HANDLE                      ConsoleInHandle;
  SIMPLE_INPUT_PROTOCOL           *ConIn;
  EFI_HANDLE                      ConsoleOutHandle;
  SIMPLE_TEXT_OUTPUT_PROTOCOL     *ConOut;
  EFI_HANDLE                      StandardErrorHandle;
  SIMPLE_TEXT_OUTPUT_PROTOCOL     *StdErr;

  EFI_RUNTIME_SERVICES            *RuntimeServices;
  EFI_BOOT_SERVICES               *BootServices;

  UINTN                           NumberOfTableEntries;
  EFI_CONFIGURATION_TABLE         *ConfigurationTable;
} EFI_SYSTEM_TABLE;

Here we see references to boot and runtime services, three standard I/O handles (as implementations of SIMPLE_TEXT_OUTPUT and SIMPLE_INPUT protocols), and pointer to Configuration Table. Configuration Table holds references to all other protocol implementations currently active in system.

First we will show example of using Boot Service. The EFI_BOOT_SERVICES is just a structure that holds pointers to functions described in UEFI Specification chapter 6: Services - Boot Services. For now we will use only simple Exit() function, that terminates current EFI application immediately.

#include <efi.h>

EFI_STATUS main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  SystemTable->BootServices->Exit();
  // we never get here
  return EFI_SUCCESS;
}

Now, we will show simple Hello World application, using the ConOut implementation of SIMPLE_TEXT_OUTPUT protocol. This protocol is described in UEFI Specification chapter 11.4 - Simple Text Output Protocol. Its C header looks like this:

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
  EFI_TEXT_RESET                Reset;
  EFI_TEXT_STRING               OutputString;
  EFI_TEXT_TEST_STRING          TestString; 
  EFI_TEXT_QUERY_MODE           QueryMode;
  EFI_TEXT_SET_MODE             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;

We are of course interested in OutputString() function, whose prototype is:

EFI_STATUS OutputString (
  IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *This,
  IN CHAR16                           *String
);

Note that UEFI uses Unicode strings only, hence the CHAR16 *String. This pointer meaning is exactly same as in any object oriented programming. With this info, we should be able to write Hello World app easily:

#include <efi.h>

EFI_STATUS main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World\r\n");
  return EFI_SUCCESS;
}

Also note that UEFI uses CRLF line terminators (\r\n) instead of just LF (\n), and when we use native EFI functions, there is no layer which reinterprets LF to CRLF. Normally, applications use additional library called EFILIB which does the LF->CRLF transform.

UEFI Programming with FASM

As an extra, I will also demonstrate same Hello World example in assembly (using FASM, that currently has experimental UEFI support since version 1.67.28):

First we need some to create simple UEFI headers (efi.inc):

;for 32/64 portability and automatic natural align in structure definitions

struc int8 {
  . db ?
}
struc int16 {
  align 2
  . dw ?
}
struc int32 {
  align 4
  . dd ?
}
struc int64 {
  align 8
  . dq ?
}
struc intn {
  align 8
  . dq ?
}
struc dptr {
  align 8
  . dq ?
}

;symbols

EFIERR = 0x8000000000000000
EFI_SUCCESS			= 0
EFI_LOAD_ERROR			= EFIERR or 1
EFI_INVALID_PARAMETER		= EFIERR or 2
EFI_UNSUPPORTED 		= EFIERR or 3
EFI_BAD_BUFFER_SIZE		= EFIERR or 4
EFI_BUFFER_TOO_SMALL		= EFIERR or 5
EFI_NOT_READY			= EFIERR or 6
EFI_DEVICE_ERROR		= EFIERR or 7
EFI_WRITE_PROTECTED		= EFIERR or 8
EFI_OUT_OF_RESOURCES		= EFIERR or 9
EFI_VOLUME_CORRUPTED		= EFIERR or 10
EFI_VOLUME_FULL 		= EFIERR or 11
EFI_NO_MEDIA			= EFIERR or 12
EFI_MEDIA_CHANGED		= EFIERR or 13
EFI_NOT_FOUND			= EFIERR or 14
EFI_ACCESS_DENIED		= EFIERR or 15
EFI_NO_RESPONSE 		= EFIERR or 16
EFI_NO_MAPPING			= EFIERR or 17
EFI_TIMEOUT			= EFIERR or 18
EFI_NOT_STARTED 		= EFIERR or 19
EFI_ALREADY_STARTED		= EFIERR or 20
EFI_ABORTED			= EFIERR or 21
EFI_ICMP_ERROR			= EFIERR or 22
EFI_TFTP_ERROR			= EFIERR or 23
EFI_PROTOCOL_ERROR		= EFIERR or 24

;helper macro for definition of relative structure member offsets

macro struct name
{
  virtual at 0
    name name
  end virtual
}

;structures

struc EFI_TABLE_HEADER {
 .Signature    int64
 .Revision     int32
 .HeaderSize   int32
 .CRC32        int32
 .Reserved     int32
}
struct EFI_TABLE_HEADER

struc EFI_SYSTEM_TABLE {
 .Hdr		       EFI_TABLE_HEADER
 .FirmwareVendor       dptr
 .FirmwareRevision     int32
 .ConsoleInHandle      dptr
 .ConIn 	       dptr
 .ConsoleOutHandle     dptr
 .ConOut	       dptr
 .StandardErrorHandle  dptr
 .StdErr	       dptr
 .RuntimeServices      dptr
 .BootServices	       dptr
 .NumberOfTableEntries intn
 .ConfigurationTable   dptr
}
struct EFI_SYSTEM_TABLE

struc SIMPLE_TEXT_OUTPUT_INTERFACE {
 .Reset 	    dptr
 .OutputString	    dptr
 .TestString	    dptr
 .QueryMode	    dptr
 .SetMode	    dptr
 .SetAttribute	    dptr
 .ClearScreen	    dptr
 .SetCursorPosition dptr
 .EnableCursor	    dptr
 .Mode		    dptr
}
struct SIMPLE_TEXT_OUTPUT_INTERFACE

And here is the assembly code itself (hello.asm):

format pe64 dll efi
entry main

section '.text' code executable readable

include 'efi.inc'

main:
 sub rsp, 4*8              ; reserve space for 4 arguments

 mov [Handle], rcx         ; ImageHandle
 mov [SystemTable], rdx    ; pointer to SystemTable

 lea rdx, [_hello]
 mov rcx, [SystemTable]
 mov rcx, [rcx + EFI_SYSTEM_TABLE.ConOut]
 call [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString]

 add rsp, 4*8
 mov eax, EFI_SUCCESS
 retn


section '.data' data readable writeable

Handle      dq ?
SystemTable dq ?
_hello      du 'Hello World',13,10,'(From EFI app written in FASM)',13,10,0

section '.reloc' fixups data discardable

Compile and link it with fasm.exe hello_world.asm.

That's all for now, hope you enjoyed yourselves.


Comments

Continue to discussion board.

You can contact the author using e-mail vid@x86asm.net.

Visit author's home page.


Revisions

2009-03-31 1.0 First public version vid

(dates format correspond to ISO 8601)