Making a Raspberry Pi bootable SD card from a root filesystem

Pi logo

I'm quite interested in building custom Linux installations for the Raspberry Pi, particularly for embedded applications. In this article I will demonstrate how to turn a root filesystem into a bootable Raspberry Pi SD card, on a Linux workstation. Note that I'm not explaining here how to create the Linux installation as a root filesystem -- I have a whole series of articles on rolling your own custom Linux for the Raspberry Pi. Here I'm assuming that you already have all the Linux software in place, in some kind of staging directory, and you want to make an image that can be burned onto an SD card (or distributed to other people who will make their own SD cards).

When automated, the process of creating a bootable SD image is reasonably fast (seconds). However, copying the image to the SD card -- particularly if you're using a USB card writer to do it -- can be slow. During the development process, it's often quicker to work on individual files, rather than creating and writing a new image for each change you test.

Overview of the Pi boot process

I'm only going to explain as much here as is needed to understand the rest of the article.

The Pi boots from the first partition of an SD card, which must be in FAT (MS-DOS) format. This partition contains the Pi firmware, which is proprietary, and a set of kernels, one for each supported ARM architecture. For true "bare-metal" programming of the Pi, you can just supply your own "kernel" implementation as one of the kernel files, and your installation is complete. For a regular Linux, the kernel files will be full Linux kernels, compiled with a set of drivers that is sufficient to continue the boot process.

The boot firmware loads and executes the Linux kernel, which by default will mount the second partition of the SD card as its root filesystem. This root partition will initially be mounted read-only, but most Linux installations change this to read-write at some point in the start-up process. The root partition can have any filesystem that the kernel understands -- ext4 is typical.

When the root filesystem is mounted, the kernel will run the executable /bin/init as process number 1. Everything that happens after that is coordinated by this init process. In mainstream Raspbian Linux, "init" is actually the systemd process launcher. However, /bin/init can be anything -- making it a symbolic link to a shell will give you a workable, single-user, console-only Linux system.

Note:
I'm aware that "Raspbian" is now called something else. However, "Raspbian" is the name that appears in much of the documentation, and in the URLs of software repositories, so I will continue to use it.

I'm assuming that you have a viable /bin/init, and everything else that goes with it, in a staging directory for the root filesystem.

The boot partition will generally be the same for all Pi Linux installations -- it will contain the boot firmware and kernels. However, there are subtle customizations that might need to be applied. The most common of these is to provide one or both of the files cmdline.txt and config.txt in the boot partition.

cmdline.txt contains the Linux kernel command line. You can use this to specify certain options, like the location of the root filesystem if it is not in the second partition. I sometimes use the quiet option, so the kernel produces less diagnostic logging on startup (which makes the boot a bit quicker).

config.txt contains a heap of configuration options related to the Pi firmware. It's probably best to copy this file from a Raspbian distribution, and edit it to suit your needs. In practice, the Pi will boot with neither cmdline.txt nor config.txt, because the defaults are sane. However, many applications do require some tweaks in one or both these files.

Create the staging directories

I will store the files for the first disk partition -- the boot firmware -- in /tmp/boot. The root filesystem, destined for the second partition, is in /tmp/rootfs. There's nothing special about these names or locations.

Unless you're compiling your own Linux kernel, your root filesystem will need to contain the kernel modules that match the running kernel, in directory /lib/modules. These modules can be found in the same place as the boot firmware (see below), but you'll need to create the directory /lib/modules to copy them into. So, in summary, you have:

/tmp
  boot (empty at present)
  rootfs
    lib
      modules
    -- and the rest of the root filesystem

Get the firmware

Note:
As is conventional, I show commands that would normally be run as an unprivileged user prefixed with '$', and commands normally run as root prefixed with '#'.

This step only needs to be carried out once or, at least, only rarely. The firmware changes only very infrequently. To fetch the latest firmware, and install it in /tmp/boot:

$ cd /tmp
$ curl -o firmware.zip \
    https://codeload.github.com/raspberrypi/firmware/zip/refs/heads/master
