arch linux arm aarch64 + ovmf uefi + qemu

since it’s some time i’m dealing with this stuff, i decided it would be a good idea to put everything on here for further reference. following are the steps to build an arch linux armv8 aarch64 qemu image, with efi and efistub boot.

pre assumptions:

can take this guide as initial reference, especially for stesps 1 to 7: juno archlinuxarm guide:

so first we create a disk file:

↬  dd if=/dev/zero of=arm64disk.img bs=100M count=80
8283750400 bytes (8.3 GB, 7.7 GiB) copied, 48 s, 172 MB/s
80+0 records in
80+0 records out
8388608000 bytes (8.4 GB, 7.8 GiB) copied, 48.7326 s, 172 MB/s

make efi and root partitions:

↬  parted arm64disk.img
GNU Parted 3.2
Using /home/andrei/Documents/armarchguide/arm64disk.img
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) mktable gpt
(parted) mkpart ESP fat32 1MiB 513MiB                                    
(parted) set 1 boot on                                                    
(parted) mkpart primary ext4 513MiB 100%                                  
(parted) quit

create loop device so we can mount the partitions:

↬  losetup -fP arm64disk.img
↬  losetup -a
/dev/loop0: [2050]:8134417 (/home/andrei/Documents/armarchguide/arm64disk.img)
↬  fdisk -l /dev/loop0
Disk /dev/loop0: 7.83 GiB, 8388608000 bytes, 16384000 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 7B57E083-2820-4C1B-87DA-E0E9BDDD092D

Device         Start      End  Sectors  Size Type
/dev/loop0p1    2048  1050623  1048576  512M EFI System
/dev/loop0p2 1050624 16381951 15331328  7.3G Linux filesystem

create the file systems; fat32 for efi system partition and ext4 for root:

↬  mkfs.vfat -F 32 -n "arch arm efi" /dev/loop0p1
mkfs.fat 4.1 (2017-01-24)
mkfs.fat: warning - lowercase labels might not work properly with DOS or Windows
↬  mkfs.ext4 /dev/loop0p2
mke2fs 1.45.4 (23-Sep-2019)
Discarding device blocks: done                            
Creating filesystem with 1916416 4k blocks and 479552 inodes
Filesystem UUID: bbcea09a-9fbb-426a-85ea-cae36d3e082f
Superblock backups stored on blocks:
 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

mount and copy over the root filesystem provided by arch linux arm:

