EFI Programming on Mac OS X

© Amit Singh. All Rights Reserved. Written in March 2006

Background

The Extensible Firmware Interface (EFI) can be traced back to 1998 to the Intel Boot Initiative (IBI) program. The EFI specification, which was developed and maintained by a consortium of companies (including Intel and Microsoft), defines a set of APIs and data structures to be exported by a system's firmware, and to be used by a variety of clients such as the following.

Some details about EFI and its origins, along with a juxtaposed discussion of Open Firmware, can be found in More Power to Firmware. Moreover, there is an entire chapter dedicated to Open Firmware and EFI in the book Mac OS X Internals.

Intel Core Duo

EFI did not really take off after it was introduced. The rather lethargic and uninteresting lifestyle of the Itanium can be partly blamed. Moreover, EFI is a disruptive solution, which makes it unappealing to many in the short term. Apple's adoption of EFI for its Intel-based line of Macintosh computers proved to be a shot in the arm for EFI. In particular, the frenzy of attempts to run non-Apple operating systems (notably Windows) on the Intel-based Apple machines generated considerable interest in EFI, and consequentially, in EFI programming.

Intel has released a variety of EFI-related software, including source code, over the years. The IntelĀ® Platform Innovation Framework for EFI (Framework for short) is Intel's next-generation firmware architecture from the ground up. The core chunks of this code are available under an open-source license at TianoCore.org. "Tiano" was the developer code name whereas "Framework" is the marketing name. The "Framework" is Intel's implementation of the EFI specification.

The Standard EFI-Development Environment

The typical EFI development environment is Windows based. This is because the EFI Development Kit (EDK)—the open-source component of the "Framework"—is only available for Windows. Specifically, for IA-32 platform development, the EDK requires Microsoft Windows 2000 or XP and a recent version of Microsoft Visual Studio .NET. This is not to say that it is impossible to use the EDK under other operating systems—the code is reasonably portable, and given the right compiler and some patience, the EDK's functionality could be used to create EFI programs on non-Windows systems.

The EDK is released under the BSD license.

There also exists the gnu-efi package, which can be used to build EFI applications—both for the IA-32 and IA-64 platforms—on Linux (and in general, any system that would support the toolchain). The functionality of gnu-efi is rather minimal as compared to that of the EDK. Therefore, for non-trivial EFI applications, the developer will have to do more work in general if using gnu-efi. The latter needs a GNU compiler toolchain that has support for EFI applications. For example, to create EFI binaries for IA-32, a version of objcopy that supports the efi-app-ia32 target is required. The ELF32 (or ELF64 on IA-64) binaries normally produced on Linux must be converted to EFI binaries, which are supposed to be in the PE32 format. ELF sections are copied (via objcopy) into PE32.

EFI Development on Mac OS X

If you are developing EFI applications or drivers for Intel-based Macintosh systems, it is likely that you will prefer to develop on Mac OS X rather than on Windows, Linux, or any other system. The rest of this document will help you jumpstart EFI development on Mac OS X. In fact, you can use either the PowerPC or x86 version of Mac OS X for hosting an EFI build environment. The approach we will use can be summarized as follows.

We need a new GNU compiler toolchain because Apple's default toolchain does not support EFI binaries. A rather easy route would be to build a cross compiler hosted on Mac OS X (either PowerPC or x86) and targeted for x86 Linux. This way, gnu-efi will build out-of-the-box. Besides, you could potentially use such a toolchain to compile a variety of sources, including the Linux kernel.

Building a cross compiler from scratch is painful, to say the least. Since the topic is well documented, we will not look at the details of building the cross compiler. You can use a build harness such as crosstool, which should make it reasonably straightforward to build the compiler. If you do not have the time or the resources to build the toolchain yourself, you can download the following pre-built toolchains for the PowerPC and x86 versions of Mac OS X.

These toolchains include the following components:

The tarball that you download should be unpacked, with superuser privileges, in the root directory. This will create a directory hierarchy under /usr/local/osxbook/. For example:

$ sudo tar -C / -xjpvf efibuildenv-x86-4.1.0-2.3.6.tar.bz2 ...

To use the toolchain, include the following directory in your path.

/usr/local/osxbook/efi/gcc-4.1.0-glibc-2.3.6/i686-osxbook-linux-gnu/bin

The toolchain programs are installed with the prefix i686-osxbook-linux-gnu- in their names. For example, the toolchain's objcopy program is called i686-osxbook-linux-gnu-objcopy.

