Headless Raspberry Pi OS virtualization, 64-bit edition

With Raspbian (now named Raspberry Pi OS) having been released as 64-bit, I can finally write a proper sequel to the previous post that dealt with virtualizing ARM/Linux distributions headlessly using QEMU.

You can read the original article here: Virtualizing Raspbian (or any ARM/Linux distro) headless using QEMU. Since the process is the same I will skip detailed explanations here.

Native emulation in QEMU

QEMU includes a raspi3b machine type and emulates UART, SD, USB controllers and more. This is enough for working headless usage.

With the root filesystem prepared and the appropriate files extracted from the boot partition the command line woud look as follows:

qemu-system-aarch64 -M raspi3b -kernel kernel8.img -dtb bcm2710-rpi-3-b.dtb \
        -drive file=rootfs.qcow2,if=sd -usb -device usb-net,netdev=u1 -netdev user,id=u1 \
        -append "root=/dev/mmcblk0 rw console=ttyAMA0" -nographic

The system boots up fine (with a few errors here and there) and is usable but I don't suggest using it like this.

A better alternative

The virt machine type is much better suited for this, our plan is to attach both the disk and network via Virtio.

For the kernel (and modules) we'll grab the linux-aarch64 package from Arch Linux ARM.

Extracting the root filesystem into a virtual disk image

Download Raspberry Pi OS (64-bit) from the official website, then run the script below or follow the steps manually.

The only difference from before is that we have to unlock the "root" user so we can actually log in later.

#!/bin/bash -e
[ -f $input ]

mkdir mnt
cp --reflink=auto $input source.img
truncate -s 10G source.img
echo ", +" | sfdisk -N 2 source.img
dev=$(sudo losetup -fP --show source.img)
[ -n "$dev" ]
sudo resize2fs ${dev}p2
sudo mount ${dev}p2 ./mnt -o rw
sudo sed '/^PARTUUID/d' -i ./mnt/etc/fstab
sudo sed '/^root:/ s|\*||' -i ./mnt/etc/shadow
sudo bash -c "rm -f \
        ./mnt/etc/systemd/system/multi-user.target.wants/{$remove_services}.service \
sudo umount ./mnt
sudo chmod a+r ${dev}p2
qemu-img convert -O qcow2 ${dev}p2 rootfs.qcow2
sudo losetup -d $dev
rm source.img; rmdir mnt

The kernel and initramfs


Extract the kernel like this:

tar -xvf linux-aarch64*.pkg.tar.* --strip-components=1 boot/Image.gz

Building an initramfs

The differences to the previous iteration of the script are:

  • Recompressing of zstd kernel modules as gzip (no busybox support for it)

  • Busybox isn't downloaded. You need to compile it for 64-bit ARM yourself and insert the path [1]

#!/bin/bash -e
pkg=$(echo linux-aarch64-*.pkg.tar.*)
[ -f "$pkg" ]

mkdir initrd; pushd initrd
mkdir bin dev mnt proc sys
tar -xaf "../$pkg" --strip-components=1 usr/lib/modules
rm -rf lib/modules/*/kernel/{sound,drivers/{net/{wireless,ethernet},media,gpu,iio,staging,scsi}}
find lib/modules -name '*.zst' -exec zstd -d --rm {} ';'
find lib/modules -name '*.ko' -exec gzip -9 {} ';'
install -p /FILL/ME/IN/busybox-aarch64 bin/busybox
cat >init <<"SCRIPT"
#!/bin/busybox sh
busybox mount -t proc none /proc
busybox mount -t sysfs none /sys
busybox mount -t devtmpfs none /dev

for mod in virtio-pci virtio-blk virtio-net; do
        busybox modprobe $mod

busybox mount -o rw /dev/vda /mnt || exit 1

busybox umount /proc
busybox umount /sys
busybox umount /dev

exec busybox switch_root /mnt /sbin/init
chmod +x bin/busybox init
bsdtar --format newc --uid 0 --gid 0 -cf - -- * | gzip -9 >../initrd.gz
popd; rm -r initrd

Booting the virtual machine

With the initramfs built, we have all parts needed to actually run the VM:

qemu-system-aarch64 -M virt -cpu cortex-a53 -m 2048 -smp 4 -kernel Image.gz -initrd initrd.gz \
        -drive file=rootfs.qcow2,if=virtio -nic user,model=virtio \
        -append "console=ttyAMA0" -nographic
After a bit of booting you should be greeted by Debian GNU/Linux 11 raspberrypi ttyAMA0 and a login prompt.
You can log in as "root" without a password.