↬  cd /mnt
↬  mkdir armroot
↬  mkdir armboot
↬  mount /dev/loop0p1 armboot/
↬  mount /dev/loop0p2 armroot/
↬  wget http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz
↬  bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C armroot/
↬  sync
↬  mv armroot/boot/* armboot/

write something like this on the guest’s /etc/fstab; this is to instruct the system to correctly mount the partition at the right place when booting. if you get into some recovery mode or in general the guest doesn’t boot, this is one of the first places you should look.

↬  cat armroot/etc/fstab
# Static information about the filesystems.
# See fstab(5) for details.

# <file system> <dir> <type> <options> <dump> <pass>
/dev/vda1      	/boot     	vfat      	defaults,rw	0 2
/dev/vda2	/         	ext4      	rw,relatime	0 1

since there’s no boot manager nor any boot process set up in the guest system, we’ll need the Image and vmlinuz-linux from the boot partitions for the first boot, so copy them over. this way we can start the kernel externally, specifying the root partition to use and bootstrap from there.

↬  cp /mnt/armboot/Image .
↬  cp /mnt/armboot/initramfs-linux.img .

we can now unmount the partitions.

↬  umount armroot
↬  umount armboot

using the two files copied before, we can boot the guest. login credentials are root:root:

qemu-system-aarch64 -M virt -m 1024 -cpu cortex-a57 -kernel ./Image -initrd ./initramfs-linux.img -drive file=arm64disk.img,format=raw,index=0,media=disk  -nographic -no-reboot  -append "root=/dev/vda2 rw console=ttyAMA0" -net nic -net bridge,br=qemu_wifibr

side note: you can use port forwarding to enable networking on the guest specifying the -net user,hostfwd=tcp::5022-:22 option instead of -net bridge,br=qemu_wifibr. read the appendix for more info

if you find you can ping 8.8.8.8 but not google.com, you just have to set up some nameserver:

[root@alarm ~]# echo 'nameserver 8.8.8.8' > /etc/resolv.conf

first thing i usually do is upgrade the system. a couple of problems arise though:

first is pacman was hanging, i commented the ‘CheckSpace’ line in /etc/pacman.conf as suggested here and for some reason it solved the issue.

second is the keyring;

[root@alarm ~]# pacman -Syu
[..]
(55/55) checking keys in keyring                   [######################] 100%
warning: Public keyring not found; have you run 'pacman-key --init'?
downloading required keys...
error: key "77193F152BDBE6A6" could not be looked up remotely
error: required key missing from keyring
error: failed to commit transaction (unexpected error)
Errors occurred, no packages were upgraded.

easily solved by initializing it:

[root@alarm ~]# pacman-key --init
gpg: /etc/pacman.d/gnupg/trustdb.gpg: trustdb created
gpg: no ultimately trusted keys found
gpg: starting migration from earlier GnuPG versions
gpg: porting secret keys from '/etc/pacman.d/gnupg/secring.gpg' to gpg-agent
gpg: migration succeeded
gpg: Generating pacman keyring master key...
gpg: key EDDF1D2FC6017A2C marked as ultimately trusted
gpg: directory '/etc/pacman.d/gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/etc/pacman.d/gnupg/openpgp-revocs.d/EF181BC21A79C65FBD4EF10DEDDF1D2FC6017A2C.rev'
gpg: Done
==> Updating trust database...
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
[root@alarm ~]# pacman-key --populate archlinuxarm
==> Appending keys from archlinuxarm.gpg...
==> Locally signing trusted keys in keyring...
  -> Locally signing key 69DD6C8FD314223E14362848BF7EEF7A9C6B5765...
  -> Locally signing key 02922214DE8981D14DC2ACABBC704E86B823CD25...
  -> Locally signing key 9D22B7BB678DC056B1F7723CB55C5315DCD9EE1A...
==> Importing owner trust values...
gpg: setting ownertrust to 4
gpg: inserting ownertrust of 4
gpg: setting ownertrust to 4
==> Updating trust database...
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   3  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: depth: 1  valid:   3  signed:   1  trust: 0-, 0q, 0n, 3m, 0f, 0u
gpg: depth: 2  valid:   1  signed:   0  trust: 1-, 0q, 0n, 0m, 0f, 0u

now it upgrades successfully. install some useful package (vim, binutils) and efibootmgr; then poweroff to boot with the efi variables.

the aarch64 efi firmware is found here:

↬  pacman -Ql ovmf-aarch64
ovmf-aarch64 /usr/
ovmf-aarch64 /usr/share/
ovmf-aarch64 /usr/share/ovmf/
ovmf-aarch64 /usr/share/ovmf/AARCH64/
ovmf-aarch64 /usr/share/ovmf/AARCH64/QEMU_EFI.fd

create efivars file:

↬  dd if=/dev/zero of=arm64efivars.img bs=128K count=1
1+0 records in
1+0 records out
131072 bytes (131 kB, 128 KiB) copied, 0.010478 s, 12.5 MB/s

now, trying to boot with them prints this weird error:

↬  qemu-system-aarch64 -M virt -m 1024 -cpu cortex-a57 -kernel ./Image -initrd ./initramfs-linux.img -drive file=arm64disk.img,format=raw,index=0,media=disk  -nographic -no-reboot  -append "root=/dev/vda2 rw console=ttyAMA0" -net nic -net bridge,br=qemu_wifibr -drive file=/usr/share/ovmf/AARCH64/QEMU_EFI.fd,if=pflash,format=raw,readonly -drive file=./arm64efivars.img,if=pflash,format=raw
qemu-system-aarch64: Initialization of device cfi.pflash01 failed: device requires 67108864 bytes, block backend provides 2097152 bytes

solution: trick it into thinking it’s 64MB (or just really pad the files to 64MB).

↬  cp /usr/share/ovmf/AARCH64/QEMU_EFI.fd arm64eficode.fd
↬  dd if=/dev/zero of=arm64eficode.fd bs=1c count=1 seek=67108863
↬  'du' arm64eficode.fd
2052	arm64eficode.fd
↬  'ls' -l arm64eficode.fd
-rw-r--r-- 1 root root 67108864 Oct  3 22:51 arm64eficode.fd

same thing for arm64efivars.fs.

now workz:

qemu-system-aarch64 -M virt -m 1024 -cpu cortex-a57 -kernel ./Image -initrd ./initramfs-linux.img -drive file=arm64disk.img,format=raw,index=0,media=disk  -nographic -no-reboot  -append "root=/dev/vda2 rw console=ttyAMA0" -net nic -net bridge,br=qemu_wifibr -drive file=arm64eficode.fd,if=pflash,format=raw,readonly -drive file=./arm64efivars.fd,if=pflash,format=raw

we have efivars!

[root@alarm ~]# efibootmgr -v
Timeout: 3 seconds
BootOrder: 0000
Boot0000* UiApp	FvVol(64074afe-340a-4be6-94ba-91b5b4d0f71e)/FvFile(462caa21-7614-4503-836e-8ab6f4662331)

let’s add the efi boot stub:

[root@alarm ~]# blkid
/dev/vda1: LABEL_FATBOOT="arch arm ef" LABEL="arch arm ef" UUID="FF4C-92F7" TYPE="vfat" PARTLABEL="ESP" PARTUUID="78e259bb-c29e-4afd-94d8-30b0ff5bad82"
/dev/vda2: UUID="bbcea09a-9fbb-426a-85ea-cae36d3e082f" TYPE="ext4" PARTLABEL="primary" PARTUUID="12a403e4-72ee-49a5-8c59-9b4c4475a4c8"
[root@alarm ~]# efibootmgr --disk /dev/vda --part 1 --create --label "Arch Linux ARM" --loader /Image --unicode 'root=PARTUUID=12a403e4-72ee-49a5-8c59-9b4c4475a4c8 rw initrd=/initramfs-linux.img' --verbose

and without kernel and initrd, boots ok:

qemu-system-aarch64 -M virt -m 1024 -cpu cortex-a57 -drive file=arm64disk.img,format=raw,index=0,media=disk -nographic -no-reboot -net nic -net bridge,br=qemu_wifibr -drive file=arm64eficode.fd,if=pflash,format=raw,readonly -drive file=./arm64efivars.fd,if=pflash,format=raw

gg

appendix - networking

the bridge is a better solution since the guest appears to be just another host in the network. you can easly create one to share your ethernet (wired) connection; wifi sharing is not so easy though. i use parprouted, creating an arp-proxy, and the following script to automate its creation and deletion:

#!/usr/bin/env bash

printusage() {
		echo "usage: $0 start/stop"
		exit 1
}

if [[ "$EUID" -ne 0 ]]; then
	echo "need root powa!"
	exit 1
fi

if [[ -z "$1" ]]; then
	printusage
fi

action="$1"

br_if="qemu_wifibr"
wifi_if="$(iw dev | awk '$1=="Interface"{print $2}' | head -n 1)"
parprouted_if_addr="10.11.12.1/32"

if ! grep -q "allow ""$br_if" /etc/qemu/bridge.conf; then
	echo "allow ""$br_if" >> /etc/qemu/bridge.conf
fi

case "$action" in
	start)
		brctl addbr "$br_if"
		ip addr add "$parprouted_if_addr" dev "$br_if"
		ip link set "$br_if" up
		parprouted "$wifi_if" "$br_if"
		echo "$br_if created"
		;;
	stop)
		ip addr del "$parprouted_if_addr" dev "$br_if"
		ip link set dev "$br_if" down
		brctl delbr "$br_if"
		echo "$br_if deleted"
		;;
	*)
		printusage
		;;
esac

keep in mind that dhcp and other lower network level stuff might not work; so for example you’d have to set addresses manually in the guest…

[root@alarm ~]# ip ad add 192.168.1.201/24 dev enp0s1
[root@alarm ~]# route add default gw 192.168.1.1
[root@alarm ~]# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=986 ms