09 · Building a Rootfs with Buildroot¶
Every lab so far used images someone else built. Time to build your own:
kernel, cross-toolchain, and BusyBox rootfs, from source, configured
entirely by you, booting in the same QEMU you already know. Buildroot
is the tool — a make menuconfig-driven system that turns a single
configuration into a complete, bootable embedded Linux image. This is also
where the pieces from modules 1–8 stop being separate lessons and become
one pipeline you control end to end.
Install prerequisites and get the source¶
Buildroot needs a normal Linux build toolchain plus ncurses for the
config UI (works natively on Linux; on macOS, build inside a Linux VM or
container — Buildroot itself targets Linux hosts):
$ sudo apt install build-essential libncurses-dev git bc cpio unzip rsync \
file wget bison flex
$ git clone https://github.com/buildroot/buildroot.git
$ cd buildroot
$ git checkout 2025.02.1 # pin a real release tag, not a moving branch
Start from a defconfig, then tune it¶
Buildroot ships defconfigs — starting points for real boards and QEMU machines. Ours matches the machine you've used since module 4:
Now open the interactive menu to see — and change — every decision this config made:
Tour it like an embedded engineer, not a checkbox-clicker:
| Menu | What you're deciding |
|---|---|
| Target options | Architecture — already AArch64 (this defconfig's whole point) |
| Toolchain | Buildroot builds its own cross-toolchain (glibc/musl/uClibc, GCC version) — no manual triplet-hunting like module 6 |
| System configuration | Hostname, /etc/inittab — yes, the very inittab from module 8, generated for you |
| Kernel | Version, and which defconfig the kernel itself uses (qemu_aarch64_virt maps to a matching kernel config) |
| Target packages | Every userspace package — BusyBox is on by default; add nano, dropbear (SSH), whatever your product needs |
| Filesystem images | Output format — ext4 for our QEMU disk |
Turn on dropbear (Target packages → Networking applications) — a
tiny SSH server — so your image can be logged into over the network, then
exit saving .config.
Build¶
This compiles a full cross-toolchain, the Linux kernel, BusyBox, and every selected package — expect 30–90+ minutes on a first run (Buildroot caches downloads and build state, so later tweaks rebuild only what changed). When it finishes:
Recognize every one of these from earlier modules: Image is the kernel
(module 3), rootfs.ext4 is your BusyBox userland (module 5) — now
compiled by your toolchain, matched to your kernel, with no
libc-mismatch risk (the module-6 bug is structurally impossible here,
because Buildroot builds the toolchain and the rootfs from the same
config).
Boot your own image in QEMU¶
Compare this to module 4's command — same machine type, but now you supply your own kernel and disk instead of a distro ISO:
$ qemu-system-aarch64 \
-M virt -cpu cortex-a53 -smp 2 -m 512M \
-kernel output/images/Image \
-append "root=/dev/vda console=ttyAMA0" \
-drive file=output/images/rootfs.ext4,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-netdev user,id=net0 -device virtio-net-device,netdev=net0 \
-nographic
Expect a much faster, sparser boot than Alpine — this rootfs contains only what you selected:
Welcome to Buildroot
buildroot login: root
# uname -a
Linux buildroot 6.6.x #1 SMP ... aarch64 GNU/Linux
# cat /etc/os-release
NAME=Buildroot
# busybox | head -1
BusyBox v1.3x.x (....) multi-call binary.
No password on this default config. Confirm dropbear made it in:
# netstat -lnt | grep :22 (or ps | grep dropbear).
Where Yocto fits¶
Buildroot just gave you one image from one config — perfect for a
single product. The moment you need several related products sharing
most software but differing in a few packages, or you need to consume a
silicon vendor's official BSP, the industry reaches for Yocto instead
— it models software as reusable layers (a kernel layer, a vendor BSP
layer, your product layer) and recipes, not one flat config. This is
exactly why NXP ships i.MX support as meta-imx, a Yocto layer — Level 2
is built entirely around this.
| Buildroot | Yocto | |
|---|---|---|
| Mental model | One .config → one image |
Layers + recipes → a customizable Linux distro |
| Learning curve | Days | Weeks |
| Build speed (first build) | Faster | Slower (but shared package cache helps at scale) |
| Multiple similar products | Awkward (config-per-product) | Natural (share layers, vary one) |
| Silicon-vendor BSPs (i.MX, etc.) | Rare/community | Standard delivery format |
| Package management on-device | Not really the point | opkg/rpm/deb variants available |
| Best for | Single products, appliances, learning, fast iteration | Product families, vendor BSP consumption, long-term maintenance |
Rule of thumb this course uses: Buildroot to learn and to ship something small and fixed; Yocto once you're consuming a vendor BSP (i.MX95 included) or maintaining a product line.
Cheat sheet¶
| Command | Purpose |
|---|---|
make qemu_aarch64_virt_defconfig |
Load the QEMU AArch64 starting config |
make menuconfig |
Interactively edit target, toolchain, kernel, packages, filesystem |
make -j$(nproc) |
Build everything: toolchain, kernel, rootfs, image |
output/images/Image |
The built kernel — same role as module 3/4's kernel |
output/images/rootfs.ext4 |
The built root filesystem — your BusyBox userland |
output/host/bin/aarch64-linux-*-gcc |
The toolchain Buildroot built — usable standalone too |
-kernel / -drive ...virtio-blk... (QEMU) |
Boot your own kernel + disk instead of an ISO |
| Buildroot vs Yocto | Single fixed image vs layered, multi-product distro builder |
How It Actually Works¶
make menuconfig's output — .config — isn't consumed directly by the
build; Buildroot treats it as the input to a dependency graph built from
every package's .mk file. Each package declares
<PKG>_DEPENDENCIES, and Buildroot's top-level Makefile topologically
sorts the whole set before building anything, so enabling openssl doesn't
just add one package, it silently pulls in zlib, rebuilds host-openssl
first (a host copy, used only so target openssl's build scripts can run
tools like its own code generator), and only then cross-compiles the
target copy against your toolchain. This host/target package duplication
is why Buildroot's output/host/ and output/target/ trees both exist.
The three-stage pipeline (toolchain → kernel → rootfs) is a strict
dependency order, not just a menu layout: the kernel build needs the
cross-toolchain's headers to compile against, and the rootfs's C library
must be the exact one the toolchain was built with — mixing a glibc
toolchain with a musl rootfs produces binaries the target's dynamic linker
can't load, because glibc and musl disagree on internal ABI details
(errno handling, dlopen behavior). The final image assembly step walks
TARGET_DIR, applies your permission table (/etc/device_table or the
newer S_IFCHR syntax) to fix up device nodes and ownership that can't
survive being built as a non-root user, then hands the tree to genext2fs
or mksquashfs to produce the actual boot image — the point where a
missing file in TARGET_DIR becomes a missing file in the booted system,
with no build error in between.
Exercise¶
(1) Add nano and htop to Target packages, rebuild (make -j$(nproc) —
only the changed pieces recompile), and confirm both run in the booted
image. (2) In make menuconfig → System configuration, change the
hostname and add a line to the root filesystem overlay skeleton
(BR2_ROOTFS_OVERLAY) that drops a file into /etc/motd; rebuild and
confirm it's shown at login. (3) Run
output/host/bin/aarch64-*-readelf -d output/target/usr/bin/nano | grep NEEDED
and compare against what module 6 taught you about dynamic linking and
libc matching — explain in one sentence why this binary is guaranteed to
run on this rootfs. (4) One paragraph: your team ships three i.MX95
products sharing 90% of their software but differing in one application
and one kernel driver — would you keep three separate Buildroot configs
or move to Yocto, and why?