$ unzip firmware.zip   # This creates /tmp/firmware_master
$ cp -aux /tmp/firmware_master/boot/* /tmp/boot
$ cp /path/to/cmdline.txt /tmp/boot # if needed
$ cp /path/to/config.txt /tmp/boot # if needed

Then, to copy the kernel modules into the root filesystem:

$ cp -aux /tmp/firmware-master/modules/* /tmp/rootfs/lib/modules

Finally, clean up the downloaded files.

$ rm -rf firmware_master firmware.zip

We should now have the boot firmware in /tmp/boot, and the root filesystem -- including the kernel modules -- in /tmp/rootfs.

Make the SD card image

We'll be creating an image that contains both boot and root partitions in a single file, ready to copy to an SD card in a single operation. You'll need first to decide how large the SD image needs to be, and how it is to be partitioned. Obviously the partitions need to be large enough to fit the entire contents of /tmp/boot and /tmp/rootfs. I'm assuming that a 2Gb SD card image will suffice for both partitions, of which I will allocate 256Mb for the boot partition, and the rest for the root filesystem. Of course, this image will fit into a larger SD card if one is available.

The process starts by creating an empty file of size 2Gb (or whatever total size you decided on):

# dd if=/dev/zero of=pi.img bs=128M count=16

I've named the file pi.img, but there's nothing special about the name. Make the large file into a loopback block device:

# losetup -fP pi.img

Assuming that no other loopback devices are in use, the empty 2Gb file is now the block device /dev/loop0. We need to partition this device using fdisk, to provide a 256Mb DOS-format boot partition, and a root partition that uses the rest of the file. Here's a script to do that.

# fdisk /dev/loop << EOF
o
n
p
1

+256m
t
c
n
p
2



w
q
EOF

# sync
Note:
The blank lines in this script are significant -- they correspond to pressing "enter" to accept default values from fdisk.

All being well, you should now have two additional block devices: /dev/loop0p1 for the first (boot) partition of the disk image and /dev/loop0p2 for the second (root filesystem) partition.

Now we need to format these partitions:

# mkfs.vfat /dev/loop0p1
# mkfs.ext4 -F /dev/loop0p2

Now mount the first partitition (anywhere, but I'm using /tmp/mnt) and copy the boot files onto it:

# mount -o loop /dev/loop0p1 /tmp/mnt 
# cp -r /tmp/boot/* /tmp/mnt/ 
# umount /dev/loop0p1

Now mount the root filesystem into the second partition and copy the files:

# mount -o loop /dev/loop0p2 /tmp/mnt 
# cp -aux ${ROOTFS}/* /tmp/mnt/ 

I normally build my root filesystems as an unprivileged user, which leaves the files owned by me, not root. It also means that I can't set the suid and sgid permissions on files that need them (and many executables typically do). This wasn't a problem for the boot partition, because that will end up as a FAT partition, which doesn't support owner or permission flags. It's critical to get these attributes right on a Linux filesystem, however.

# chown -R root:root /tmp/mnt/ 
# chmod ug+s /tmp/mnt/usr/bin/sudo
# chmod ug+s /tmp/mnt/bin/ping
# chmod ug+s /tmp/mnt/usr/bin/passwd
# chmod ug+s /tmp/mnt/bin/su
# chmod ug+s /tmp/mnt/bin/login
# ... and maybe others

Finally, clean up -- unmount and disconnect the loopback devices:

# umount /dev/loop0p2
# losetup -D

At this point, the file pi.img contains a bootable image, ready to be distributed, or copied to an SD card.

Copy the image to an SD card

The method I've described above produces an SD card image that is in exactly the same format as regular Raspberry Pi Linux distributions, so the process for installing on an SD card is well-documented. On a Linux workstation, however, it's trivially easy:

# dd if=pi.img of=/dev/sdXXX bs=128M 

/dev/sdXXX should be replaced by the real device that corresponds to the SD card. Of course, care must be taken here, because the dd command will blithely overwrite a filesystem full of data.

Closing remarks

The steps I described above are tedious, but they can easily be scripted. When scripted, the whole process takes about ten seconds on a typical Linux workstation. A disadvantage of the process I describe -- which applies to mainstream Pi Linux distributions as well -- is that the size of the root filesystem is set when the image is created. Copying the image onto an SD card with larger capacity doesn't get you a larger root filesystem at runtime. However, because this is such a common problem, solutions to it are well documented.