[personal profile] mjg59
It's not difficult to write a UEFI application under Linux. You'll need three things:

  • An installed copy of the gnu-efi library and headers
  • A copy of the UEFI specification from here
  • Something running UEFI (OVMF is a good option if you don't have any UEFI hardware)

So, let's write a trivial UEFI application. Here's one from the gnu-efi source tree:
#include <efi.h>
#include <efilib.h>

EFI_STATUS
efi_main (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
        SIMPLE_TEXT_OUTPUT_INTERFACE *conout;

        conout = systab->ConOut;
        InitializeLib(image, systab);
        uefi_call_wrapper(conout->OutputString, 2, conout, L"Hello World!\n\r");

        return EFI_SUCCESS;
}
The includes are fairly obvious. efi.h gives you UEFI functions defined by the specification, and efilib.h gives you functions provided by gnu-efi. The runtime will call efi_main() rather than main(), so that's our main function. It returns an EFI_STATUS which will in turn be returned to whatever executed the binary. The EFI_HANDLE image is a pointer to the firmware's context for this application, which is used in certain calls. The EFI_SYSTEM_TABLE *systab is a pointer to the UEFI system table, which in turn points to various other tables.

So far so simple. Now things get interesting. The heart of the UEFI programming model is a set of protocols that provide interfaces. Each of these interfaces is typically a table of function pointers. In this case, we're going to use the simple text protocol to print some text. First, we get a pointer to the simple text output table. This is always referenced from the system table, so we can simply assign it. InitializeLib() just initialises some internal gnu-efi functions - it's not technically required if we're purely calling native UEFI functions as we are here, but it's good style since forgetting it results in weird crashes.

Anyway. We now have conout, a pointer to a SIMPLE_TEXT_OUTPUT_INTERFACE structure. This is described in section 11.4 of the UEFI spec, but 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;
The majority of these are functions - the only exception is the SIMPLE_TEXT_OUTPUT_MODE pointer, which points to the current mode. In an ideal world you'd be able to just call these directly, but sadly UEFI and Linux calling conventions are different and we need some thunking. That's where the uefi_call_function() call comes in. This takes a UEFI function as its first argument (in this case, conout->OutputString), the number of arguments (2, in this case) and then the arguments. Checking the spec for OutputString, we see this:
typedef
EFI_STATUS
(EFIAPI *EFI_TEXT_STRING) (
    IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
    IN CHAR16 *String
);
The first argument is a pointer to the specific instance of the simple text output protocol. This permits multiple instances of the same protocol to exist on a single machine, without having to duplicate all of the code. The second argument is simply a UCS-2 string. Our conout pointer is a pointer to the protocol, so we pass that as the first argument. For the second argument, we pass L"Hello World!\n", with the L indicating that this is a wide string rather than a simple 8-bit string. uefi_call_function() then rearranges these arguments into the appropriate calling convention and calls the UEFI function. The firmware then prints "Hello World!" on the screen. Success.

(Warning: uefi_call_function() does no type checking on its arguments, so if you pass in a struct instead of a pointer it'll build without complaint and then give you a bizarre error at runtime)

Finally, we clean up by simply returning EFI_SUCCESS. Nothing more for us to worry about. That's the simple case. What about a more complicated one? Let's do something with that EFI_HANDLE that got passed into efi_main().
#include <efi.h>
#include <efilib.h>

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

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

        Print(L"Image base        : %lx\n", loaded_image->ImageBase);
        Print(L"Image size        : %lx\n", loaded_image->ImageSize);
        Print(L"Image file        : %s\n", DevicePathToStr(loaded_image->FilePath));
        return EFI_SUCCESS;
}
UEFI handles can have multiple protocols attached to them. A handle may represent a piece of physical hardware, or (as in this case) it can represent a software object. In this case we're going to get a pointer to the loaded image protocol that's associated with the binary we're running. To do this we call the HandleProtocol() function from the Boot Services table. Boot services are documented in section 6 of the UEFI specification and are the interface to the majority of UEFI functionality. HandleProtocol takes an image and a protocol identifier, and hands back a pointer to the protocol. UEFI protocol identifiers are all GUIDs and defined in the specification. If you call HandleProtocol on a handle that doesn't implement the protocol you request, you'll simply get an error back in the status argument. No big deal.

The loaded image protocol is fairly different to the simple text output protocol in that it's almost entirely data rather than function pointers. In this case we're printing the address that our executable was loaded at and the size of our relocated executable. Finally, we print the path of the file. UEFI device paths are documented in section 9.2 of the UEFI specification. They're a binary representation of a file path, which may include the path of the device that the file is on. DevicePathToStr is a gnu-efi helper function that converts the objects it understands into a textual representation. It doesn't cover the full set of specified UEFI device types, so in some corner cases it may print "Unknown()".

That covers how to use protocols attached to the image handle. How about protocols that are attached to other handles? In that case we can do something like this:
#include <efi.h>
#include <efilib.h>

EFI_STATUS
efi_main (EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *systab)
{
        EFI_STATUS status;
        EFI_GRAPHICS_OUTPUT_PROTOCOL *gop;
        EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;

        InitializeLib(image_handle, systab);

        status = uefi_call_wrapper(systab->BootServices->LocateProtocol,
                                   3,
                                   &gop_guid,
                                   NULL,
                                   &gop);

        Print(L"Framebuffer base is at %lx\n", gop->Mode->FrameBufferBase);

        return EFI_SUCCESS;
}
This will find the first (where first is an arbitrary platform ordering) instance of a protocol and return a pointer to it. If there may be multiple instances of a protocol (as there often are with the graphics output protocol) you should use LocateHandleBuffer() instead. This returns an array of handles that implement the protocol you asked for - you can then use HandleProtocol() to give you the instance on the specific handle. There's various helpers in gnu-efi like LibLocateHandle() that make these boilerplate tasks easier. You can find the full list in /usr/include/efi/efilib.h.

That's a very brief outline of how to use basic UEFI functionality. The spec documents the full set of UEFI functions available to you, including things like direct block access, filesystems and networking. The process is much the same in all cases - you locate the handle that you want to interact with, open the protocol that's installed on it and then make calls using that protocol.

Building these examples

Building these examples is made awkward due to UEFI wanting PE-COFF binaries and Linux toolchains building ELF binaries. You'll want a makefile something like this:
ARCH            = $(shell uname -m | sed s,i[3456789]86,ia32,)
LIB_PATH        = /usr/lib64
EFI_INCLUDE     = /usr/include/efi
EFI_INCLUDES    = -nostdinc -I$(EFI_INCLUDE) -I$(EFI_INCLUDE)/$(ARCH) -I$(EFI_INCLUDE)/protocol

EFI_PATH        = /usr/lib64/gnuefi
EFI_CRT_OBJS    = $(EFI_PATH)/crt0-efi-$(ARCH).o
EFI_LDS         = $(EFI_PATH)/elf_$(ARCH)_efi.lds

CFLAGS          = -fno-stack-protector -fpic -fshort-wchar -mno-red-zone $(EFI_INCLUDES)
ifeq ($(ARCH),x86_64)
        CFLAGS  += -DEFI_FUNCTION_WRAPPER
endif

LDFLAGS         = -nostdlib -znocombreloc -T $(EFI_LDS) -shared -Bsymbolic -L$(EFI_PATH) -L$(LIB_PATH) \
                  $(EFI_CRT_OBJS) -lefi -lgnuefi

TARGET  = test.efi
OBJS    = test.o
SOURCES = test.c

all: $(TARGET)

test.so: $(OBJS)
       $(LD) -o $@ $(LDFLAGS) $^ $(EFI_LIBS)

%.efi: %.so
        objcopy -j .text -j .sdata -j .data \
                -j .dynamic -j .dynsym  -j .rel \
                -j .rela -j .reloc -j .eh_frame \
                --target=efi-app-$(ARCH) $^ $@
There's rather a lot going on here, but the important stuff is the CFLAGS line and everything after that. First, we need to disable the stack protection code - there's nothing in the EFI runtime for it to call into, so we'll build a binary with unresolved symbols otherwise. Second, we need to build position independent code, since UEFI may relocate us anywhere. short-wchar is needed to indicate that strings like L"Hi!" should be 16 bits per character, rather than gcc's default of 32 bits per character. no-red-zone tells the compiler not to assume that there's 256 bytes of stack available to it. Without this, the firmware may modify stack that the binary was depending on.

The linker arguments are less interesting. We simply tell it not to link against the standard libraries, not to merge relocation sections, and to link in the gnu-efi runtime code. Nothing very exciting. What's more interesting is that we build a shared library. The reasoning here is that we want to perform all our linking, but we don't want to build an executable - the Linux executable runtime code would be completely pointless in a UEFI binary. Once we have our .so file, we use objcopy to pull out the various sections and rebuild them into a PE-COFF binary that UEFI will execute.

Calling convention

Date: 2012-10-30 04:11 pm (UTC)
From: (Anonymous)
What calling convention does UEFI use? stdcall, or some strange variant of it?

Could GCC learn to support that calling convention, so that it could become an __attribute__ on the function declaration, and allow direct calls with proper typechecking?

Re: Calling convention

Date: 2012-10-30 04:24 pm (UTC)
From: (Anonymous)
Could UEFI calls on 32-bit just use __attribute__((stdcall)) then, allowing direct invocation without a type-unsafe wrapper?

Does GCC have any support for the Microsoft x64 convention?

Re: Calling convention

Date: 2012-10-30 04:53 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
GCC does support that now, and function pointers will work correctly. Last I brought it up in terms of gnu-efi, the sentiment was that the feature was still pretty new, and gnu-efi needs to work with older versions of GCC.

Re: Calling convention

Date: 2012-10-30 08:19 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
You put the function attribute on both the header and the function, and make sure you build with -maccumulate-outgoing-args. So basically your C file looks like:

#include
[Error: Irreparable invalid markup ('<stdio.h>') in entry. Owner must fix manually. Raw contents below.]

You put the function attribute on both the header and the function, and make sure you build with -maccumulate-outgoing-args. So basically your C file looks like:

#include <stdio.h>

#define EFI_ABI __attribute__((ms_abi))

int EFI_ABI x(int a, int b, int c, int d, int e, int f, int g, int h);

int EFI_ABI x(int a, int b, int c, int d, int e, int f, int g, int h)
{
printf("%d%d%d%d%d%d%d%d", a, b, c, d, e, f, g, h);
return 0;
}

int main(void)
{
int rc = x(1,2,3,4,5,6,7,8);
return rc;
}

And the compiler generates the resulting giant thunking nightmare:

eddie:~$ gcc -c -o foo -Wall -Werror foo.c -maccumulate-outgoing-args
eddie:~$ objdump -d foo

foo: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <x>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 57 push %rdi
5: 56 push %rsi
6: 48 81 ec c0 00 00 00 sub $0xc0,%rsp
d: 0f 29 74 24 20 movaps %xmm6,0x20(%rsp)
12: 0f 29 7c 24 30 movaps %xmm7,0x30(%rsp)
17: 44 0f 29 44 24 40 movaps %xmm8,0x40(%rsp)
1d: 44 0f 29 4d 80 movaps %xmm9,-0x80(%rbp)
22: 44 0f 29 55 90 movaps %xmm10,-0x70(%rbp)
27: 44 0f 29 5d a0 movaps %xmm11,-0x60(%rbp)
2c: 44 0f 29 65 b0 movaps %xmm12,-0x50(%rbp)
31: 44 0f 29 6d c0 movaps %xmm13,-0x40(%rbp)
36: 44 0f 29 75 d0 movaps %xmm14,-0x30(%rbp)
3b: 44 0f 29 7d e0 movaps %xmm15,-0x20(%rbp)
40: 89 4d 10 mov %ecx,0x10(%rbp)
43: 89 55 18 mov %edx,0x18(%rbp)
46: 44 89 45 20 mov %r8d,0x20(%rbp)
4a: 44 89 4d 28 mov %r9d,0x28(%rbp)
4e: 44 8b 4d 30 mov 0x30(%rbp),%r9d
52: 44 8b 45 28 mov 0x28(%rbp),%r8d
56: 8b 55 20 mov 0x20(%rbp),%edx
59: 8b 45 18 mov 0x18(%rbp),%eax
5c: 8b 4d 48 mov 0x48(%rbp),%ecx
5f: 89 4c 24 10 mov %ecx,0x10(%rsp)
63: 8b 4d 40 mov 0x40(%rbp),%ecx
66: 89 4c 24 08 mov %ecx,0x8(%rsp)
6a: 8b 4d 38 mov 0x38(%rbp),%ecx
6d: 89 0c 24 mov %ecx,(%rsp)
70: 89 d1 mov %edx,%ecx
72: 89 c2 mov %eax,%edx
74: 8b 75 10 mov 0x10(%rbp),%esi
77: bf 00 00 00 00 mov $0x0,%edi
7c: b8 00 00 00 00 mov $0x0,%eax
81: e8 00 00 00 00 callq 86 <x+0x86>
86: b8 00 00 00 00 mov $0x0,%eax
8b: 0f 28 74 24 20 movaps 0x20(%rsp),%xmm6
90: 0f 28 7c 24 30 movaps 0x30(%rsp),%xmm7
95: 44 0f 28 44 24 40 movaps 0x40(%rsp),%xmm8
9b: 44 0f 28 4d 80 movaps -0x80(%rbp),%xmm9
a0: 44 0f 28 55 90 movaps -0x70(%rbp),%xmm10
a5: 44 0f 28 5d a0 movaps -0x60(%rbp),%xmm11
aa: 44 0f 28 65 b0 movaps -0x50(%rbp),%xmm12
af: 44 0f 28 6d c0 movaps -0x40(%rbp),%xmm13
b4: 44 0f 28 75 d0 movaps -0x30(%rbp),%xmm14
b9: 44 0f 28 7d e0 movaps -0x20(%rbp),%xmm15
be: 48 81 c4 c0 00 00 00 add $0xc0,%rsp
c5: 5e pop %rsi
c6: 5f pop %rdi
c7: 5d pop %rbp
c8: c3 retq

00000000000000c9 <main>:
c9: 55 push %rbp
ca: 48 89 e5 mov %rsp,%rbp
cd: 48 83 ec 50 sub $0x50,%rsp
d1: c7 44 24 38 08 00 00 movl $0x8,0x38(%rsp)
d8: 00
d9: c7 44 24 30 07 00 00 movl $0x7,0x30(%rsp)
e0: 00
e1: c7 44 24 28 06 00 00 movl $0x6,0x28(%rsp)
e8: 00
e9: c7 44 24 20 05 00 00 movl $0x5,0x20(%rsp)
f0: 00
f1: 41 b9 04 00 00 00 mov $0x4,%r9d
f7: 41 b8 03 00 00 00 mov $0x3,%r8d
fd: ba 02 00 00 00 mov $0x2,%edx
102: b9 01 00 00 00 mov $0x1,%ecx
107: e8 00 00 00 00 callq 10c <main+0x43>
10c: 89 45 fc mov %eax,-0x4(%rbp)
10f: 8b 45 fc mov -0x4(%rbp),%eax
112: c9 leaveq
113: c3 retq

Re: Calling convention

Date: 2012-10-30 08:24 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
... And by way of demonstrating function pointers, this works exactly the same:

#include
[Error: Irreparable invalid markup ('<stdio.h>') in entry. Owner must fix manually. Raw contents below.]

... And by way of demonstrating function pointers, this works exactly the same:

#include <stdio.h>

#define EFI_ABI __attribute__((ms_abi))

int EFI_ABI x(int a, int b, int c, int d, int e, int f, int g, int h);

int EFI_ABI x(int a, int b, int c, int d, int e, int f, int g, int h)
{
printf("%d%d%d%d%d%d%d%d", a, b, c, d, e, f, g, h);
return 0;
}

typedef int EFI_ABI (*y_func)(int a, int b, int c, int d,
int e, int f, int g, int h);

struct poorstruct {
y_func y;
};

int main(void)
{
struct poorstruct z = {
.y = x
};

int rc = z.y(1,2,3,4,5,6,7,8);
return rc;
}

(in fact the disassembly is almost exactly the same.) If you build that without -c, it even runs fine - it thunks both ways, as you can see in the objdump output above.

Re: Calling convention

Date: 2012-10-30 08:49 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
The problem there is that the /generated/ function has the thunking code, rather than the call point. So we'd still need a wrapper function that includes the thunking, it'd just be generated code. But you'd still lose the type-safety aspect.

Re: Calling convention

Date: 2012-10-31 01:07 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
Er.. The more I think about it and look at the objdump, the more it obviously happens on both sides.

Re: Calling convention

Date: 2012-10-31 02:43 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
For those playing the home game: https://github.com/vathpela/gnu-efi/commit/006379eb54c4cfc9e6da3979c2c91a294e3b618f .

This means that from now on you can just compile the same code with -DGNU_EFI_USE_MS_ABI , and you'll get working type checking.

Re: Calling convention

Date: 2012-10-31 11:45 pm (UTC)
From: [personal profile] lersek
(What is this "home game" you both keep mentioning?)

I think the patchlevel should be lowered to 6 (as in gcc-4.4.6, which is in RHEL-6), possibly even lower. edk2 supports it. I build OvmfPkg with gcc-4.4.6. If you look at "BaseTools/Conf/tools_def.template" in the edk2 tree, GCC44_X64_CC_FLAGS contains "-DEFIAPI=__attribute__((ms_abi))".

Re: Calling convention

Date: 2012-11-01 11:31 am (UTC)
From: [identity profile] pjones.id.fedoraproject.org
Well, it'll actually be higher than that as we need https://bugzilla.redhat.com/show_bug.cgi?id=871889 to be fixed.

The "home game" is a reference to television game shows that sell versions of the game you can play at home. The phrase is sometimes used in discussions on the internet when part of the discussion happens in private or at another place, and somebody is posting more for the benefit of those who were incidentally excluded from following along.

Re: Calling convention

Date: 2012-11-01 01:42 pm (UTC)
From: [personal profile] lersek
Re RHBZ#871889

The edk2 tree obviously uses the ellipsis notation (...) and it builds well with gcc-4.4.6. So as one example I checked UnicodeSPrintAsciiFormat() in MdePkg/Library/BasePrintLib/PrintLib.c. It uses VA_START() style macros.

VA_START() and co. have three definitions in MdePkg/Include/Base.h.

The first is for ARM.

The second is for gcc if NO_BUILTIN_VA_FUNCS is absent (ie. builtins are allowed). However GCC44_X64_CC_FLAGS in BaseTools/Conf/tools_def.template defines NO_BUILTIN_VA_FUNCS. Thus the second definition group, which would use __builtin_va_start() and friends, is not used.

The third definition group takes effect:

#define VA_START(Marker, Parameter) (Marker = (VA_LIST) ((UINTN) & (Parameter) + _INT_SIZE_OF (Parameter)))

#define _INT_SIZE_OF(n) ((sizeof (n) + sizeof (UINTN) - 1) &~(sizeof (UINTN) - 1))

(UINTN is basically size_t, 32-bit on 32-bit, 64-bit on 64-bit.)

That is, they directly poke at the stack. You might have gotten the ICE with gcc because in your code va_start() expands to __builtin_va_start() (see /usr/lib/gcc/x86_64-redhat-linux/4.4.4/include/stdarg.h), which might not play nice with __attribute__((ms_abi)). The edk2 tree works around this by directly accessing the stack on x86_64. I agree though that this should be generally fixed in gcc or glibc.

Re: Calling convention

Date: 2012-11-01 02:47 pm (UTC)
From: [identity profile] pjones.id.fedoraproject.org
Yeah, and strictly speaking none of the code in gnu-efi itself that uses variadic functions is required to be EFIAPI. So I've worked around it by making my patch better. Nevertheless, one should never get an ICE for any reason, so the bug must be fixed, even if it' just doesn't allow __builtin_va_start() for EFIAPI functions (or supplies a different version that works with it.)

Accessing fat filesystem

Date: 2012-11-15 05:21 pm (UTC)
From: [personal profile] skiloup
I'm just getting started with gnu-efi and am having a heck of a time figuring out how to do file I/O with a fat32 filesystem. Any chance I could get a couple pointers from a seasoned vet? You getting started examples in this post have proven highly useful. Thank you kindly!

Re: Accessing fat filesystem

Date: 2012-11-17 01:36 am (UTC)
From: [personal profile] skiloup
It seems LocateHandleBuffer is missing from the EFI_BOOT_SERVICES struct in the gnu-efi headers. The spec says the "new" LocateHandleBuffer() saves me two calls to LocateHandle() and a call to AllocatePool(), but doesn't tell me what the old procedure actually was.

Re: Accessing fat filesystem

Date: 2012-11-17 05:07 am (UTC)
From: [personal profile] skiloup
Shoot that's good to know. The current package that ubuntu is distributing is gnu-efi_3.0i.orig. I should have known better. Thanks for the info.

Re: Accessing fat filesystem

Date: 2012-11-20 06:45 pm (UTC)
From: [personal profile] skiloup
I'm having problems getting LocateHandle and LocateHandleBuffer to return properly. The status being returned from uefi_call_wrapper is 14 (EFI_NOT_FOUND). I wrote a standalone dxe driver and built it into our BIOS that calls locatehandlebuffer in this same manner. The dxe driver returns the correct numHandles as expected. I must be misunderstanding how to properly call locatehandlebuffer using the uefi_call_wrapper. Any help would be appreciated. I think I'm going to start looking for differences in the boot_services defined in the efiapi header next. Also of note, I get this same error when using the OVMF firmware in qemu.

Here's what I have:
EFI STATUS status;
EFI_HANDLE *handles;
UINTN numHandles;

InitializeLib(ImageHandle, SystemTable);

status = uefi_call_wrapper(
SystemTable->BootServices->LocateHandleBuffer,5,
ByProtocol,
&FileSystemProtocol,
NULL,
&numHandles,
&handles
);
Edited Date: 2012-11-20 07:29 pm (UTC)

EFI_HANDLE

Date: 2013-08-02 10:59 pm (UTC)
From: (Anonymous)
This is a great page! I was testing the EFI_HANDLE sample. I compiled it with the Makefile provided in the page. I didn't get any errors, however, when running the .efi in EFI shell, the computer just "hung". I have to manually reboot the system. Not sure if the program takes a while to run. I can run another .efi without any problems. Comments are welcome! Thank you! BTW, I'm using fedora.

Profile

Matthew Garrett

About Matthew

Power management, mobile and firmware developer on Linux. Security developer at Nebula. Ex-biologist. @mjg59 on Twitter. Content here should not be interpreted as the opinion of my employer.

Page Summary

Expand Cut Tags

No cut tags