$ cd /usr/local/osxbook/efi/gcc-4.1.0-glibc-2.3.6/i686-osxbook-linux/gnu/bin $ ls fix-embedded-paths i686-osxbook-linux-gnu-gcov i686-osxbook-linux-gnu-addr2line i686-osxbook-linux-gnu-gprof i686-osxbook-linux-gnu-ar i686-osxbook-linux-gnu-ld i686-osxbook-linux-gnu-as i686-osxbook-linux-gnu-nm i686-osxbook-linux-gnu-c++ i686-osxbook-linux-gnu-objcopy i686-osxbook-linux-gnu-c++filt i686-osxbook-linux-gnu-objdump i686-osxbook-linux-gnu-cpp i686-osxbook-linux-gnu-ranlib i686-osxbook-linux-gnu-g++ i686-osxbook-linux-gnu-readelf i686-osxbook-linux-gnu-gcc i686-osxbook-linux-gnu-size i686-osxbook-linux-gnu-gcc-4.1.0 i686-osxbook-linux-gnu-strings i686-osxbook-linux-gnu-gccbug i686-osxbook-linux-gnu-strip

Next, let us compile the gnu-efi package. You can download it form the following location.

ftp://ftp.hpl.hp.com/pub/linux-ia64/gnu-efi-3.0c.tar.gz

The package should readily compile after setting the ARCH and prefix variables in the Make.defaults file within the package's root directory. The file's contents should look like the following (modified contents are shown in red).

... ARCH = ia32 ... else ifeq ($(ARCH),ia32) # # gcc-3.x is required # prefix = i686-osxbook-linux-gnu- ...

Thereafter, we can compile and install gnu-efi. By default, the headers and object files are installed under /usr/local/.

$ make ... $ sudo make install ...

Let us now use our newly installed EFI build environment to write a trivial EFI program. We will not discuss the EFI programming interfaces here. Please refer to one or more of the following resources for details.

