UEFI on AARCH64
by Mike Krinkin
In the previous post we covered how an EFI application can load another binary in memory. That’s basically what a bootloader does. In this short post I will show that basically the same code will work on a different architecture.
As usual all the sources are available on GitHub.
UEFI Specification supports multiple different architectures, including
aarch64 is basically a 64-bit version of
It appears that in the
ARM ecosystem there is quite a variety of differnt
turn-up methods and approaches, so there is no guarantees that a particular
board firmware implements UEFI Specification.
I happened to have a board that does have a version of firmware implementing the
UEFI Specification - HiKey960. Given that I have this board, I though that
it might be curious to see if the same code I have will work for
I will not cover the board in any depth here. That being said, I had this board for quite a while, so I wasn’t sure if it’s still working of if it has the reasonably new version of the firmware.
Here are a few links that helped me to update the firmware and bring up the board:
- board recovery instructions are available here
- instructions to setup
ser2netto connect to the device console are availbe here
- instructions to install
Debianon the board are here.
Those instructions overlap significantly, but somehow none of them is complete.
From now on I assume that the board has the latest UEFI firmware and by default
GRUB and boots into
Debian when you switch the board on.
HiKey960 has internal storage where all the firmware,
Debian image and root
filesystem live. As normal for UEFI firmware on this internal storage we also
You can explore this
FAT partition if you boot the board into
root@linaro-developer:~# cat /etc/fstab | grep /boot/efi UUID=6D97-E8F4 /boot/efi vfat rw,nofail 0 2 root@linaro-developer:~# ls -l /boot/efi/EFI/BOOT total 816 -rwxr-xr-x 1 root root 120832 Jul 20 2020 BOOTAA64.EFI -rwxr-xr-x 1 root root 702464 Jul 20 2020 GRUBAA64.EFI
As you can see one of the binaries in the partition is the
GRUB itself. I
suspect that it’s a regular
EFI binary just like the one I built, though with
quite a bit more functionality.
If you want to prevent the board from booting into
GRUB right away by default
you can just rename those two binaries:
root@linaro-developer:~# mv /boot/efi/EFI/BOOT/GRUBAA64.EFI /boot/efi/EFI/BOOT/GRUBAA64.EFI.old root@linaro-developer:~# mv /boot/efi/EFI/BOOT/BOOTAA64.EFI /boot/efi/EFI/BOOT/BOOTAA64.EFI.old
If you do that the firmware would not find the expected
GRUB binary to load
and will fail to load the system. In my case when it happens the I see the
Press RETURN or SPACE key to quit.
When I press
ENTER the board shows some kind of a
In that UI you can select the
Boot Manager item in the menu and then
UEFI Shell to start the shell.
In the EFI getting started post I covered how to load and exectue a binary
UEFI Shell. There is one caveat though, the binaries were not built yet
aarch64 and we didn’t store them into the internal storage of the
Building for ARM
GCC toolchain to cross-compile a binary for a different architecture we’d
need to download or build a toolchain for the target platform.
toolchain might actually have a built-in support for multiple architectures, so
all it takes to cross-compile is to specify the target platform.
Here is the complete
Makefile content that works for me:
CC := clang LD := lld CFLAGS := \ -ffreestanding -MMD -mno-red-zone -std=c11 \ -target aarch64-unknown-windows -Wall -Werror -pedantic LDFLAGS := -flavor link -subsystem:efi_application -entry:efi_main KERNEL_CFLAGS := \ -ffreestanding -MMD -mno-red-zone -std=c11 \ -target aarch64-unknown-gnu -Wall -Werror -pedantic KERNEL_LDFLAGS := \ -flavor ld -e main SRCS := main.c clib.c kernel.c default: all %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ boot.efi: clib.o main.o $(LD) $(LDFLAGS) $^ -out:$@ kernel.elf: kernel.c $(CC) $(KERNEL_CFLAGS) -c $< -o kernel.o $(LD) $(KERNEL_LDFLAGS) kernel.o -o $@ -include $(SRCS:.c=.d) .PHONY: clean all default all: boot.efi kernel.elf clean: rm -rf *.efi *.elf *.o *.d *.lib
You might notice that this
Makefile is quite similar to the one in
EFI getting started post. The only important difference is the
-target flag for the EFI binary now contains
-target flag for the ELF binary now contains
So all it took in this case is it to tell
clang the target architecture. In
general the flags between different architectures might not really match.
For example, I was surprised to find out that
-mno-red-zone option works for
aarch64 architecture. Even though red zone describes a relatively generic
optimization that makes sense for different architectures, it was surprising to
find out that a similar optimization for
aarch64 is also named red zone.
NOTE: the version of the
Makefilein the repository is slightly more generic and supports building for both
aarch64, so it’s different from the code in the post.
If we try to build the code now it will fail in the following way:
clang -ffreestanding -MMD -mno-red-zone -std=c11 -target aarch64-unknown-windows -Wall -Werror -pedantic -c main.c -o main.o main.c:457:22: error: 'sysv_abi' calling convention is not supported for this target [-Werror,-Wignored-attributes] int (__attribute__((sysv_abi)) *entry)(void); ^ main.c:465:31: error: 'sysv_abi' calling convention is not supported for this target [-Werror,-Wignored-attributes] entry = (int (__attribute__((sysv_abi)) *)(void))elf->image_entry; ^ 2 errors generated. make: *** [Makefile:18: main.o] Error 1
main.c when I’m calling into the code just loaded from an ELF file I
have to specify a calling convention explicitly. That’s because on
calling convention doesn’t match the calling convention used by the code in the
However, when compiling for
sysv_abi attribute is not supported.
Fortunately enough it appears that calling conventions for
aarch64 do not
diverge as much, so we don’t really need to explicitly tell compiler to use a
different calling convention and that part of the code might be deleted.
The way I handled it in the repository is that I introduce a file with compiler
specific magic, macroses, etc. I called this file
compiler.h and defined there
ELFABI macro in the following way:
#ifndef __COMPILER_H__ #define __COMPILER_H__ #ifdef __x86_64__ #define ELFABI __attribute__((sysv_abi)) #else #define ELFABI #endif #endif // __COMPILER_H__
So now where I need to explicitly specify the calling convention I can just use
ELFABI macro. After this change I managed to get the two binaries:
Copying to the Board
There are a few ways to make the newly built binaries accesible on the board.
One simple way is to use an
micro SD card.
You can just rename
BOOTAA64.EFI.old to their original
UEFI Shell command
Debian. Alternatively, you can just load
UEFI Shell without renaming.
Debian you can insert the
micro SD card into the slot on the board and
mount it. Depending on how the
micro SD card was formatted the commands to do
that might look something like this:
root@linaro-developer:~# cd /mnt/ root@linaro-developer:/mnt# mkdir boot root@linaro-developer:/mnt# mount -t vfat /dev/mmcblk0p1 /mnt/boot
After that just copy the files from
the rest of the EFI binaries live:
root@linaro-developer:/mnt# cp boot/boot.efi /boot/efi/EFI/BOOT/boot.efi root@linaro-developer:/mnt# cp boot/kernel.elf /boot/efi/EFI/BOOT/kernel
NOTE: it doesn’t matter how we call the EFI binary since we explicitly call it by name in the
UEFI Shell, but the path to the ELF binary is hardcoded in the EFI binary, so it must live in
UEFI firmware actually also recognizes
micro SD card if it’s in the slot.
So instead of booting into
Debian you can just copy the files using the
Now when all the files are in place refer to the EFI getting started post for actually running the binary.
Bonus: testing in QEMU
If you don’t have an
aarch64 board (or even if you do, but working with the
actual hardware is too bothersome) you can use
QEMU. First you need to
aarch64 EFI firmware for
sudo apt-get install qemu-efi-aarch64
This package contains the
aarch64 EFI image for
QEMU. You can copy it into
the working directory:
cd ~/ws/efi cp /usr/share/qemu-efi-aarch64/QEMU_EFI.fd OVMF_aarch64.fd
You can use this file in the same way as described in the EFI getting started
post, but with the
qemu-system-aarch64 -machine virt -cpu max \ -drive if=pflash,format=raw,file=/home/kmu/ws/efi/OVMF_aarch64.fd \ -drive format=raw,file=fat:rw:/home/kmu/ws/efi/root \ -net none \ -nographic
/home/kmu/ws/efiis the working directory on my computor, the path might be different on yours.
NOTE: as in EFI getting started
rootis the root of the EFI file system, so all the files we want to be available in the virtual machine should be copied there.
NOTE: for some reason I couldn’t make it work without explicitly specifing
Instead of conclusion
Rather suprisingly making it work on
ARM turned out to be almost problem-free.