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.


