Build a tiny custom Linux kernel inside a Podman container and boot it against an Ubuntu cloud image with QEMU — entirely on an Apple Silicon Mac, no sudo required.
The whole point is a fast edit‑compile‑run cycle: boot to a userspace shell in well under a second thanks to hardware acceleration (HVF).
This is the arm64 variant. Because the Mac is itself arm64, the kernel is built
natively (no cross‑compiler) and QEMU runs the guest with the hvf accelerator at
near‑native speed. (The original write‑up targeted x86_64; everything below is adapted
for aarch64.)
- A
~4 MBarch/arm64/boot/Imagebuilt from atinyconfigbase + just enough drivers. - Boots a stock Ubuntu Noble cloud image straight into
/bin/bashas init. - Verified boot log:
virtio_blk ... [vda]→EXT4-fs (vda1): mounted→Run /bin/bash as init process.
brew install qemu podman
podman machine init # if you don't already have a machine
podman machine startYou also need a Linux kernel source tree. This guide assumes it lives at ~/linux:
git clone --depth=1 https://github.com/torvalds/linux.git ~/linuxThe kernel must be built on Linux, so we use a container. The host ~/linux is mounted
in, so the build artifacts land in your real tree.
podman run -dit --name kbuild -v "$HOME/linux:/linux" -w /linux ubuntu:24.04 bash
podman exec kbuild bash -c 'apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential flex bison libssl-dev libelf-dev bc libncurses-dev cpio kmod'Because the container is arm64 (matching the Mac), the native
gccbuilds an arm64 kernel directly — noCROSS_COMPILEneeded.
Start from tinyconfig and enable only what's needed to (a) print to the serial
console, (b) find the virtio disk over the QEMU virt machine's PCIe host bridge, and
(c) mount an ext4 rootfs and exec userspace.
podman exec kbuild bash -c '
cd /linux
make ARCH=arm64 tinyconfig
# console + printk
./scripts/config -e PRINTK -e PRINTK_TIME -e TTY
# block layer + virtio disk
./scripts/config -e BLOCK -e BLK_DEV
./scripts/config -e PCI -e VIRTIO_MENU -e VIRTIO_PCI -e VIRTIO_BLK -e VIRTIO_MMIO
# *** the part tinyconfig misses: the arm64 "virt" PCIe ECAM host bridge ***
# without this the PCI bus is never enumerated and the disk never appears
./scripts/config -e PCI_HOST_GENERIC -e PCI_HOST_COMMON -e PCI_ECAM
# root filesystem
./scripts/config -e EXT4_FS
# run userspace binaries + scripts
./scripts/config -e BINFMT_ELF -e BINFMT_SCRIPT -e BINFMT_MISC
# almost nothing works without these
./scripts/config -e PROC_FS -e SYSFS -e DEVTMPFS -e DEVTMPFS_MOUNT
./scripts/config -e FUTEX -e MULTIUSER
# arm64 "virt" platform essentials (replaces the x86 8250 UART path):
# PL011 serial = ttyAMA0, GICv3 interrupt controller, arch timer, PSCI
./scripts/config -e SERIAL_AMBA_PL011 -e SERIAL_AMBA_PL011_CONSOLE
./scripts/config -e ARM_GIC -e ARM_GIC_V3
./scripts/config -e ARM_ARCH_TIMER
./scripts/config -e ARM_PSCI_FW
./scripts/config -e POWER_RESET -e POWER_RESET_SYSCON
./scripts/config -e OF -e OF_FLATTREE
# debug symbols (so you can attach with qemu -s and gdb)
./scripts/config -e DEBUG_INFO_DWARF5
make ARCH=arm64 olddefconfig
'| x86_64 (original) | arm64 (this guide) |
|---|---|
-e 64BIT |
n/a (arm64 is always 64‑bit) |
SERIAL_8250* → ttyS0 |
SERIAL_AMBA_PL011* → ttyAMA0 |
| (implicit i440fx/PCI) | PCI_HOST_GENERIC required for virt ECAM |
| — | ARM_GIC_V3, ARM_ARCH_TIMER, ARM_PSCI_FW |
arch/x86_64/bzImage |
arch/arm64/boot/Image |
qemu-system-x86_64 |
qemu-system-aarch64 -machine virt -cpu host -accel hvf |
podman exec kbuild bash -c 'cd /linux && make ARCH=arm64 -j"$(nproc)" Image'
# -> arch/arm64/boot/Image (~4 MB)mkdir -p ~/kernel-qemu && cd ~/kernel-qemu
curl -LO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-arm64.imgcd ~/kernel-qemu
qemu-system-aarch64 \
-nographic \
-machine virt,gic-version=3 \
-cpu host -accel hvf \
-m 2048 -smp 4 \
-kernel "$HOME/linux/arch/arm64/boot/Image" \
-drive file=noble-server-cloudimg-arm64.img,if=virtio,format=qcow2 \
-append "console=ttyAMA0 root=/dev/vda1 init=/bin/bash" \
-snapshot-accel hvf -cpu host→ hardware‑accelerated, near‑native arm64 guest.console=ttyAMA0→ the PL011 UART (arm64), notttyS0.root=/dev/vda1 init=/bin/bash→ mount the cloud image and drop straight into a shell.-snapshot→ discards writes on exit, so the image stays pristine and reusable.
You'll land in root@(none):/# with all the usual userspace tools (lspci, ls, …).
To quit QEMU from -nographic: Ctrl-a x.
cat > ~/kernel-qemu/run.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
exec qemu-system-aarch64 -nographic -machine virt,gic-version=3 \
-cpu host -accel hvf -m 2048 -smp 4 \
-kernel "$HOME/linux/arch/arm64/boot/Image" \
-drive file=noble-server-cloudimg-arm64.img,if=virtio,format=qcow2 \
-append "console=ttyAMA0 root=/dev/vda1 init=/bin/bash" -snapshot
EOF
chmod +x ~/kernel-qemu/run.sh- Hack on
~/linux. podman exec kbuild bash -c 'cd /linux && make ARCH=arm64 -j"$(nproc)" Image'~/kernel-qemu/run.sh
Rebuilds are incremental and the boot is sub‑second — the fast cycle the original write‑up is all about.
Prompt for Claude Code
Paste this into Claude Code (running on an Apple Silicon Mac with
podman+qemuinstalled) to do the whole thing end‑to‑end: