Timeline

2025-12-31

init

2026-01-14

add buildroot


Environment

Host OS

Using WSL2 Archlinux:

Environment

Ubuntu also works, refer to:

Compiler

ARM has released 8 architectures so far: ARMv1, ARMv2, ARMv3, ARMv4, ARMv5, ARMv6, ARMv7, ARMv8.

For processors supporting the ARMv8 instruction set, use -march=armv8-a to compile code. The ARM GNU toolchain can be downloaded from:

For example, the aarch64-linux-gnu-gcc installed via pacman on Archlinux is version 15.1.0 with a flat file layout, which is not suitable for buildroot’s toolchain.

aarch64-linux-gnu-gcc

ARM’s official compiler is recommended — it’s portable and can be used after extraction.

This article uses version 11.2 cross-compiler:

Add it to the environment variables:

1
2
3
# ~/.bashrc or ~/.zshrc
emacs ~/.bashrc
export PATH="$HOME/tools/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin:$PATH"

Using aarch64-none-linux-gnu-gcc version 11.2.1:

aarch64-none-linux-gnu-gcc

Building the Kernel

1
2
3
4
5
6
7
8
9
10
11
sudo pacman -Syu bc
git clone --depth=1 --branch rpi-5.10.y --single-branch https://github.com/raspberrypi/linux.git linux-5.10.y-raspi
make mrproper
make ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- bcm2711_defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- modules_prepare
make ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- Image modules dtbs -j$(nproc)

# Place compiled kernel image and device tree in designated directory
mkdir -p ~/tftp/raspi4b
cp arch/arm64/boot/Image ~/tftp/raspi4b
cp arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb ~/tftp/raspi4b

Outputs:

  • arch/arm64/boot/Image
  • arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb

Building the Root Filesystem

busybox

Reference:

Note that this blog uses the aarch64-linux-gnu-gcc compiler installed via pacman on Archlinux. The compiler should ideally match the kernel compiler — here we choose aarch64-none-linux-gnu-gcc.

When copying dynamic library files, copy from the compiler directory:

1
2
cp ~/tools/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/libc/lib/ld-linux-aarch64.so.1 ./lib/
cp ~/tools/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/libc/lib64/* ./lib64/

After compilation, enter the Linux kernel source directory and install drivers:

1
2
3
4
5
6
7
# Install drivers, copy to rootfs
cd /home/zhaohang/repository/linux/linux-5.10.y-raspi
# busybox
make ARCH=arm64 \
CROSS_COMPILE=aarch64-none-linux-gnu- \
INSTALL_MOD_PATH=/home/zhaohang/repository/linux/busybox-1.37.0/_install/ \
modules_install

buildroot

Archlinux’s default toolchain and the AUR cross-compilation toolchains cannot be used directly because Buildroot copies the toolchain to the working directory during the build process. Therefore, we need a Portable toolchain.

1
2
3
4
5
6
7
wget https://buildroot.org/downloads/buildroot-2025.11.tar.gz
# WSL PATH contains spaces; temporarily reset
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib

tar xvf buildroot-2025.11.tar.gz
cd buildroot-2025.11
make menuconfig

Configuration as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Target Options  --->
Target Architecture (AArch64 (little endian)) --->
Target Architecture Variant (cortex-A72) --->
Floating point strategy (FP-ARMv8) --->
MMU Page Size (4KB) --->
Target Binary Format (ELF) --->

Toolchain --->
Toolchain type (External toolchain) --->
*** Toolchain External Options ***
Toolchain (Custom toolchain) --->
Toolchain origin (Pre-installed toolchain) --->
(/home/zhaohang/tools/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu) Toolchain path
($(ARCH)-none-linux-gnu) Toolchain prefix
External toolchain gcc version (11.x) --->
External toolchain kernel headers series (4.20.x) --->
External toolchain C library (glibc) --->
[*] Toolchain has SSP support? (NEW)
[*] Toolchain has SSP strong support? (NEW)
[ ] Toolchain has RPC support? (NEW)
[*] Toolchain has C++ support?
[ ] Toolchain has D support? (NEW)
[*] Toolchain has Fortran support?
[*] Toolchain has OpenMP support?
[ ] Copy gdb server to the Target (NEW)


System configuration --->
/dev management (Dynamic using devtmpfs + mdev) --->
[*] Enable root login with password (NEW)
(root) Root password


Filesystem images --->
[*] ext2/3/4 root filesystem
ext2/3/4 variant (ext4) --->
(rootfs) filesystem label
(512M) exact size

Do not enable any kernel options here (default is disabled), as we will compile the kernel ourselves. Also, disable all bootloader options since Qemu boot doesn’t require a bootloader.

External toolchain kernel headers series (4.20.x) refers to the kernel version corresponding to the toolchain. Check gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/libc/usr/include/linux/version.h — it should be 4.20.x.

Build:

1
2
sudo pacman -Syu unzip cpio rsync
make

Enter the Linux kernel directory and install drivers:

1
2
3
4
5
6
7
8
9
10
11
12
# Install drivers, copy to rootfs
cd /home/zhaohang/repository/linux/linux-5.10.y-raspi

# buildroot
make ARCH=arm64 \
CROSS_COMPILE=aarch64-none-linux-gnu- \
INSTALL_MOD_PATH=/home/zhaohang/repository/linux/buildroot-2025.11/output/target \
modules_install

make
# Build generates rootfs.ext4
cp output/images/rootfs.ext4 ~/tftp

yocto

1
2
3
4
5
6
mkdir -p ~/repository/linux/yocto
cd ~/repository/linux/yocto
git clone https://git.openembedded.org/bitbake

sudo pacman -Sy chrpath diffstat inetutils rpcsvc-proto
python3 ./bitbake/bin/bitbake-setup init --non-interactive poky-whinlatter poky distro/poky machine/qemuarm64

Yocto will not automatically recognize arbitrary external toolchains!
A dedicated layer must provide support for that toolchain (via tcmode-*.inc and toolchain-*.conf files).
Common support layer: meta-arm → supports ARM’s official GNU Toolchain.

Distribution configurations available in the Yocto Project / OpenEmbedded build system:

  1. poky-master — Poky reference distribution, -master uses the latest main branch (unreleased, under development).

  2. oe-nodistro-master — OpenEmbedded no-distribution mode, -master uses the development mainline.

  3. oe-nodistro-whinlatter — Based on OpenEmbedded nodistro config; uses Yocto 5.3 codename “whinlatter” stable branch; LTS version.

  4. poky-whinlatter — Poky distribution 5.3 “whinlatter” stable version; includes full default configuration (systemd, RPM/deb packages, default toolchain, etc.); LTS version; Yocto Project’s officially tested and certified reference platform.

“whinlatter” is Yocto Project 5.3’s codename (Yocto started using bird names from version 4.0; 5.3 = Whinchat + Lark → “Whinlatter”).

BitBake build configurations (layers / templates):

  1. poky — Standard Poky build config. All packages built from source (unless local cache exists). Safe, reliable, predictable.

  2. poky-with-sstate — Enables remote shared state (sstate) cache. Attempts to download precompiled intermediate artifacts from Yocto’s official or designated sstate mirror servers, significantly speeding up builds. Strict prerequisites: local network must be very stable with sufficient bandwidth; must be able to access external sstate servers; network interruptions or mirror mismatches may cause build failures or inconsistencies. “Use with caution” is the warning here.

sstate is Yocto’s caching mechanism: it saves task outputs (such as do_compile results) so that subsequent builds of the same content can reuse them directly without redoing the work.

Target Machines:

  1. machine/qemux86-64 — Emulates x86_64 PC via QEMU virtual machine.
  2. machine/qemuarm64 — Emulates ARM64 (AArch64) architecture virtual device (e.g., Cortex-A57 based system), also via QEMU.
  3. machine/qemuriscv64 — Emulates RISC-V 64-bit architecture VM.
  4. machine/genericarm64 — Reference config for generic ARM64 real hardware (not emulated). Requires flashing to a real ARM64 dev board.
  5. machine/genericx86-64 — Reference config for generic x86_64 real hardware. Generated image can be written to USB and booted on a real x86_64 PC.

Distribution configuration variants:

  1. distro/poky — Standard Poky distribution. Full Linux system: glibc, systemd, package manager (RPM or IPK), common tools. Default uses systemd as init. Suitable for general development.

  2. distro/poky-altcfg — Poky alternative configuration. Typically used for testing different low-level component combinations (e.g., musl libc instead of glibc, busybox + sysvinit instead of systemd). Less documentation, stability inferior to standard Poky.

  3. distro/poky-tiny — Minimal Poky for resource-constrained devices. Features: musl libc, busybox + sysvinit (no systemd), removes many non-essential packages. Root filesystem can be as small as 10-20 MB. Suitable for microcontroller-level applications.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Enter build environment
cd /home/zhaohang/repository/linux/yocto/bitbake-builds/poky-whinlatter

# Configure
emacs build/conf/local.conf
# Add the following:
IMAGE_FSTYPES = "ext4 cpio.gz"
# EXTERNAL_TOOLCHAIN = "/home/zhaohang/tools/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu"

# Activate current build
source ./build/init-build-env
# Check current configuration
bitbake-config-build list-fragments

# Build minimal rootfs
bitbake core-image-minimal

Kernel Module

Kernel module example code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <linux/init.h>
#include <linux/module.h>

static int __init hello_world_init(void){
printk(KERN_INFO "hello world!\n");
return 0;
}

static void __exit hello_world_exit(void){
pr_info("hello world module exit\n");
}
module_init(hello_world_init);
module_exit(hello_world_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("hello world!");

External Module

Using busybox rootfs as an example.

Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
DRIVER_NAME := hello_world

obj-m += $(DRIVER_NAME).o
KERNEL_SRC:=/home/zhaohang/repository/linux/linux-5.10.y-raspi

PWD ?=$(shell pwd)
ARCH = arm64
CROSS_COMPILE = aarch64-none-linux-gnu-


all: build

build:
$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_SRC) M=$(PWD) modules

clean:
$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_SRC) M=$(PWD) modules clean
rm -rf *.ko *.o *.mod.o *.mod.c *.symvers *.order .tmp*

.PHONY: all deploy qemu
deploy:
cp $(DRIVER_NAME).ko /home/zhaohang/repository/linux/busybox-1.37.0/_install
/bin/bash /home/zhaohang/repository/linux/busybox-1.37.0/deploy.sh

qemu:
qemu-system-aarch64 \
-M raspi4b \
-cpu cortex-a72 \
-m 2G \
-nographic \
-kernel /home/zhaohang/tftp/raspi4b/Image \
-dtb /home/zhaohang/tftp/raspi4b/bcm2711-rpi-4-b.dtb \
-initrd /home/zhaohang/tftp/initramfs.cpio.gz \
-append "console=ttyAMA0 earlycon=pl011,0xfe201000 rdinit=/linuxrc"

The rootfs packaging script deploy.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

INSTALL_PATH=/home/zhaohang/repository/linux/busybox-1.37.0/_install

cd "$INSTALL_PATH" || exit 1

rm -rf ../initramfs.cpio.gz ~/tftp/initramfs.cpio.gz

find . | cpio -o -H newc | gzip -c > ../initramfs.cpio.gz

cp ../initramfs.cpio.gz ~/tftp

echo "cp initramfs.cpio.gz ~/tftp"

There is an issue on WSL2 Archlinux where neither aarch64-linux-gnu-gcc nor aarch64-none-linux-gnu-gcc produces the “CC [M]” build output. A detailed description of the problem is on Stack Overflow:

This issue does not occur with WSL2 Ubuntu 20.04.

Built-in Module

In drivers/char (using char driver as an example), create a helloworld folder, place the driver source inside, then create a Kconfig file:

1
2
3
4
5
config HELLO_WORLD
bool "helloworld support"
default y
help
helloworld

Modify the parent Kconfig:

1
2
3
emacs ../Kconfig
# Add:
source "drivers/char/helloworld/Kconfig"

Create a Makefile in the driver source directory:

1
obj-$(CONFIG_HELLO_WORLD) += helloworld.o

Then add to the parent Makefile:

1
2
3
emacs ../Makefile
# Add:
obj-y += helloworld/

Then rebuild the kernel.

Running in Qemu

Qemu version:

1
2
3
4
$ sudo pacman -Syu qemu-system-aarch64
$ qemu-system-aarch64 --version
QEMU emulator version 10.1.2
Copyright (c) 2003-2025 Fabrice Bellard and the QEMU Project developers

busybox Root Filesystem

For busybox, we compile it into initramfs.cpio.gz and pass it as the initrd parameter:

1
2
3
4
5
6
7
8
9
qemu-system-aarch64 \
-M raspi4b \
-cpu cortex-a72 \
-m 2G \
-nographic \
-kernel /home/zhaohang/tftp/raspi4b/Image \
-dtb /home/zhaohang/tftp/raspi4b/bcm2711-rpi-4-b.dtb \
-initrd /home/zhaohang/tftp/initramfs.cpio.gz \
-append "console=ttyAMA0 earlycon=pl011,0xfe201000 rdinit=/linuxrc"

Press command + a, then x to exit Qemu.

buildroot Root Filesystem

For buildroot, we use an SD card to mount the filesystem:

1
2
3
4
5
6
7
8
9
$ qemu-system-aarch64 \
-M raspi4b \
-cpu cortex-a72 \
-m 2G \
-nographic \
-kernel /home/zhaohang/tftp/raspi4b/Image \
-dtb /home/zhaohang/tftp/raspi4b/bcm2711-rpi-4-b.dtb \
-sd /home/zhaohang/tftp/rootfs.ext4 \
-append "console=ttyAMA0 earlycon=pl011,0xfe201000 root=/dev/mmcblk1 rw rootwait"

Press command + a, then x to exit Qemu.

yocto Root Filesystem

Using initramfs.cpio.gz or SD card to mount the filesystem:

1
2
3
4
5
6
7
8
9
$ qemu-system-aarch64 \
-M raspi4b \
-cpu cortex-a72 \
-m 2G \
-nographic \
-kernel /home/zhaohang/tftp/raspi4b/Image \
-dtb /home/zhaohang/tftp/raspi4b/bcm2711-rpi-4-b.dtb \
-initrd /home/zhaohang/repository/linux/yocto/bitbake-builds/poky-whinlatter/build/tmp/deploy/images/qemuarm64/core-image-minimal-qemuarm64.rootfs.cpio.gz \
-append "console=ttyAMA0 earlycon=pl011,0xfe201000 init=/sbin/init"

References