Skip to main content

TECH VEDA

Embedded Linux on Edge-AI 23rd Sept 2026 enrollingLinux kernel & Device drivers starts on 24th Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulesSharpen your kernel skills: deep dives, drivers, Yocto, CVEs, careers — updated daily. Read the blog →Embedded Linux fast track starts 23rd sept 2026 enrollingEmbedded Linux Mastery track starts 23rd sept 2026 enrollingLinux systems engineering starts 23rd sept 2026 enrolling
Tutorials

Cross-Compile the Linux Kernel for ARM64 and Boot It in QEMU

Cross-compile the Linux kernel for ARM64, build a BusyBox initramfs, and boot the system in QEMU using exact, reproducible commands and expected output.

Cross-Compile the Linux Kernel for ARM64 and Boot It in QEMU

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 ARCH and CROSS_COMPILE, then build the architecture image target; on ARM64 that target is Image, found in arch/arm64/boot/.
  • The ARM64 defconfig already enables the PL011 serial console, initramfs support, and devtmpfs, so a default build boots under QEMU without further configuration.
  • A statically linked BusyBox plus a small /init script is enough for a working userspace; the kernel runs /init as the first process from an initramfs.
  • The QEMU virt machine with console=ttyAMA0 gives you a complete ARM64 system on your laptop, with no hardware required.

Further reading

Was this worth your time?
RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.