How does Deploying Root Filesystem Images work in Linux-Free Embedded Linux Course online

PREV_LEC NEXT_LEC
Deploying Root Filesystem Images
A free embedded Linux course lecture — pack your staging directory into a real image and put it on target hardware
Format: ext2 image
Tool: genext2fs
Target: SD card / eMMC

Every embedded Linux board eventually needs its root filesystem sitting somewhere the bootloader can hand off to — an SD card partition, an eMMC partition, or a raw NAND/NOR region. This lecture is part of our free embedded Linux course and walks through turning a staging directory into a real root filesystem image, then getting that image onto a target board and booting it. If you’ve been following this free linux kernel development course, you already have a populated staging directory from earlier lectures — now we make it bootable.

root filesystem image genext2fs device table free embedded systems course free linux device drivers course U-Boot bootargs

What You Will Learn

Lecture Outcomes

Device table syntax for unprivileged builds Generating an ext2 root filesystem image Sizing an image correctly Writing the image to an SD card / eMMC Setting bootargs so the kernel finds the right root Choosing the right image format for your use case

Prerequisites

You should already have a staging directory built from the earlier lectures in this root filesystem image series — BusyBox or ToyBox installed, shared libraries copied in, device nodes planned, and an init system chosen. You’ll also need a Linux host with the standard build-essential toolchain, and a target board (real hardware or QEMU) whose bootloader can load a kernel and mount a block-device root.

Why You Can’t Just Copy Files In as Root

A staging directory built by an ordinary user has one structural problem: device nodes need root privileges to create with mknod, and many files in a real root filesystem — /dev/console, /dev/null, setuid binaries — need ownership and permission bits that an unprivileged build process cannot set directly. Rather than running your entire build as root, the standard approach is a device table: a plain text file that describes the extra nodes, ownership, and permission overrides to apply while the image is generated, without ever needing root on the build host.

Each line in a device table describes one path and the metadata to stamp onto it:

path type mode uid gid major minor start inc count
  • type — f (regular file), d (directory), c (character device), b (block device), or p (pipe)
  • mode — octal permission bits
  • uid / gid — numeric owner and group to stamp on the node
  • major / minor — device numbers, only meaningful for c/b entries
  • start, inc, count — only meaningful for device nodes; they let one line generate a numbered group of nodes, starting at minor number start and incrementing by inc, count times

Here’s an original device table for a small target with a UART console and a single I2C bus exposed to user space:

#<path>       <type> <mode> <uid> <gid> <major> <minor> <start> <inc> <count>
/dev          d      755    0     0     -       -       -       -     -
/dev/console  c      600    0     0     5       1       -       -     -
/dev/null     c      666    0     0     1       3       -       -     -
/dev/ttyS0    c      600    0     0     4       64      -       -     -
/dev/i2c-0    c      660    0     20    89      0       -       -     -

Fields left as - aren’t used for that entry. The last row shows the group id set to 20 so the i2c group (not root) can access the bus — a pattern worth reusing anywhere you want a device node accessible to a non-root daemon.

Build Flow: Staging Directory to Bootable Image
staging/ (user-owned files) → device-table.txt (root metadata) → genext2fs (builds image) → rootfs.ext2 → dd to SD/eMMC

Generating the Image with genext2fs

genext2fs reads your staging directory plus the device table and writes out a complete ext2 image in one pass, applying every override from the table without touching the real filesystem permissions on your build host. You do not list every file individually the way you would with gen_init_cpio for an initramfs — you point it at the staging directory and it walks the tree itself.

Size the image with headroom: a bare BusyBox-based rootfs is usually a few megabytes, but leave room for logs, temporary files, and future package installs. This example builds an 8 MiB image (8,192 blocks of the default 1,024-byte block size):

$ genext2fs -b 8192 -d staging -D device-table.txt -U rootfs.ext2
$ ls -lh rootfs.ext2
-rw-r--r-- 1 dev dev 8.0M Aug 14 09:12 rootfs.ext2
FlagMeaning
-bTotal blocks in the image (block size × blocks = final image size)
-dSource directory to populate the image from
-DDevice table file — ownership, permissions, and device nodes to stamp in
-UAssign sequential inode UIDs/GIDs matching the host, useful for reproducible builds

The result, rootfs.ext2, is a self-contained image file you can mount, inspect with debugfs, or write straight to a block device.

Writing the Image to an SD Card

