Chapter 35A: Ubuntu-base rootfs as a peer to BusyBox/Buildroot¶
sysfs: a kernel-generated filesystem under /sys that exposes devices, drivers, and attributes.
What: an Ubuntu rootfs on the i.MX6ULL. Ubuntu-the-distro with GNOME is too heavy. We use Ubuntu-base instead, the same Debian-family userland your laptop has, in a 80 MB tarball that runs
aptandbashnatively. You unpack it,chrootinto it viaqemu-user-static, install packages from the host’s network, then NFS-boot the target into it. NFS: Network File System, which lets the target mount a host directory over Ethernet during development. rootfs: root filesystem, the directory tree mounted at / that contains /bin, /etc, /dev, and libraries.Why: for projects where the developer’s familiarity matters more than image footprint, Ubuntu-base wins. Glibc, full coreutils, systemd, an actual
bash, all present, in exchange for ~80 MB on disk and ~30 MB RAM at idle vs. BusyBox’s < 5 MB / < 10 MB.Focus: the
qemu-user-static+chroottrick, installing armhf packages from x86_64 host into the armhf rootfs by transparently running ARM binaries on the host CPU through QEMU emulation. Apt-get doesn’t notice and the workflow works.
35A.1 When this is the right answer¶
Reason |
Buildroot wins |
Ubuntu-base wins |
|---|---|---|
Smallest possible image |
✓ |
|
Fastest boot (<2 s) |
✓ |
|
Compatible with team’s |
✓ |
|
Easy to |
✓ |
|
Reproducible byte-for-byte builds |
✓ |
|
Long-term security backports for free |
✓ (Ubuntu LTS) |
|
Lowest RAM |
✓ |
|
Best for prototypes / dev boards |
✓ |
|
Best for shipping products |
✓ (usually) |
The headline feature is apt install <anything>, Ubuntu’s ~100 000-package archive available, no recompilation. For early development this is hard to beat.
The headline cost is ~25× the disk and ~3× the RAM vs Buildroot. On i.MX6ULL with 512 MiB DRAM and an 8 GB eMMC, both are fine. On a 32 MiB device, only Buildroot fits.
Buildroot: a configuration-driven build system that produces a complete root filesystem and related images.
35A.2 Get the rootfs tarball¶
$ cd ~/imx6ull
$ wget http://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/ubuntu-base-22.04.5-base-armhf.tar.gz
$ mkdir ubuntu-rootfs
$ sudo tar -xzf ubuntu-base-22.04.5-base-armhf.tar.gz -C ubuntu-rootfs/
$ ls ubuntu-rootfs/
bin dev etc home lib media mnt opt
proc root run sbin srv sys tmp usr
var
That’s already a minimal Ubuntu 22.04 LTS rootfs for ARM. Total size: ~80 MB. Use 20.04 or 24.04 if you prefer, same workflow.
Three release tracks Ubuntu publishes:
20.04 LTS: supported through April 2025 (and ESM through 2030).
22.04 LTS: supported through April 2027 (ESM 2032). Current LTS recommended for new work.
24.04 LTS: supported through April 2029 (ESM 2034).
Pick the most-recent LTS unless something specific requires older.
35A.3 Install qemu-user-static on the host¶
The trick that makes the whole flow work:
$ sudo apt install qemu-user-static binfmt-support
$ ls /usr/bin/qemu-arm-static
/usr/bin/qemu-arm-static
qemu-user-static runs one ARM binary at a time on your x86_64 host. binfmt-support registers it with the kernel via binfmt_misc. When the kernel sees exec of an ARM ELF, it transparently invokes qemu-arm-static to run it.
ELF: Executable and Linkable Format, the standard Linux object and executable file format.
This means: inside the chroot, when you type apt install nano, apt is an ARM binary, dpkg is an ARM binary, every .postinst script’s binary callouts are ARM, and they all run on your x86_64 host via QEMU emulation. The chroot doesn’t know the difference. Neither do the binaries.
Copy the QEMU binary into the rootfs so the chroot has it:
$ sudo cp /usr/bin/qemu-arm-static ubuntu-rootfs/usr/bin/
35A.4 The mount-and-chroot script¶
Inspired by the PA workflow, but adapted. Save as ~/imx6ull/mount-ubuntu.sh:
#!/bin/bash
set -e
ROOTFS=$HOME/imx6ull/ubuntu-rootfs
echo "Mounting host filesystems into $ROOTFS..."
sudo mount -t proc /proc "$ROOTFS/proc"
sudo mount -t sysfs /sys "$ROOTFS/sys"
sudo mount --bind /dev "$ROOTFS/dev"
sudo mount --bind /dev/pts "$ROOTFS/dev/pts"
# Copy host resolv.conf so DNS works in chroot
sudo cp /etc/resolv.conf "$ROOTFS/etc/resolv.conf"
# Drop into the rootfs
sudo chroot "$ROOTFS" /bin/bash
And the unmount counterpart ~/imx6ull/unmount-ubuntu.sh:
#!/bin/bash
ROOTFS=$HOME/imx6ull/ubuntu-rootfs
echo "Unmounting..."
sudo umount "$ROOTFS/dev/pts" || true
sudo umount "$ROOTFS/dev" || true
sudo umount "$ROOTFS/sys" || true
sudo umount "$ROOTFS/proc" || true
Make both executable:
$ chmod +x mount-ubuntu.sh unmount-ubuntu.sh
35A.5 Configure the rootfs from inside the chroot¶
$ ./mount-ubuntu.sh
# (host shell becomes ARM shell — transparently, via qemu-arm-static)
root@host:/# uname -m
armv7l # ← we are inside the chroot, claiming to be ARM
You are now inside a fake ARM machine. Whatever you run here runs on QEMU. Whatever you write to the filesystem is in the rootfs.
Set up apt sources¶
The default sources.list in ubuntu-base is empty. Add the official Ubuntu ports archive (ports.ubuntu.com hosts the armhf packages):
root@host:/# cat > /etc/apt/sources.list <<'EOF'
deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse
deb http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse
deb http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse
EOF
(jammy is the codename for 22.04. Use focal for 20.04 or noble for 24.04.)
For users in China who find the official mirrors slow, the Tsinghua or USTC mirror is much faster:
deb http://mirrors.ustc.edu.cn/ubuntu-ports jammy main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu-ports jammy-updates main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu-ports jammy-security main restricted universe multiverse
Install essential packages¶
Privilege boundary: $ means normal user. # or sudo means root and can change host or target state. After a privileged command, verify the expected device, service, or file appears before continuing. Roll back by undoing the config change or stopping the service you just enabled.
root@host:/# apt update
root@host:/# apt install -y \
sudo vim openssh-server kmod net-tools \
ifupdown iputils-ping rsyslog less htop \
language-pack-en-base
That’s the minimum for: a non-root user (sudo), an editor, ssh in, kernel module loading, networking, ping, syslog. ~50 MB of additional packages.
For the curious, this is what runs:
apt update, downloads package metadata. Runs on QEMU. HTTP calls go to the host’s network.apt install, for each package: download.deb, dpkg-extract files into/, run pre/post-install scripts. The scripts themselves are ARM binaries running under QEMU.
Set root password and create a user¶
root@host:/# passwd root
New password: <something>
root@host:/# adduser dev
# Creates /home/dev, sets password
root@host:/# usermod -aG sudo dev
Set hostname¶
root@host:/# echo pa-mini > /etc/hostname
root@host:/# cat > /etc/hosts <<'EOF'
127.0.0.1 localhost
127.0.1.1 pa-mini
EOF
Enable the serial console for systemd¶
Ubuntu uses systemd. We need a getty on ttymxc0 for serial login:
root@host:/# systemctl enable serial-getty@ttymxc0.service
systemctl here just creates a symlink, it doesn’t actually start anything (we’re in a chroot). The symlink ensures the service is enabled on first boot of the target.
Done, exit and unmount¶
root@host:/# exit
# (back to host x86_64 shell)
$ ./unmount-ubuntu.sh
35A.6 Boot it from NFS¶
Export ~/imx6ull/ubuntu-rootfs/ over NFS (same as Ch 31, different rootfs path). In U-Boot:
MCU bridge: Think of U-Boot like a much larger boot stub plus debug monitor: it initializes hardware, loads the next image, and gives you commands before Linux starts. U-Boot: the bootloader that initializes enough hardware to load and start the Linux kernel.
=> setenv bootargs 'console=ttymxc0,115200 earlycon \
root=/dev/nfs nfsroot=192.168.7.1:/home/you/imx6ull/ubuntu-rootfs,vers=3,nolock,tcp \
ip=192.168.7.2:192.168.7.1:192.168.7.1:255.255.255.0::eth0:off \
rw rootwait'
=> run bootnet
After kernel boot:
[OK] Started Serial Getty on ttymxc0.
Ubuntu 22.04.5 LTS pa-mini ttymxc0
pa-mini login: dev
Password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.6.0 armv7l)
...
dev@pa-mini:~$ uname -a
Linux pa-mini 6.6.0 #1 SMP ... armv7l armv7l armv7l GNU/Linux
dev@pa-mini:~$ sudo apt install screen
[sudo] password for dev:
...
dev@pa-mini:~$ which python3
/usr/bin/python3
dev@pa-mini:~$ python3 -c 'print("hello from an ARM Ubuntu")'
hello from an ARM Ubuntu
A full Ubuntu shell. apt install works, python3 is there, every command you’re used to is there.
Boot time on i.MX6ULL: ~12-15 seconds from bootz to login prompt (vs ~3 s for BusyBox). Most of that is systemd’s startup.
35A.7 Persistence, burning to eMMC¶
NFS-rooted Ubuntu is great for development but you eventually want it on the target’s flash.
# On the host, pack the configured rootfs:
$ cd ubuntu-rootfs
$ sudo tar -czf ../ubuntu-rootfs.tar.gz .
$ cd ..
# Format an SD card or eMMC partition (assume /dev/sdb1 is the rootfs partition):
$ sudo mkfs.ext4 /dev/sdb1
$ sudo mount /dev/sdb1 /mnt/rootfs
$ sudo tar -xzf ubuntu-rootfs.tar.gz -C /mnt/rootfs/
$ sudo umount /mnt/rootfs
# Configure U-Boot to boot from eMMC:
# bootargs: root=/dev/mmcblk1p2 rw rootwait ...
The same rootfs works whether served over NFS or mounted from eMMC.
35A.8 Comparison: BusyBox vs Buildroot vs Ubuntu-base¶
BusyBox by hand (Ch 31) |
Buildroot (Ch 35) |
Ubuntu-base (Ch 35A) |
|
|---|---|---|---|
Initial rootfs size |
~5 MB |
~10 MB |
~80 MB |
RAM at idle |
~10 MB |
~15 MB |
~30 MB |
Boot to login |
< 3 s |
< 5 s |
~12 s |
Customisation method |
Edit |
Overlay + defconfig |
|
New package |
Cross-compile + copy |
menuconfig + rebuild |
|
Reproducibility |
Notes |
Defconfig |
Snapshot of |
Long-term security |
You backport |
Buildroot package version |
Ubuntu LTS team |
Best for |
Learning, tiny |
Most embedded products |
Dev boards, prototypes |
For our continuing work in this book, Buildroot is the default. Ubuntu-base is a great option when (a) you’re spinning up a new dev board, (b) you need a weird package that’s in Ubuntu but not Buildroot, or (c) you’d rather pay 70 MB and 10 seconds of boot for the comfort of apt install.
35A.9 Lab¶
Build the Ubuntu rootfs. Follow §35A.2 through §35A.6. Get to a login prompt over NFS.
apt installsomething useful. Tryhtop, thenpython3-pip, thennodejs. Verify each runs.Time the boot. Compare BusyBox rootfs boot time vs Ubuntu-base.
Try
systemd-analyze. On the Ubuntu rootfs,systemd-analyze blameshows which services took longest at boot. Identify the top three.Disable a heavyweight service. Pick one (e.g.,
snapdif present,unattended-upgrades).sudo systemctl disable <name>. Reboot. Compare boot times.Build a packaged image. Tar the rootfs, write to a real partition, change
bootargsto mount from disk, verify the system boots without the host. This is what you’d ship.
35A.10 Pitfalls¶
qemu-arm-staticnot copied into the rootfs. Symptom:chrootfails withExec format errorbecause the kernel doesn’t know how to run an ARM binary. Fix: copy/usr/bin/qemu-arm-staticintoubuntu-rootfs/usr/bin/.binfmt-supportnot installed on the host. Same symptom. Fix:apt install binfmt-supportand restart the service.Forgetting
/etc/resolv.conf. Inside the chroot,apt updatewill fail with “Temporary failure resolving ‘ports.ubuntu.com’”. Fix: copy host’s/etc/resolv.confinto the rootfs beforechroot.mount --bind /devwithout--bind /dev/pts. Symptom:passwdand other terminal-needing commands fail in the chroot. Fix: bind both.Forgetting to unmount. If you reboot the host without
unmount-ubuntu.sh, the host’s/dev/pts/is still bind-mounted under the rootfs. Later operations may misbehave. Always pair mount and unmount.Choosing the wrong armhf. Ubuntu publishes both
armhf(32-bit, hard-float) andarm64(64-bit). I.MX6ULL isarmhf. Downloadingarm64and trying to run it on a 32-bit i.MX6ULL is a slow, confusing failure.Mirror URL with HTTPS but
aptcan’t validate certs. Older Ubuntu base images may not haveca-certificatesinstalled. Usehttp://mirrors initially, installca-certificatesfirst, then switch tohttps://.systemctl enablein chroot is harmless butsystemctl startis not. Inside a chroot,startmay try to manipulate cgroups or dbus and fail. Only useenable/disablefor one-shot config in the chroot.
35A.11 Going deeper¶
debootstrap: Debian’s equivalent ofubuntu-base.tar.gz. Builds a fresh Debian rootfs from package archives. More flexible. More complex.multistrap: debootstrap with support for non-Debian package sources. Used by Yocto when assembling Debian-based images.
Yocto: a metadata-driven build system for producing custom Linux distributions.
schroot: manage chroot environments without writing your own mount scripts.Ubuntu Core: Ubuntu’s official “embedded” variant. Uses snaps instead of apt. Immutable rootfs. Different philosophy from this chapter. Worth knowing about.Yocto-meta-ubuntulayer, combines Yocto-style builds with Ubuntu’s package archive.
Next chapter: Chapter 35B: Read-only rootfs + overlayfs. Whichever rootfs you chose, when you ship to the field, you want it mounted read-only. Here is how.