Booting an Embedded OS: the Booting and U-Boot Phase
Published at – 12 min read – 2372 words
In the first post, we introduced embedded devices and started to scour through and extract information from Reolink IP camera firmware. At the end of the post, we ran Binwalk, which showed various types of files such as Flattened Device Tree, uImage Header, and UBI File System.
For the second article in this Reolink series, we are going to introduce the theory regarding the various stages of booting the operating system and explore the different types of files. So, let’s begin our analysis.
The booting phase (i.e., loading) constitutes a critical phase of our operating system. When we turn on any device, be it a TV or a general-purpose computer, the system must execute a series of routines to load modules and executables that will be needed later to continue executing other tasks. Network management, routines to load executables ― these are all services that ensure the stability of the operating system and, therefore, are essential to start using the device.
Just before starting the operating system, all devices are in a reset state. The RAM interior is empty and I/O devices are ready to be called and executed. As soon as the power button is pressed, however, this is when a series of actions are performed by the device(s) to load the OS.
Each time a device is turned on, execution starts from a module called read-only memory (ROM), provided by the manufacturer, which takes care of some tests and starts loading the operating system. Once it is loaded, control passes to the operating system, which has full authority to create, modify, and delete any data structure and value in the CPU, memory, and I/O devices. The bootloader is also responsible for running a series of tests (such as the Power On Self Test) to check whether everything is actually working at the hardware level. Tests include low-level checks (e.g., voltage or circuitry) to see whether or not the ROM can proceed with the actual boot.
Without going into too much technical detail, we can think of the ROM or bootloader as a program. The bootloader is executed by the CPU, which loads a number of other components of the operating system. With embedded devices, consisting of a specific board (thus, a particular hardware configuration), we can optimize the booting of the operating system. The bootstrap (i.e., the loading phase) must be very fast and must require as few resources as possible to avoid wasting power. A core requirement that the development of embedded-based designs must consider is time. Since these devices interface and are used in a real-world context, the response time must be very short ― from microseconds to tens of nanoseconds.
Most embedded operating systems use U-Boot (also called Das U-Boot, from a pun based on the classic 1981 movie Das Boot, set on a German submarine) as their booloader. Written primarily in C and Assembly, U-Boot is an open-source project developed by Magnus Damm, considered to this day to be the richest, most flexible and most actively developed bootloader. It supports various architectures such as ARM, MicroBlaze, MIPS, PPC, RISC-V, x86, and also contains some drivers for embedded device development boards. It is used by a wide number of devices made by Nintendo, along with Chromebooks, Raspberry PI, and not to mention automotive boards.
U-Boot supports FAT, ext2/3/4, CramFS, SquashFS, JFFS2, UBIF, ZFS, and many others as file systems. This mainly means that U-Boot is very powerful ― supporting both different hardware configurations and multiple operating systems. The bootloader implements a subset of specifications compatible with UEFI systems and starts the operating system through a number of environment variables (one in particular, the most important,
bootargs) that we will address later in this article.
U-Boot requires that commands specified on bootargs be low-level and, therefore, explicitly specify physical memory addresses as the destination for copying data. This, on the one hand, adds complexity for the developer who must know the technical details, but, on the other hand, allows for minimal overhead (i.e., overhead time).
Another feature that U-Boot supports is the Flattened Device Tree file, a data structure that is used to describe hardware configuration. In general-purpose devices, when the power button is pressed, the bootloader polls all the devices to get information about the manufacturer ― what kind of device it is, its state, and a lot of other information. In embedded devices, we cannot afford to waste valuable time! So, I include within my bootloader a device tree to allow the machine to already load all the information about my hardware. The device tree will be explored in depth during the third part of this series.
Having concluded the more technical part of U-Boot, let’s specify what happens during the loading of a unix-based operating system in the points below. To better describe the booting phase, let’s break it down into seven distinct points.
Booting the device and hardware routines that serve to stabilize the voltage inside the board:
The CPU as well as many other devices within a board are controlled through electrical signals ― this current is generated by a potential. Once the circuit is turned on, this potential must be constant over time (or at least its fluctuations must remain minimal) to avoid damaging the circuit. The only concern for this phase is bringing electric current to each component. Components (such as the CPU) are in the reset state and ready to be used. If there are broken or damaged components (i.e., if the potential is not the standard one), the power-up will not continue.
Performance of POST (Power-On Self-Test):
This phase involves running a series of tests that verify hardware integrity before proceeding to the next phase. In the event that this test fails, the machine will not power-on and show no signs of life.
Hardware device initialization:
Once the tests are done, device controllers such as static random access memory (SRAM), serial ports, and the interrupt service table are initialized. When a CPU starts up, it performs some internal consistency checks and transfers control to a programmable read-only memory (PROM) or erasable programmable read-only memory (EPROM) device that contains non-volatile code (which is intended to survive a power loss). Everything is ready to load the first booting stage.
Loading the minimal bootloader (first part):
Reading a set of routines from the ROM that initializes a RAM internal to the chip. This step allows you to use the minimal bootloader, which will read our device and load the second part of the bootloader. Once the procedure is done, the CPU jumps to a predefined memory address to continue execution. Dynamic random access memory (DRAM, i.e., main RAM memory as we understand it) is prepared, filling the entire area of uninitialized data with zeros. It allocates space for the stack and initializes registers (such as the stack pointer).
Reading the U-Boot bootloader (second part):
The second part of the bootloader is loaded and read from the mass storage that we are using (in our case, flash memory). Two pointers here are critical: the base address of the executable image that will load the operating system (kernel) and the base address of RAM. Execution then jumps to the RAM base address that will contain the first instructions to be executed for the third part.
Content reading (third part):
The bootloader decompresses the contents of the operating system contained in the microSD. The checksums (if any) of the image are checked and it is verified whether the execution environment matches the type of kernel to be loaded. The
uImageis loaded into the address space in RAM and the (Device Tree Blob) (DTB) is initialized using the information contained in the DTB file. The bootloader has finished its work and execution can jump to the first Linux kernel instruction.
The kernel (
initrd) is executed:
The physical file system is mounted (i.e., loaded into memory) and runs the first routines for the operating system (low-level services that interface with hardware controllers). It runs
init, which allows loading all the modules required by the operating system and the high-level user ― system services, user services, and GUI services. The two virtual file systems (
/devfor devices and
/proc) are also mounted.
The operating system then loads all the binaries necessary for proper camera operation (web server, daemon for video management, various controllers, etc.) and waits for new commands.
U-Boot Environment Variables
The following is a brief explanation of the parameters that can be set on U-Boot:
|if set, the image loaded into memory will be executed automatically|
|contains the command that U-Boot will use just after loading all the minimum components to run the operating system|
|contains the arguments passed to the Linux kernel|
|time in seconds to wait before U-Boot continues execution|
|you can specify a server IP address that affects network commands|
|the local IP address of the target|
|the MAC address|
|the netmask for communicating with the server|
U-Boot Parameters for Reolink Firmware
To find the Reolink parameters used for U-Boot, without knowing any reverse engineering tools, we can use one of the oldest utilities called
strings. Strings is a command that reads data from a file ― searching for byte sequences representing valid strings of visible characters. It presents a very simple syntax:
We can combine strings with a more powerful tool called the General Regular Expression Print (GREP) which looks for lines that match one or more patterns specified with RegExs or simple strings. Setting aside the idea of using RegExs, we proceed to find one of U-Boot’s parameters called
bootargs. Most existing firmware using U-Boot have always set this parameter, because it allows you to specify booting options.
We then combine the power of GREP with strings, using
| to combine the two commands:
strings firmware_rlc_810_a.pak.bak | grep "bootargs=" -A 20
Pay attention to the query we are using! We look for
bootargs= to avoid other matches with bootargs. You can search for another environment variable by replacing
bootargs with the variable name of your choice. We also specify that after finding the match, we want to print the next 20 lines on the screen via the option
-A N or
--after-context N, where N represents the number of lines we want to print. 20 is an approximate number that guarantees to consider all the environment variables specified on U-Boot.
The result is as follows:
bootargs=earlyprintk console=ttyS0,115200 rootwait nprofile_irq_duration=on root=ubi0:rootfs rootfstype=ubifs ubi.fm_autoconvert=1 init=/linuxrc bootcmd=nvt_boot bootdelay=0 baudrate=115200 ipaddr=192.168.1.99 serverip=192.168.1.11 gatewayip=192.168.1.254 netmask=255.255.255.0 hostname=soclnx arch=arm cpu=armv7 board=nvt-na51055 board_name=nvt-na51055 vendor=novatek soc=nvt-na51055_a32
Many interesting details can be found. Let’s focus on each of the environment variables:
bootargs― represents a set of arguments that are passed at the beginning to U-Boot and starts booting the operating system. The following flags have been specified:
earlyprintk― shows the first kernel boot messages (the first messages means the messages are printed before U-Boot loads the boot console ― Step 4)
console― specifies the devices used as screen/console. In this case, we find
ttyS0, which is the first serial UART port (COM1) and the second parameter indicates the speed. Serial ports use a variety of serial signals to talk to each other and are often used as control/configuration, diagnostic, or emergency maintenance consoles
rootwait― U-Boot waits until the device is ready (device is where U-Boot loads the operating system from ― be it a hard disk, SSD, or from the network)
nprofile_irq_duration― we could not find any documentation regarding this setting and assume it is related to interrupt handling
root― indicates the device from which to fetch the rootfs (i.e., the file system that will contain the operating system)
rootfstype― the type of file system that U-Boot will use (i.e., ubifs). We will discuss more about ubifs in the third part of this series
ubi.fm_autoconvert― is a flag that specifies the use of an experimental feature of ubifs called Fastmap that ensures minimal boot times
init― specifies the binary to boot first after loading the operating system into memory
bootcmd― specifies the command to execute. In this case,
bootdelay― number in seconds indicating how much time U-Boot has to wait before loading the kernel. This is useful in some embedded contexts, but most of the time it is set to 0
baudrate― indicates speed for serial ports
ipaddr― default IP address of the board, which is used to establish an IP connection with a remote machine and load an operating system over the network
serverip― IP address of a remote machine from which an operating system can be loaded
gatewayip― IP address that serves as a gateway
netmask― netmask used for the server
hostname― machine name. In this case,
arch― architecture. In this case,
cpu― type of cpu. In this case,
board_name― board name is
nvt-na51055(NVT stands for Novatek)
vendor― company that produced the
soc― model of the board (
We can also get the version of U-Boot by searching for the string
strings firmware_rlc_810_a.pak | grep "U-Boot" U-Boot 2019.04 (Oct 11 2021 - 12:40:43 +0800)
The software version is
2019.04 and was compiled in October 2021. It is not too old; although, it is always good to keep U-Boot updated. It can be hypothesized that Reolink and the team behind the firmware did not update U-Boot because of some deprecated component that they are using in their chain (or the usual “If it works, don’t touch it!” line of thinking).
Well, this wraps up the second part of this series. See you soon for the next article where we will explore Reolink’s hardware!
- Part 1 – Introduction to Firmware Analysis of a Reolink IP Camera
- Part 2 – Booting an Embedded OS: the Booting and U-Boot Phase
- Part 3 – Dissecting Reolink RLC-810A Hardware: A Detailed View
- Part 4 – Understanding the UBI File System in Embedded Devices
- Part 5 – Exploring the Operating System of Reolink RLC-810A
- Part 6 – Techniques for Setting up Peripherals via PIO and DMA
- Part 7 – Reverse Engineering the OMNIVISION OS12D40 Driver