The root filesystem is the fourth and final element of embedded Linux. Once you have read this chapter, you will be able build, boot, and run a simple embedded Linux system.
This chapter explores the fundamental concepts behind the root filesystem by building one from scratch. The main aim is to provide the background information that you need to understand and make best use of build systems like Buildroot and the Yocto Project, which I will cover in Chapter 6, Selecting a Build System.
The techniques I will describe here are broadly known as roll your own or RYO. Back in the earlier days of embedded Linux, it was the only way to create a root filesystem. There are still some use cases where an RYO root filesystem is applicable, for example, when the amount of RAM or storage is very limited, for quick demonstrations, or for any case in which your requirements are not (easily) covered by the standard build system tools. Nevertheless, these cases are quite rare. Let me emphasize that the purpose of this chapter is educational, it is not meant to be a recipe for building everyday embedded systems: use the tools described in the next chapter for that.
The first objective is to create a minimal root filesystem that will give us a shell prompt. Then, using that as a base, we will add scripts to start up other programs and configure a network interface and user permissions. Knowing how to build the root filesystem from scratch is a useful skill and it will help you to understand what is going on when we look at more complex examples in later chapters.
The kernel will get a root filesystem, either as a ramdisk, passed as a pointer from the bootloader, or by mounting the block device given on the kernel command line by the root=
parameter. Once it has a root filesystem, the kernel will execute the first program, by default named init
, as described in the section Early Userspace in Chapter 4, Porting and Configuring the Kernel. Then, as far as the kernel is concerned, its job is complete. It is up to the init
program to begin processing scripts, start other programs, and so on, by calling system functions in the C library, which translate into kernel system calls.
To make a useful system, you need these components as a minimum:
init
and other programs.init
.init
and other daemons is stored in a series of ASCII text files, usually in the /etc
directory./lib/modules/[kernel version]
.In addition, there are the system application or applications that make the device do the job it is intended for, and the runtime end user data that they collect.
As an aside, it is possible to condense all of the above into a single program. You could create a statically linked program that is started instead of init
and runs no others. I have come across such a configuration only once. For example, if your program was named /myprog
, you would put the following command in the kernel command line:
init=/myprog
Or, if the root filesystem was loaded as a ramdisk, you would put the following command:
rdinit=/myprog
The downside of this approach is that you can't make use of the many tools that normally go into an embedded system; you have to do everything yourself.
Interestingly, Linux does not care about the layout of files and directories beyond the existence of the program named by init=
or rdinit=
, so you are free to put things wherever you like. As an example, compare the file layout of a device running Android to that of a desktop Linux distribution: they are almost completely different.
However, many programs expect certain files to be in certain places, and it helps us developers if devices use a similar layout, Android aside. The basic layout of a Linux system is defined in the Filesystem Hierarchy Standard (FHS), see the reference at the end of this chapter. The FHS covers all implementations of Linux operating systems from the largest to the smallest. Embedded devices have a sub-set based on need but it usually includes the following:
/bin
: programs essential for all users/dev
: device nodes and other special files/etc
: system configuration/lib
: essential shared libraries, for example, those that make up the C library/proc
: the proc
filesystem/sbin
: programs essential to the system administrator/sys
: the sysfs
filesystem/tmp
: a place to put temporary or volatile files/usr
: as a minimum, this should contain the directories /usr/bin
, /usr/lib
and /usr/sbin,
which contain additional programs, libraries, and system administrator utilities/var
: a hierarchy of files and directories that may be modified at runtime, for example, log messages, some of which must be retained after bootThere are some subtle distinctions here. The difference between /bin
and /sbin
is simply that /sbin
need not be included in the search path for non-root users. Users of Red Hat-derived distributions will be familiar with this. The significance of /usr
is that it may be in a separate partition from the root filesystem so it cannot contain anything that is needed to boot the system up. That is what essential means in the preceding description: it contains files that are needed at boot time and so must be part of the root filesystem.
You should begin by creating a staging directory on your host computer where you can assemble the files that will eventually be transferred to the target. In the following examples, I have used ~/rootfs
. You need to create a skeleton directory structure in that, for example:
$ mkdir ~/rootfs $ cd ~/rootfs $ mkdir bin dev etc home lib proc sbin sys tmp usr var $ mkdir usr/bin usr/lib usr/sbin $ mkdir var/log
To see the directory hierarchy more clearly you can use the handy tree
command, used in the following example with the -d
option to show only directories:
$ tree -d ├── bin ├── dev ├── etc ├── home ├── lib ├── proc ├── sbin ├── sys ├── tmp ├── usr │ ├── bin │ ├── lib │ └── sbin └── var └── log
Every process which, in the context of this discussion, means every running program, belongs to a user and one or more groups. The user is represented by a 32-bit number called the user ID or UID. Information about users, including the mapping from a UID to a name, is kept in /etc/passwd
. Likewise, groups are represented by a group ID or GID, with information kept in /etc/group
. There is always a root user with a UID of 0 and a root group with a GID of 0. The root user is also called the super-user because, in a default configuration, it bypasses most permission checks and can access all the resources in the system. Security in Linux-based systems is mainly about restricting access to the root account.
Each file and directory also has an owner and belongs to exactly one group. The level of access a process has to a file or directory is controlled by a set of access permission flags, called the mode of the file. There are three collections of three bits: the first collection applies to the owner of the file, the second to members of the same group as the file, and the last to everyone else, the rest of the world. The bits are for read (r), write (w), and execute (x) permissions on the file. Since three bits fit neatly into an octal digit, they are usually represented in octal, as shown in the following figure:
There is a further group of three bits that have special meanings:
/tmp
and /var/tmp
.The SUID bit is probably the most often used. It gives non-root users a temporary privilege escalation to super-user to perform a task. A good example is the ping
program: ping
opens a raw socket which is a privileged operation. In order for normal users to use ping
, it is normally owned by the root and has the SUID bit set so that, when you run ping
, it executes with UID 0 regardless of your UID.
To set these bits, use the octal numbers, 4, 2, 1, with the chmod
command. For example, to set SUID on /bin/ping
in your staging root directory, you could use the following:
$ cd ~/rootfs $ ls -l bin/ping -rwxr-xr-x 1 root root 35712 Feb 6 09:15 bin/ping $ sudo chmod 4755 bin/ping $ ls -l bin/ping -rwsr-xr-x 1 root root 35712 Feb 6 09:15 bin/ping
For security and stability reasons, it is vitally important to pay attention to the ownership and permissions of the files that will be placed on the target device. Generally speaking, you want to restrict sensitive resources to be accessible only by the root and to run as many of the programs using non-root users so that, if they are compromised by an outside attack, they offer as few system resources to the attacker as possible. For example, the device node /dev/mem
gives access to system memory, which is necessary in some programs. But, if it is readable and writeable by everyone, then there is no security because everyone can access everything. So /dev/mem
should be owned by root, belong to the root group and have a mode of 600, which denies read and write access to all but the owner.
There is a problem with the staging directory though. The files you create there will be owned by you but, when they are installed on the device, they should belong to specific owners and groups, mostly the root user. An obvious fix is to change the ownership at this stage with the command shown here:
$ cd ~/rootfs $ sudo chown -R root:root *
The problem is that you need root privileges to run that command and, from that point onward, you will need to be root to modify any files in the staging directory. Before you know it, you are doing all your development logged on as root, which is not a good idea. This is a problem that we will come back to later.