If you work on embedded Linux, one of the most useful skills you can build early is the ability to cross-compile the Linux kernel for a target architecture and boot it without any hardware. In this tutorial you will cross-compile the kernel for ARM64 on an x86-64 host, build a small root filesystem with BusyBox, package it as an initramfs, and boot the whole system inside QEMU. Every command below is reproducible on a normal Ubuntu machine, and the expected output is shown so you can check your progress at each step.
This workflow is the same one used during real board bring-up. The only difference on hardware is the kernel configuration and how the image is loaded. Learning it on QEMU first lets you focus on the build process without worrying about flashing, serial cables, or power.
What you need
You need a 64-bit Linux host (this tutorial uses Ubuntu 22.04 or later), about 20 GB of free disk space, and an internet connection to download the sources. No target board is required. You should be comfortable on the command line and have basic familiarity with make. All work happens in your home directory; nothing is installed system-wide except the toolchain packages.
Install the cross-compiler and build tools
The ARM64 cross-compiler on Ubuntu is provided by the gcc-aarch64-linux-gnu package. It runs on your x86-64 host but produces aarch64 binaries. Install it along with the libraries the kernel build needs and QEMU for ARM system emulation.
raghu@techveda.org:~$ sudo apt update
raghu@techveda.org:~$ sudo apt install -y gcc-aarch64-linux-gnu build-essential
libncurses-dev bison flex libssl-dev libelf-dev bc cpio
qemu-system-arm
Confirm the compiler is present. The prefix aarch64-linux-gnu- is what you will pass to the kernel build as CROSS_COMPILE.
raghu@techveda.org:~$ aarch64-linux-gnu-gcc --version
aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Cross-compile the Linux kernel for ARM64
Download a recent stable kernel from kernel.org. At the time of writing the current stable series is 7.1; check kernel.org for the latest and adjust the version in the commands below.
raghu@techveda.org:~$ wget https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.1.tar.xz
raghu@techveda.org:~$ tar xf linux-7.1.tar.xz
raghu@techveda.org:~$ cd linux-7.1
Set two environment variables so you do not have to repeat them on every make invocation. ARCH=arm64 selects the target architecture and CROSS_COMPILE selects the toolchain prefix.
raghu@techveda.org:~$ export ARCH=arm64
raghu@techveda.org:~$ export CROSS_COMPILE=aarch64-linux-gnu-
Generate a configuration. ARM64 ships a single unified defconfig that already enables everything needed to boot under QEMU, including the PL011 serial driver and initramfs support.
raghu@techveda.org:~$ make defconfig
If you want to confirm the relevant options are set, grep the generated .config. These control the serial console, initramfs loading, and the automatically populated /dev.
raghu@techveda.org:~$ grep -E "CONFIG_SERIAL_AMBA_PL011=|CONFIG_BLK_DEV_INITRD=|CONFIG_DEVTMPFS=" .config
CONFIG_SERIAL_AMBA_PL011=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_DEVTMPFS=y
Now build the kernel image. The ARM64 boot image is called Image (an uncompressed kernel), not bzImage as on x86. Use all CPU cores to speed up the build.
raghu@techveda.org:~$ make -j$(nproc) Image
...
OBJCOPY arch/arm64/boot/Image
Verify the result. The file command should report an ARM64 kernel image.
raghu@techveda.org:~$ file arch/arm64/boot/Image
arch/arm64/boot/Image: Linux kernel ARM64 boot executable Image, little-endian, 4K pages
Build a minimal root filesystem with BusyBox
A kernel alone cannot do anything useful. It needs a root filesystem with at least one program to run as the first userspace process. BusyBox packs the common Unix utilities into a single binary, and when built statically it has no shared-library dependencies, which keeps the root filesystem small and simple.
raghu@techveda.org:~$ cd ..
raghu@techveda.org:~$ wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
raghu@techveda.org:~$ tar xf busybox-1.36.1.tar.bz2
raghu@techveda.org:~$ cd busybox-1.36.1
raghu@techveda.org:~$ make defconfig
Enable static linking. You can do this in make menuconfig under Settings → Build static binary (no shared libs), or set it directly in .config.
raghu@techveda.org:~$ sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
Build and install. Because ARCH and CROSS_COMPILE are still exported from the previous step, BusyBox is cross-compiled for ARM64. The install target places the binary and its symlinks under a directory named _install.
raghu@techveda.org:~$ make -j$(nproc)
raghu@techveda.org:~$ make install
Check that the resulting binary is a statically linked ARM64 executable.
raghu@techveda.org:~$ file _install/bin/busybox
_install/bin/busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ...
Assemble the root filesystem and init script
Create a rootfs directory, copy the BusyBox tree into it, and add the directories the system mounts at runtime. The two device nodes give the kernel a console and a null device before devtmpfs is mounted.
raghu@techveda.org:~$ cd ..
raghu@techveda.org:~$ mkdir -p rootfs
raghu@techveda.org:~$ cp -a busybox-1.36.1/_install/* rootfs/
raghu@techveda.org:~$ mkdir -p rootfs/proc rootfs/sys rootfs/dev rootfs/etc rootfs/tmp
raghu@techveda.org:~$ sudo mknod -m 622 rootfs/dev/console c 5 1
raghu@techveda.org:~$ sudo mknod -m 666 rootfs/dev/null c 1 3
The kernel runs /init from an initramfs as the first process. Create rootfs/init with the following content. It mounts the kernel pseudo-filesystems and then starts an interactive shell.
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "TECH VEDA: minimal ARM64 system is up"
exec /bin/sh
Make the script executable.
raghu@techveda.org:~$ chmod +x rootfs/init
Package the initramfs
An initramfs is a compressed cpio archive that the kernel unpacks into RAM at boot. Build it from inside the rootfs directory so the paths inside the archive are relative, with init at the top level.
raghu@techveda.org:~$ cd rootfs
raghu@techveda.org:~$ find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
4521 blocks
raghu@techveda.org:~$ cd ..
The -H newc option selects the modern ASCII cpio format that the kernel expects. The archive is now ready to hand to QEMU.
Boot the kernel in QEMU
Launch QEMU with the generic virt machine, which is the board model intended for software emulation. Point it at your kernel image and the initramfs, and route the serial console to your terminal with -nographic.
raghu@techveda.org:~$ qemu-system-aarch64
-M virt
-cpu cortex-a57
-smp 2 -m 512M
-kernel linux-7.1/arch/arm64/boot/Image
-initrd initramfs.cpio.gz
-append "console=ttyAMA0"
-nographic
The console=ttyAMA0 argument tells the kernel to use the PL011 UART that the virt board exposes. After a few seconds of boot messages you should see the init script run and a shell prompt appear.
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
...
[ 1.892341] Run /init as init process
TECH VEDA: minimal ARM64 system is up
/ #
You now have an interactive shell running on an emulated ARM64 machine. Confirm the architecture and look at the filesystem. BusyBox install also creates a top-level linuxrc symlink, and our own init script is present alongside it.
/ # uname -m
aarch64
/ # ls /
bin dev etc init linuxrc proc sbin sys tmp usr
To leave QEMU, press Ctrl-A followed by x. Because the root filesystem lives entirely in RAM, any changes you make inside the guest are discarded when you exit, which makes this an easy environment to experiment in and reset.
If you are moving from this QEMU exercise toward real board bring-up and BSP work, our Embedded Linux and Yocto training covers the same build process applied to production hardware, including bootloaders, device trees, and image generation.
Key takeaways
- To cross-compile the Linux kernel you set
ARCHandCROSS_COMPILE, then build the architecture image target; on ARM64 that target isImage, found inarch/arm64/boot/. - The ARM64
defconfigalready enables the PL011 serial console, initramfs support, anddevtmpfs, so a default build boots under QEMU without further configuration. - A statically linked BusyBox plus a small
/initscript is enough for a working userspace; the kernel runs/initas the first process from an initramfs. - The QEMU
virtmachine withconsole=ttyAMA0gives you a complete ARM64 system on your laptop, with no hardware required.
Further reading
- The Linux Kernel documentation
- Using the initial RAM disk (initrd) — kernel.org
- QEMU ARM System emulation documentation
- BusyBox project




