Skip to content

Instantly share code, notes, and snippets.

@robertkirkman
Forked from thalamus/ArchLinuxARM-M1
Last active December 7, 2025 12:57
Show Gist options
  • Select an option

  • Save robertkirkman/f79441c79811ad263f2f881f7864e793 to your computer and use it in GitHub Desktop.

Select an option

Save robertkirkman/f79441c79811ad263f2f881f7864e793 to your computer and use it in GitHub Desktop.
How to install Arch Linux ARM or Debian ARM in QEMU full system emulator

How to install a GNU/Linux ARM emulator

Prerequisites:

  • A PC with a Linux distribution - Arch Linux amd64 used here

Dependencies (for Arch Linux amd64 but very easy to get on most distros):

  1. Download the Arch Linux ARM generic tarball and create an image, replacing 60G with your desired maximum size.
wget http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz
qemu-img create -f qcow2 arch-aarch64.qcow2 60G

Tip

Step 1 equivalent for Debian guest:

wget https://cdimage.debian.org/cdimage/release/current/arm64/iso-dvd/debian-12.7.0-arm64-DVD-1.iso
wget https://cdimage.debian.org/cdimage/release/current/arm64/iso-dvd/SHA256SUMS
shasum -a 256 --ignore-missing -c SHA256SUMS
qemu-img create -f qcow2 debian-aarch64.qcow2 60G

Users of Debian guest should now skip to step 7.

  1. Become root, connect the image to nbd and partition it with fdisk.
sudo modprobe nbd
sudo qemu-nbd --connect=/dev/nbd0 arch-aarch64.qcow2
sudo fdisk /dev/nbd0
  • then g (to create a new GPT partition table)
  • then n (to create a new partition), then Enter twice, then +400M and Enter
  • then t (to change the type), then 1 for EFI System Partition
  • then n and Enter three times, then w to write changes and exit
  1. Format the partitions of the image, mount them, and extract the Arch Linux ARM tarball to them.
sudo mkfs.vfat /dev/nbd0p1
sudo mkfs.ext4 /dev/nbd0p2
sudo mkdir rootfs
sudo mount /dev/nbd0p2 rootfs
sudo mkdir rootfs/boot
sudo mount /dev/nbd0p1 rootfs/boot 
sudo bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C rootfs
  1. Edit fstab.
  • You will need both partitions' UUIDs - the UUID of the vfat partition in /dev/nbd0p1 looks like UUID="XXXX-XXXX" and the UUID of the ext4 partition in /dev/nbd0p2 looks like UUID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX":
sudo blkid
  • Then, edit rootfs/etc/fstab:
sudo vim rootfs/etc/fstab
  • Paste the following, replacing each instance of X with the corresponding digit of the UUID of the corresponding partition, /dev/nbd0p1 and /dev/nbd0p2 respectively, then save the file:
/dev/disk/by-uuid/XXXX-XXXX                            /boot vfat defaults 0 0
/dev/disk/by-uuid/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX /     ext4 defaults 0 0
  1. Create startup.nsh, which is read by the UEFI firmware to initially boot.
  • Edit rootfs/boot/startup.nsh:
sudo vim rootfs/boot/startup.nsh
  • Paste the following, replacing each instance of X with the corresponding digit of the UUID of the /dev/nbd0p2 partition, then save the file:
Image root=UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw initrd=\initramfs-linux.img
  1. Unmount the partitions, sync, disconnect the image from nbd, and exit the root shell.
sudo umount -R rootfs
sudo sync
sudo qemu-nbd --disconnect /dev/nbd0
sudo rmmod nbd
  1. Create flash images for the UEFI firmware and variables

Note

if you downloaded the RELEASEAARCH64_QEMU_EFI.fd instead of using the one from your distro's package, use that here in place of the QEMU_CODE.fd. AARCH64_QEMU_EFI.fds (which are installed into "flash0.img" here) can very slowly become slightly outdated over time. After using one with an Arch Linux ARM emulator for several years, then installing a new emulator from scratch and comparing its behavior with the old one, I've noticed very slight, subtle differences in behavior between them in the pre-boot stage before guest OS code runs. I would say the newer one's behavior seems slightly more polished and desirable.

truncate -s 64M flash0.img
truncate -s 64M flash1.img
dd if=/usr/share/edk2-armvirt/aarch64/QEMU_CODE.fd of=flash0.img conv=notrunc
  1. Launch QEMU, removing or adding anything you see fit.