Identify which partition holds your root filesystem before you write anything — an accidental dd to the wrong device node will happily overwrite your host’s disk. Check with lsblk first, then write to the correct partition, not the whole-disk device:

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
mmcblk0     179:0    0 14.9G  0 disk
├─mmcblk0p1 179:1    0  100M  0 part /media/dev/boot
└─mmcblk0p2 179:2    0 14.8G  0 part

$ sudo dd if=rootfs.ext2 of=/dev/mmcblk0p2 bs=4M status=progress
$ sync

bs=4M speeds up the write considerably over the default 512-byte block size, and status=progress gives you visible feedback on a large card. The trailing sync matters — without it, part of the write can still be sitting in the host’s page cache when you pull the card.

Telling the Kernel Where to Find It

With the image on the card, the kernel needs a matching root= argument on its command line so it knows which block device to mount as /. Set this through your bootloader’s environment before loading the kernel:

=> setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait
=> fatload mmc 0:1 0x82000000 zImage
=> fatload mmc 0:1 0x83000000 board.dtb
=> bootz 0x82000000 - 0x83000000

rootwait is easy to forget and causes intermittent boot failures on real hardware: it tells the kernel to wait for the SD/eMMC controller to finish enumerating the card before it tries to mount root, which matters because that enumeration can finish after the rest of early boot on some controllers.

Choosing an Image Format for the Job

FormatBest forTrade-off
ext2 (genext2fs)SD card / eMMC production imagesWritable, journalless — fine for read-mostly rootfs
squashfsRead-only, compressed production imagesSmaller flash footprint, needs an overlay for writable paths
cpio initramfsEarly boot, recovery images, diskless boardsLives fully in RAM — no persistent storage needed
UBIFSRaw NAND flashHandles wear-leveling and bad blocks that ext2 cannot

Common Mistakes and Troubleshooting

  • Writing to the whole-disk node instead of the partition — /dev/mmcblk0 instead of /dev/mmcblk0p2 destroys the partition table. Always confirm with lsblk first.
  • Image too small — genext2fs fails outright, or worse, silently truncates content, if -b is undersized. Size generously and check the exit status.
  • Forgetting rootwait — boot works from a fast desktop card reader in testing, then fails intermittently on the real board once the controller timing differs.
  • Device table typos — a wrong major/minor number produces a device node that exists but refers to the wrong driver; ls -l /dev after mounting the image will show the numbers so you can cross-check against Documentation/admin-guide/devices.txt.

Best Practices

  • Keep the device table under version control alongside your staging build scripts — it’s as much a build artifact as the kernel config.
  • Generate a checksum (sha256sum rootfs.ext2) alongside every image you ship, so field updates can be verified before flashing.
  • Prefer read-only root plus a small writable overlay in production; a read-only ext2/squashfs image is far more resilient to power loss during writes than a fully writable one.

Security Considerations

An unsigned image written straight to a card can be swapped for a malicious one by anyone with physical access. For production devices, pair this workflow with your bootloader’s image-verification support (signed FIT images or equivalent) rather than trusting an unauthenticated block device blindly.

Summary

Device tables let you stamp root-owned metadata onto a staging directory without ever running the build as root. genext2fs turns that staging directory plus the table into a real ext2 root filesystem image, sized generously and written to the correct partition with dd. Matching root= and rootwait bootargs complete the handoff so the kernel mounts exactly the image you built. This same device-table technique carries over directly to squashfs and UBIFS images later in this free linux device drivers course.

FAQ

Why do I need a device table instead of just running mknod as root during the build?

You can run as root, but it complicates CI pipelines, container builds, and reproducibility — the device table achieves the same result from an unprivileged build step.

What happens if my rootfs image is too small for genext2fs?

The build fails with a “no space” style error. Increase the block count passed to -b and rebuild; always leave headroom beyond the staging directory’s current size.

Can I resize an ext2 image after it’s built?

Yes, with resize2fs, but it’s simpler to rebuild from the device table with a larger -b value during development.

Do I need rootwait on every board?

It’s needed whenever the storage controller can finish probing after the kernel would otherwise try to mount root — which is common enough on SD/eMMC that it’s safe to always include.

Is ext2 a good choice for a production embedded root filesystem?

It’s fine for a writable root on eMMC/SD, but squashfs or UBIFS are usually better for flash-constrained, read-mostly production images.

How is this different from the initramfs images covered earlier in this course?

An initramfs lives entirely in RAM and is unpacked at boot; this ext2 image is a persistent, block-device-backed root that survives a reboot without being rebuilt.

Continue the Free Embedded Linux Course

Next up: mounting your root filesystem over NFS for a much faster development loop.

PREV_LEC NEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *