This guide documents a reproducible workflow to:
- Build the Linux kernel (ARM64) using Docker
- Build a BusyBox-based initramfs
- Boot the kernel using QEMU (AArch64) on macOS (Apple Silicon / M1–M3)
- macOS on Apple Silicon (M1 / M2 / M3)
- Docker Desktop (Compose v2)
- Homebrew
- QEMU (AArch64)
Install QEMU:
brew install qemu.
├── docker-compose.yml
├── Dockerfile
├── linux/
├── out/
├── initramfs/
└── initramfs.cpio.gz
docker compose -f docker-compose.yml builddocker compose -f docker-compose.yml run --rm kernel-builderOr one-shot build:
docker compose run --rm kernel-builder make -C /work/linux O=/work/out ARCH=arm64 -j$(nproc)cd /work
git clone --depth 1 --branch v6.12 https://github.com/torvalds/linux.git
# or git clone --depth 1 https://github.com/torvalds/linux.git
cd linux
# if you have older linux cloned folder to cleanup
# or git clean -idx for intractive clean
git clean -fdx
make ARCH=arm64 O=/work/out defconfig
make ARCH=arm64 O=/work/out -j$(nproc)Kernel image produced:
/work/out/arch/arm64/boot/Image
clone BusyBox → /work/busybox
cd /work
git clone --depth 1 --branch 1_37_0 https://github.com/mirror/busybox.git
# or git clone https://github.com/mirror/busybox.git
cd busybox
make distclean
make defconfig ARCH=arm64
make menuconfigDisable:
- Settings → SHA1/SHA256 hardware acceleration
- Networking Utilities → tc
Enable static build:
sed -i.bak 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .configBuild and install:
make ARCH=arm64 -j$(nproc)
make ARCH=arm64 CONFIG_PREFIX=/work/initramfs installThis installs BusyBox into:
/work/initramfs/{bin,sbin,usr/bin,...}
cat > /work/initramfs/init <<'EOF'
#!/bin/sh
# 1. Mount basic filesystems
mkdir -p /proc /sys /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
# 2. Mount devtmpfs FIRST
# This populates /dev with device nodes like console, null, etc.
mount -t devtmpfs none /dev
# 3. NOW create pts inside the newly mounted /dev and mount devpts
mkdir -p /dev/pts
mount -t devpts none /dev/pts
echo "Booted into initramfs!"
echo "Kernel: $(uname -a)"
# drop into a shell
# exec /bin/sh
# cttyhack detects the console and runs the shell with a controlling terminal
exec setsid cttyhack /bin/sh
EOFchmod +x /work/initramfs/initcd /work/initramfs
# mkdir -p proc sys dev dev/pts tmp # Not required as init script will do it
find . -print0 | cpio --null -ov --format=newc | gzip -9 > /work/initramfs.cpio.gz
# exit from container
exitNow you have:
- /work/out/arch/arm64/boot/Image
- /work/initramfs.cpio.gz
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a72 \
-m 2048 \
-nographic \
-kernel ./out/arch/arm64/boot/Image \
-initrd ./initramfs.cpio.gz \
-append "console=ttyAMA0 loglevel=8 rdinit=/init"Exit QEMU:
Ctrl + A, then X
You are now running a custom ARM64 Linux kernel in QEMU on macOS.