qemu-system-aarch64 -M virt -m 8192 -cpu cortex-a72 -smp 8 \
      -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash0.img \
      -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash1.img \
      -drive if=none,file=arch-aarch64.qcow2,format=qcow2,id=hd0 \
      -device virtio-scsi-pci,id=scsi0 \
      -device scsi-hd,bus=scsi0.0,drive=hd0,bootindex=1 \
      -nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
      -monitor none -display none -vga none

Tip

Step 8 equivalent for Debian guest:

qemu-system-aarch64 -M virt -m 8192 -cpu cortex-a72 -smp 8 \
     -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash0.img \
     -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash1.img \
     -drive if=none,file=debian-aarch64.qcow2,format=qcow2,id=hd0 \
     -cdrom debian-12.7.0-arm64-DVD-1.iso \
     -device virtio-scsi-pci,id=scsi0 \
     -device scsi-hd,bus=scsi0.0,drive=hd0,bootindex=2 \
     -nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
     -nographic
  1. Upon successful first boot, initialize Arch Linux ARM and install a new bootloader.
  • Log in as alarm, password alarm:
ssh -p 2222 alarm@localhost
  • Become root, password root:
su
  • Initialize the pacman keyring, update the system and install efibootmgr, replacing each instance of X with the corresponding digit of the UUID of the /dev/nbd0p2 partition from earlier (which is now /dev/sda2), then shut down:
pacman-key --init
pacman-key --populate archlinuxarm
pacman -Syu
pacman -S efibootmgr
efibootmgr --disk /dev/sda --part 1 --create --label "Arch Linux ARM" --loader /Image --verbose \
           --unicode 'root=UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw initrd=\initramfs-linux.img'
poweroff

Tip

Step 9 equivalent for Debian guest:

  • At the EDK II Shell, type this command
FS1:\efi\boot\grubaa64.efi

Users of Debian guest can choose "Install" and use the guided installer.

  1. Launch QEMU again, exactly as in step 8.
qemu-system-aarch64 -M virt -m 8192 -cpu cortex-a72 -smp 8 \
      -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash0.img \
      -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash1.img \
      -drive if=none,file=arch-aarch64.qcow2,format=qcow2,id=hd0 \
      -device virtio-scsi-pci,id=scsi0 \
      -device scsi-hd,bus=scsi0.0,drive=hd0,bootindex=1 \
      -nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
      -monitor none -display none -vga none

Tip

Step 10 equivalent for Debian guest:

qemu-system-aarch64 -M virt -m 8192 -cpu cortex-a72 -smp 8 \
     -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash0.img \
     -drive if=pflash,media=disk,format=raw,cache=writethrough,file=flash1.img \
     -drive if=none,file=debian-aarch64.qcow2,format=qcow2,id=hd0 \
     -device virtio-scsi-pci,id=scsi0 \
     -device scsi-hd,bus=scsi0.0,drive=hd0,bootindex=2 \
     -nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
     -nographic
  1. Proceed with configuring Arch Linux ARM as normal (time, locales, users, software, configuration), using the Arch Linux Wiki as a guide.

Note

If your host has a simple network configuration, you can replace the slower -nic user argument with an efficient -netdev tap argument if you want using my minimal tap0 guide.