Figure 1 shows a trivial EFI program: one that prints "Hello, EFI!" and exits. Note that when using gnu-efi, the "main" function (the program's entry point) must be named efi_main.

// hello.c #include <efi.h> #include <efilib.h> EFI_STATUS efi_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *systab) { InitializeLib(image_handle, systab); Print(L"Hello, EFI!\n"); return EFI_SUCCESS; }


Figure 1. A trivial EFI program

Figure 2 shows the Makefile that can be used to generate the EFI binary for the program shown in Figure 1. The Makefile can be adapted to compile other EFI binaries by adding their names to TARGETS.

# EFI Build Environment for Mac OS X # www.osxbook.com # # Makefile for EFI applications # # Defaults # ARCH = ia32 EFIROOT = /usr/local HDRROOT = $(EFIROOT)/include/efi INCLUDES = -I. -I$(HDRROOT) -I$(HDRROOT)/$(ARCH) -I$(HDRROOT)/protocol CRTOBJS = $(EFIROOT)/lib/crt0-efi-$(ARCH).o CFLAGS = -O2 -fpic -Wall -fshort-wchar -fno-strict-aliasing \ -fno-merge-constants CPPFLAGS = -DCONFIG_$(ARCH) FORMAT = efi-app-$(ARCH) INSTALL = install LDFLAGS = -nostdlib LDSCRIPT = $(EFIROOT)/lib/elf_$(ARCH)_efi.lds LDFLAGS += -T $(LDSCRIPT) -shared -Bsymbolic -L$(EFIROOT)/lib $(CRTOBJS) LOADLIBS = -lefi -lgnuefi $(shell $(CC) -print-libgcc-file-name) # Toolchain prefix # prefix = i686-osxbook-linux-gnu- CC = $(prefix)gcc AS = $(prefix)as LD = $(prefix)ld AR = $(prefix)ar RANLIB = $(prefix)ranlib OBJCOPY = $(prefix)objcopy # Rules # %.efi: %.so $(OBJCOPY) -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel \ -j .rela -j .reloc --target=$(FORMAT) $*.so $@ %.so: %.o $(LD) $(LDFLAGS) $^ -o $@ $(LOADLIBS) %.o: %.c $(CC) $(INCLUDES) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ # Targets # TARGETS = hello.efi all: $(TARGETS) clean: rm -f $(TARGETS)


Figure 2. Makefile for building EFI applications on Mac OS X

Figure 3 shows a more complex example of an EFI program—one that solves the Towers of Hanoi problem. The complexity comes from the fact that unlike the EDK, the gnu-efi package does not provide a ready-made mechanism to access command-line arguments passed to a program from the EFI shell. In Figure 3's program, we access these arguments ourselves, which serves as an example of using EFI protocols.

// hanoi.c // // The Towers of Hanoi as an Extensible Firmware Interface application. // Copyright (c) 2006 Amit Singh. All Rights Reserved. // <www.osxbook.com> // #include <efi.h> // pulls in public EFI header files #include <efilib.h> // EFI library header void dohanoi(int n, int from, int to, int using); INTN populate_argv(CHAR16 *buf, UINTN len, CHAR16 **argv); enum { FROMTOWER = 1, // move disks "from" this tower ID USINGTOWER = 2, // move disks "using" this tower ID TOTOWER = 3, // move disks "to" this tower ID NDISKS_MIN = 1, // lower bound on the number of disks NDISKS_MAX = 10, // upper bound on the number of disks MAX_ARGS = 32, // upper bound on the number of command-line arguments }; // our application's entry function EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) { UINTN ndisks; INTN argc = 0; CHAR16 *argv[MAX_ARGS]; CHAR16 *rawargs = NULL; EFI_LOADED_IMAGE *info; EFI_STATUS status; // Initialize the EFI library. InitializeLib(image, systab); // After the EFI library is initialized, we can access the EFI system // table, the EFI boot services table, and the EFI runtime services // table through 'ST', 'BS', and 'RT', respectively. // Prepare to gather command-line arguments. First, retrieve image info. status = BS->HandleProtocol(image, &LoadedImageProtocol, (VOID *)&info); if (EFI_ERROR(status)) { Print(L"%r = BS->HandleProtocol(..., &LoadedImageProtocol, ...)\n", status); return status; } // Allocate memory for processing command-line arguments. status = BS->AllocatePool(EfiLoaderData, info->LoadOptionsSize + sizeof(CHAR16), (VOID *)&rawargs); if (status != EFI_SUCCESS) { Print(L"%r = BS->AllocatePool(...)\n", status); return status; } // Populate C-style argc and argv. CopyMem(rawargs, info->LoadOptions, info->LoadOptionsSize); argc = populate_argv(rawargs, info->LoadOptionsSize, argv); if (argc != 2) { Print(L"The Towers of Hanoi <www.osxbook.com>\n"); Print(L"usage: %s N\n", argv[0]); status = EFI_INVALID_PARAMETER; goto out; } ndisks = Atoi((CHAR16 *)argv[1]); if ((ndisks < NDISKS_MIN) || (ndisks > NDISKS_MAX)) { Print(L"%s: illegal value for number of disks\n", argv[0]); status = EFI_INVALID_PARAMETER; goto out; } dohanoi(ndisks, FROMTOWER, TOTOWER, USINGTOWER); out: (void)BS->FreePool((VOID *)rawargs); return status; } void dohanoi(int n, int from, int to, int using) { if (n > 0) { dohanoi(n - 1, from, using, to); Print(L"move %d --> %d\n", from, to); dohanoi(n - 1, using, to, from); } } // the following function is stolen/adapted from elilo's argify() // // Copyright (C) 2001-2003 Hewlett-Packard Co. // Contributed by Stephane Eranian <eranian@hpl.hp.com> // Copyright (C) 2001 Silicon Graphics, Inc. // Contributed by Brent Casavant <bcasavan@sgi.com> INTN populate_argv(CHAR16 *buf, UINTN len, CHAR16 **argv) { UINTN i = 0; UINTN j = 0; CHAR16 *p = buf; #define CHAR_SPACE L' ' if (buf == 0) { argv[0] = NULL; return 0; } // len is the number of bytes (and not the number of CHAR16's) len = len >> 1; // Here we use CHAR_NULL as the terminator rather than the length because // the EFI shell returns rather bogus values for it. Apparently, we are // guaranteed to find '\0' in the buffer where the real input arguments // stop, so we use it instead. // for(;;) { while (buf[i] == CHAR_SPACE && buf[i] != CHAR_NULL && i < len) i++; if (buf[i] == CHAR_NULL || i == len) goto end; p = buf+i; i++; while (buf[i] != CHAR_SPACE && buf[i] != CHAR_NULL && i < len) i++; argv[j++] = p; if (buf[i] == CHAR_NULL) goto end; buf[i] = CHAR_NULL; if (i == len) goto end; i++; if (j == (MAX_ARGS - 1)) { Print(L"%s: too many arguments (%d), truncating", argv[0], j); goto end; } } end: argv[j] = NULL; return j; }


Figure 3. The Towers of Hanoi in EFI

To compile the program shown in Figure 3, the Makefile shown in Figure 2 can be trivially modified: simply change the value of TARGETS from hello.efi to hanoi.efi.