Kevin Boone

Building a 32-bit EFI application on a 64-bit Linux system

I’ve recently been experimenting with bare-metal (no operating system) programming for x86-based, PC-type computers. I’ve written a series of articles on how to start doing this, and I’ll probably write more over the coming months.

One of the issues I skirted around in the original articles was how to handle the fact that both 32-bit and 64-bit EFI firmwares exist. It may come as a surprise that some computers that are capable of running 64-bit operating systems, still have 32-bit EFI firmware. Although some 64-bit EFI firmwares do have a 32-bit compatibility mode, this isn’t something a developer ought to rely on. For broad applicability, you’ll need to provide both 32-bit and 64-bit versions of your EFI application.

However, building an EFI application for a target architecture other than that of the build host can be a bit fiddly.

This short article is about how to build a 32-bit, i386 EFI application on a 64-bit, x86_64 Linux system. My earlier articles already covered how to build 64-bit applications using Linux, and I’m assuming the reader knows how to do that, and has the necessary tooling. As before, I’m assuming you’re using the gnu-efi library as the supporting framework for your application.

Specific steps

1: Build gnu-efi from source

If you get gnu-efi from a Linux distribution’s repository, you’ll almost certainly get a version that matches the host architecture, with no cross-architecture support. If you build from source, though, you’ll be able to target multiple architectures (and not just x86-based ones).

Check out the gnu-efi source from its GitHub repository. Then, to build a 32-bit x86 version, run

$ make ARCH=ia32

This will create the directory ia32/, in which you’ll find the libraries you need to link with your application:

ia32/lib/libefi.a
ia32/gnuefi/libgnuefi.a

The linker scripts for all architectures are in the directory gnuefi/, and not in the ia32/ directory. The header files are mainly in inc/ and, again, are not architecture-specific.

You’ll need to tell your compiler and linker the locations of all these bits when you build the application.

2: Build with 32-bit settings for gcc

Broadly, this amounts to running gcc -m32 ... to get 32-bit binaries. Additional settings might be helpful for broader CPU compatibility (see the example Makfile below).

3: Use the relevant linker script

Link using ld -T elf_ia32_efi.lds ... The other linker settings are the same as in my previous articles.

4: Transform the linker output to 32-bit PE format using objcopy

Use objcopy ... -O efi-app-ia32. Again, other arguments to objcopy are unchanged from my previous articles.

5: Store the executable in the EFI system partition with the appropriate name and location

The correct path for the 32-bit EFI application is /EFI/BOOT/BOOTIA32.EFI. You can include a 64-bit application in the same directory if you wish.

Putting the steps together

As an example, here’s a snippet from the Makefile I’m using to build a 32-bit EFI application (here called myapp.efi).

ARCH           = ia32

# EFI_BASE is the location of the gnu-efi source
EFI_BASE       = $(HOME)/lib/gnu-efi-master

# Derive the locations of all the libraries, headers, and build scripts from
#   this base directory, and the target architecture
EFI_BASE_ARCH  = $(EFI_BASE)/$(ARCH)
EFI_LIB        = $(EFI_BASE_ARCH)/lib
GNUEFI_LIB     = $(EFI_BASE_ARCH)/gnuefi
EFI_CRT        = $(GNUEFI_LIB)/crt0-efi-$(ARCH).o
EFI_LDS        = $(GNUEFI_LIB)/elf_$(ARCH)_efi.lds
GNU_EFI_LIB    = $(HOME)/lib/gnu-efi-master/$(ARCH)/gnuefi
EFI_LDS_BASE   = $(HOME)/lib/gnu-efi-master/gnuefi
EFI_LDS        = $(EFI_LDS_BASE)/elf_$(ARCH)_efi.lds
EFI_INC        = $(HOME)/lib/gnu-efi-master/inc
EFI_CRT_OBJS   = $(GNU_EFI_LIB)/crt0-efi-$(ARCH).o

CFLAGS         = -DCONFIG_$(ARCH) -Wno-error=pragmas -std=c11 -DGNU_EFI_USE_MS_ABI -maccumulate-outgoing-args -mno-red-zone -mno-avx -mno-sse3  -mno-popcnt  -mno-bmi2  -fPIC  -O2 -g -Wall -Wextra -Wstrict-prototypes -Werror -fno-strict-aliasing -ffreestanding -fno-stack-protector -fno-merge-all-constants -D__KERNEL__ -m32

LDFLAGS        = -nostdlib  --warn-common --no-undefined --fatal-warnings --build-id=sha1 -z norelro -z nocombreloc -pie -Bsymbolic --no-dynamic-linker

...

all: build/myapp.efi

build/%.o: src/%.c
        @mkdir -p build/
        gcc $(CFLAGS) -I $(EFI_INC) -DARCH=\"$(ARCH)\" -fPIE -c $< -o $@

build/myapp.so: $(OBJECTS)
        ld $(LDFLAGS) -L$(EFI_LIB) -L$(GNUEFI_LIB) $(EFI_CRT) $(OBJECTS) \
            -o build/myapp.so -T $(EFI_LDS) -lefi -lgnuefi

build/myapp.efi: build/myapp.so
        objcopy -j .text -j .sdata -j .data -j .dynamic -j .rodata -j .rel \
            -j .rela -j .rel.* -j .rela.* -j .rel* -j .rela* \
            -j .areloc -j .reloc -O efi-app-$(ARCH) \
            build/myapp.so build/myapp.efi

Closing remarks

So that’s how to build a 32-bit EFI application on a 64-bit Linux system. There are a few subtleties you might have to contend with, if you want to build 32-bit and 64-bit versions of your application from the same source. In particular, you’ll need to make sure your data type sizes are appropriate for the EFI architecture. This is unlikely to be a big problem if you use the type definitions provided in the gnu-efi header filesi (UINTN, etc), but you probably won’t be using these in code from libraries or that you’ve carried over from other projects.

As in all EFI development, remember that gnu-efi uses 2-byte characters, and filenames use ‘\’ as the separator.


Have you posted something in response to this page?
Feel free to send a webmention to notify me, giving the URL of the blog or page that refers to this one.