#!/bin/bash
# qemu-system gives the power of fine-grained control over guest:
# - firmware
# - bootloader
# - kernel
# - init system
# - docker engine
# that proot, chroot, qemu-user, docker and other containers usually do not
set -e
# guest firmware architecture
EDK2_ARCHITECTURE=AARCH64
# guest bootloader architecture
GRUB2_ARCHITECTURE=aa64
# guest kernel architecture
KERNEL_ARCHITECTURE=aarch64
# guest userspace architecture
USERSPACE_ARCHITECTURE=arm64
# guest termux-docker architecture
TERMUX_ARCH=arm
GUEST_RELEASE=trixie
QEMU_VERSION=10.0.3
OPENSSH_VERSION=10.0p2
# manifestation of https://github.com/tianocore/edk2/issues/10663
# last known PXE-compatible QEMU_EFI.fd builds from a tianocore/edk2 commit around early 2024
# QEMU_EFI.fd files require a GCC cross toolchain to build, and the retrage repository is not official,
# but it builds them entirely in CI.
EDK2_NIGHTLY_RELEASE=cdffebde798a04e793b8b7e4ad857c8d06e80af9
GUEST_SRCURL=https://deb.debian.org/debian/dists/${GUEST_RELEASE}/main/installer-${USERSPACE_ARCHITECTURE}/current/images/netboot/netboot.tar.gz
QEMU_SRCURL=https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz
OPENSSH_SRCURL=https://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-${OPENSSH_VERSION}.tar.gz
EDK2_NIGHTLY_SRCURL=https://github.com/retrage/edk2-nightly/raw/${EDK2_NIGHTLY_RELEASE}/bin/RELEASE${EDK2_ARCHITECTURE}_QEMU_EFI.fd
EMULATOR_INSTALL_DIR=$(pwd)/emulator
__install_qemu-system() {
rm -rf "${EMULATOR_INSTALL_DIR}"
mkdir -p "${EMULATOR_INSTALL_DIR}"
pushd "${EMULATOR_INSTALL_DIR}"
mkdir -p qemu-src/
pushd qemu-src/
wget -O qemu-src.tar.xz "${QEMU_SRCURL}"
tar xvJf qemu-src.tar.xz --strip-components=1
./configure \
--disable-docs \
--disable-fuzzing \
--disable-gettext \
--disable-modules \
--disable-sparse \
--disable-guest-agent \
--disable-qga-vss \
--disable-kvm \
--disable-xen \
--disable-stack-protector \
--disable-dbus-display \
--disable-tpm \
--disable-keyring \
--disable-libkeyutils \
--disable-af-xdp \
--disable-attr \
--disable-auth-pam \
--disable-brlapi \
--disable-bzip2 \
--disable-cap-ng \
--disable-blkio \
--disable-bpf \
--disable-curl \
--disable-gio \
--disable-glusterfs \
--disable-hv-balloon \
--disable-libdw \
--disable-libnfs \
--disable-mpath \
--disable-iconv \
--disable-curses \
--disable-libcbor \
--disable-gnutls \
--disable-nettle \
--disable-gcrypt \
--disable-libdaxctl \
--disable-libpmem \
--disable-libssh \
--disable-libudev \
--disable-libusb \
--disable-lzfse \
--disable-lzo \
--disable-rbd \
--disable-opengl \
--disable-rdma \
--disable-gtk \
--disable-sdl \
--disable-sdl-image \
--disable-seccomp \
--disable-smartcard \
--disable-snappy \
--disable-spice \
--disable-spice-protocol \
--disable-u2f \
--disable-canokey \
--disable-usb-redir \
--disable-l2tpv3 \
--disable-netmap \
--disable-pixman \
--disable-vde \
--disable-vmnet \
--disable-virglrenderer \
--disable-rutabaga-gfx \
--disable-png \
--disable-vnc \
--disable-vte \
--disable-xkbcommon \
--disable-zstd \
--disable-qpl \
--disable-uadk \
--disable-qatzip \
--disable-fuse \
--disable-alsa \
--disable-jack \
--disable-oss \
--disable-pa \
--disable-pipewire \
--disable-sndio \
--disable-vhost-kernel \
--disable-vhost-net \
--disable-vhost-user \
--disable-vhost-crypto \
--disable-vhost-vdpa \
--disable-libvduse \
--disable-capstone \
--disable-selinux \
--disable-replication \
--disable-colo-proxy \
--disable-bochs \
--disable-cloop \
--disable-dmg \
--disable-qcow1 \
--disable-vdi \
--disable-vhdx \
--disable-vmdk \
--disable-vpc \
--disable-vvfat \
--disable-qed \
--disable-parallels \
--disable-qom-cast-debug \
--disable-linux-aio \
--disable-linux-io-uring \
--disable-virtfs \
--enable-lto \
--prefix="${EMULATOR_INSTALL_DIR}" \
--target-list="${KERNEL_ARCHITECTURE}-softmmu"
make -j$(nproc) install
popd # qemu-src
mkdir -p openssh-src/
pushd openssh-src/
wget -O openssh-src.tar.gz "${OPENSSH_SRCURL}"
tar xzvf openssh-src.tar.gz --strip-components=1
# prevent from touching real ~/.ssh folder
find . -type f | xargs -n 1 sed -i \
-e "s|~/\.ssh|$EMULATOR_INSTALL_DIR/tftp|g" \
-e "s|\"~/\"|\"$EMULATOR_INSTALL_DIR/\"|g" \
-e "s|user_pw->pw_dir|\"$EMULATOR_INSTALL_DIR/\"|g" \
-e "s|s->pw->pw_dir|\"$EMULATOR_INSTALL_DIR/\"|g" \
-e "s|pw->pw_dir|\"$EMULATOR_INSTALL_DIR/\"|g" \
-e "s|\"\.ssh\"|\"tftp\"|g"
export CFLAGS="-Wno-incompatible-pointer-types"
touch configure
./configure \
--disable-largefile \
--disable-pkcs11 \
--disable-security-key \
--disable-strip \
--disable-etc-default-login \
--disable-fd-passing \
--disable-lastlog \
--without-shadow \
--with-default-path="" \
--with-privsep-path="${EMULATOR_INSTALL_DIR}"/var/empty \
--with-pid-dir="${EMULATOR_INSTALL_DIR}"/var/run \
--prefix="${EMULATOR_INSTALL_DIR}"
make -j$(nproc)
make install
popd # openssh-src
export PATH="${EMULATOR_INSTALL_DIR}/bin:${PATH}"
truncate -s 64M firmware.img
truncate -s 64M efivars.img
wget -O QEMU_EFI.fd "${EDK2_NIGHTLY_SRCURL}"
dd if=QEMU_EFI.fd of=firmware.img conv=notrunc
qemu-img create -f qcow2 debian-"${USERSPACE_ARCHITECTURE}".qcow2 16G
mkdir -p tftp/
pushd tftp/
ssh-keygen -b 2048 -t ed25519 -f ./id_ed25519 -q -N ""
wget -O netboot.tar.gz "${GUEST_SRCURL}"
tar xzvf netboot.tar.gz
patch -p1 << EOF
--- a/debian-installer/$USERSPACE_ARCHITECTURE/grub/grub.cfg
+++ b/debian-installer/$USERSPACE_ARCHITECTURE/grub/grub.cfg
@@ -3,9 +3,11 @@ set menu_color_highlight=white/blue
insmod gzio
-menuentry 'Install' {
+set timeout=1
+
+menuentry 'Custom Automated Install' {
set background_color=black
- linux /debian-installer/$USERSPACE_ARCHITECTURE/linux --- quiet
+ linux /debian-installer/$USERSPACE_ARCHITECTURE/linux auto=true priority=critical url=tftp://192.168.130.2/preseed.cfg ---
initrd /debian-installer/$USERSPACE_ARCHITECTURE/initrd.gz
}
menuentry 'Graphical install' {
EOF
cat > preseed.cfg << EOF
#_preseed_V1
# this configuration is based on https://www.debian.org/releases/bookworm/example-preseed.txt
d-i debian-installer/locale string en_US
d-i keyboard-configuration/xkb-keymap select us
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string debian-$USERSPACE_ARCHITECTURE
d-i netcfg/get_domain string localdomain
d-i mirror/country string manual
d-i mirror/http/hostname string http.us.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i mirror/suite string $GUEST_RELEASE
d-i passwd/make-user boolean false
d-i passwd/root-password password password
d-i passwd/root-password-again password password
d-i clock-setup/utc boolean true
d-i time/zone string America/Chicago
d-i clock-setup/ntp boolean true
d-i partman-auto/disk string /dev/sda
d-i partman-auto/method string regular
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-auto/choose_recipe select atomic
d-i partman-md/confirm boolean true
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i apt-setup/non-free boolean false
d-i apt-setup/contrib boolean false
d-i pkgsel/include string curl binutils zip jq devscripts
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i grub-installer/bootdev string /dev/sda
d-i finish-install/reboot_in_progress note
d-i debian-installer/exit/poweroff boolean true
grub-pc grub2/linux_cmdline_default string
grub-pc grub-pc/timeout string 1
# uncomment this and remove 'passwd -l root' from preseed_postscript.sh
# to enable ssh password login as root for debugging or development
# (false means enable here, yes really)
#openssh-server openssh-server/permit-root-login boolean false
d-i preseed/late_command string \
in-target curl -O tftp://192.168.130.2/preseed_postscript.sh; \
in-target chmod +x preseed_postscript.sh; \
in-target ./preseed_postscript.sh;
EOF
cat > preseed_postscript.sh << EOF
#!/bin/bash
passwd -l root
mkdir /root/.ssh
chmod 0700 /root/.ssh
curl tftp://192.168.130.2/id_ed25519.pub > /root/.ssh/authorized_keys
echo 'debian-$USERSPACE_ARCHITECTURE' > /etc/hostname
echo 'kernel.pid_max=65535' >> /etc/sysctl.d/local.conf
EOF
popd # tftp
# autoinstallation session
qemu-system-"${KERNEL_ARCHITECTURE}" -M virt -m 12288 -cpu cortex-a72 -smp 4 \
-drive if=pflash,media=disk,format=raw,cache=writethrough,file=firmware.img \
-drive if=pflash,media=disk,format=raw,cache=writethrough,file=efivars.img \
-drive if=none,file="${EMULATOR_INSTALL_DIR}"/debian-"${USERSPACE_ARCHITECTURE}".qcow2,format=qcow2,id=hd0 \
-device virtio-scsi-pci,id=scsi0 \
-device scsi-hd,bus=scsi0.0,drive=hd0 \
-netdev user,id=net0,net=192.168.130.0/24,tftp=tftp,bootfile=debian-installer/"${USERSPACE_ARCHITECTURE}"/bootnet"${GRUB2_ARCHITECTURE}".efi \
-device virtio-net-pci,netdev=net0,mac=52:55:00:d1:55:05 \
-nographic
popd # $EMULATOR_INSTALL_DIR
}
__start_qemu-system() {
# daemonization session
qemu-system-"${KERNEL_ARCHITECTURE}" -M virt -m 12288 -cpu cortex-a72 -smp 4 \
-drive if=pflash,media=disk,format=raw,cache=writethrough,file="${EMULATOR_INSTALL_DIR}"/firmware.img \
-drive if=pflash,media=disk,format=raw,cache=writethrough,file="${EMULATOR_INSTALL_DIR}"/efivars.img \
-drive if=none,file="${EMULATOR_INSTALL_DIR}"/debian-"${USERSPACE_ARCHITECTURE}".qcow2,format=qcow2,id=hd0 \
-device virtio-scsi-pci,id=scsi0 \
-device scsi-hd,bus=scsi0.0,drive=hd0,bootindex=1 \
-netdev user,id=net0,net=192.168.130.0/24,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=net0,mac=52:55:00:d1:55:05 \
-daemonize
}
__run_in_qemu-system() {
ssh -tt -p 2222 -o StrictHostKeyChecking=no -o LogLevel=QUIET root@localhost "$@"
}
__run_in_termux-docker() {
# based on twaik code seen here https://github.com/termux/termux-docker/issues/69
__run_in_qemu-system docker start termux-docker \>/dev/null 2\>\&1 || \
__run_in_qemu-system docker run --detach --init --name termux-docker \
-it termux/termux-docker:"${TERMUX_ARCH}"
__run_in_qemu-system docker exec -itu system termux-docker "$@"
}
__stop_qemu-system() {
__run_in_qemu-system shutdown now
}
# fixes a lot of bugs in termux-docker, preventing annoying things like "Error: None of the mirrors are accessible"
__install_termux-docker_development_branch() {
__run_in_qemu-system git clone -b deblobbify https://github.com/robertkirkman/termux-docker.git
__run_in_qemu-system apt build-dep -y docker.io
__run_in_qemu-system apt source docker.io
__run_in_qemu-system cp termux-docker/custom-docker-with-unrestricted-personality.patch docker.io-\*/debian/patches/
__run_in_qemu-system bash -c \"echo\ 'custom-docker-with-unrestricted-personality.patch'\ \>\>\ docker.io-\*/debian/patches/series\"
__run_in_qemu-system bash -c \"cd\ docker.io-\*/\ \&\&\ DEB_BUILD_OPTIONS=nocheck\ debuild\ -b\ -uc\ -us\"
__run_in_qemu-system rm golang\*
__run_in_qemu-system apt install -y ./\*.deb
__run_in_qemu-system apt-mark hold docker.io
__run_in_qemu-system systemctl enable --now docker
__run_in_qemu-system bash -c \"cd\ termux-docker\ \&\&\ TERMUX_ARCH=$TERMUX_ARCH\ ./generate.sh\"
}
__install_qemu-system
__start_qemu-system
__install_termux-docker_development_branch
__run_in_termux-docker uname -a
__run_in_termux-docker login
__stop_qemu-system
@horvathcsabalaszlo
Copy link

You were right, at first i followed the Qemu Raspberry guide, and it referred a different rootfs package. With your suggestion, it booted (in emergency, but this is another problem).

Thanks for your time :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment