The security of embedded devices has never been more critical. In a world
where attacks targeting IoT systems are becoming increasingly sophisticated,
ensuring the integrity of the boot process is a must. This is where Secure
Boot comes in—an essential technology that guarantees only authorized code can
execute on a device from the moment it starts. In this article, we will explore
the implementation of Secure Boot using AHAB, the solution provided by NXP to
secure the i.MX93 from its initial boot stages.
Why is Secure Boot crucial for your device?
A secure boot ensures that no malicious code interferes with the critical boot
process, protecting your device from attacks targeting the bootloader and early
boot stages. Furthermore, AHAB, integrated into i.MX93 processors, enables
advanced authentication right from the initial boot stages, ensuring that only
validated components can be loaded, thereby strengthening security from the
get-go.
Secure boot is a critical security feature that ensures only authenticated and
authorized code can run on a device. It operates through a chain of trust, where
each component verifies the integrity of the next element in the chain.
Several mechanisms must be used to authenticate each element of this chain, but
the mechanism for authenticating the first boot stages depends on the target SoC.
The i.MX93 series uses NXP's Advanced High Assurance Boot (AHAB) to secure the
first boot stages.
For subsequent stages, you can implement mechanisms such as:
- Using U-Boot's "verified boot" feature to sign the kernel,
- Using the default environment (cf. USE_DEFAULT_ENV_FILE), and restricting
write access to only a few environment variables (cf. ENV_WRITEABLE_LIST),
which are necessary for writable access, such as for OTA updates,
- Using DM-verity to authenticate the root filesystem,
- And finally, using OverlayFS combined with DM-crypt to mount encrypted,
writable subfolders.
Here, we'll focus on the first part of the secure boot process, using NXP's AHAB
to authenticate the bootloader on the NXP i.MX93 in single-boot mode. We will
also briefly discuss how to generate the keys to sign the bootloader and provide
an introduction to AHAB.
Note: AHAB also provides a complementary encryption feature designed to protect
the confidentiality and integrity of data, whereas secure boot focuses on
verifying the integrity and authenticity of the boot process. This post will not
cover encryption in detail.
AHAB Architecture
The AHAB authentication mechanism is based on public key cryptography using
asymmetric keys.
On the i.MX93, AHAB support is provided by a security co-processor, the EdgeLock
enclave (ELE), which handles the authentication of binaries signed with one or
more private keys. This co-processor contains fuses that must be burned with the
hash of the public keys.
AHAB Containers
Since multiple boot stages (e.g., TF-A, OP-TEE, U-Boot, etc.) and firmwares are
required to boot i.MX93 platforms, these binaries are packed into containers
using the imx-mkimage tool:
bl31.bin
lpddr4_dmem_1d_v202201.bin
lpddr4_dmem_2d_v202201.bin
lpddr4_imem_1d_v202201.bin
lpddr4_imem_2d_v202201.bin
mx93a1-ahab-container.img
tee.bin
u-boot.bin
u-boot-spl.bin
In i.MX93 single-boot mode, the bootloader image contains at least three
containers:
- mx93a1-ahab-container.img: Contains the ELE Firmware.
- u-boot-atf-container.img: Contains at least the SPL.
- flash.bin: Contains TF-A, OP-TEE, and U-Boot.
*start ----> +---------------------------+ ---------
| 1st Container header | ^
| and signature | |
+---------------------------+ |
| Padding for 1kB alignment | |
*start + 0x400 ----> +---------------------------+ |
| 2nd Container header | |
| and signature | |
+---------------------------+ |
| Padding | | Authenticated at
+---------------------------+ | ELE ROM/FW Level
| ELE FW | |
+---------------------------+ |
| Padding | |
+---------------------------+ |
| Cortex-M Image | |
+---------------------------+ |
| SPL Image | v
+---------------------------+ ---------
| 3rd Container header | ^
| and signature | |
+---------------------------+ |
| Padding | | Authenticated
+---------------------------+ | at SPL Level
| TF-A | |
+---------------------------+ |
| OP-TEE | |
+---------------------------+ |
| U-Boot | v
+---------------------------+ ---------
These containers are signed offline using NXP Code-Signing Tools (CST), which
also allow the creation of an OEM private key infrastructure (PKI) and the
generation of the associated public keys (SRK) table, which is burned into the
fuses. The CST can also be used with the PKCS#11 standard to access
cryptographic services from tokens or devices such as HSM, TPM, and smart cards.
The first container is signed with NXP keys and is authenticated by the ELE ROM,
while the other containers are signed with OEM keys.
AHAB Boot Flow
In single boot mode, the Cortex-A55 ROM reads data from the selected boot
device, loading all containers in the chosen boot image set one by one. All
images within each container (e.g., EdgeLock secure enclave firmware, Cortex-M33
firmware, A55 firmware, OP-TEE, and U-Boot) are loaded, and the EdgeLock secure
enclave (ELE) is tasked with authenticating them. The ELE firmware is
authenticated by the ELE ROM, and images in the second container are verified by
the ELE firmware.
If the bootloader image contains more than two containers, the third and
subsequent containers are authenticated by the SPL instead of the ELE.
PKI Generation
To authenticate the bootloader, we need to generate keys. These keys can be
created with the CST. The private key will be used to sign the bootloader, and
the public key will be burned into the i.MX93 fuses to authenticate the
bootloader during boot.
Follow these steps to generate the keys:
cd cst-3.4.1/keys
echo 00000001 > serial
Write the passphrase for the certificate (replace "fooahabcert" with your
choice) in two lines, separated by \n. It is important to store this
passphrase securely with backups:
echo -e "fooahabcert\nfooahabcert" > key_pass.txt
Generate a P384 ECC PKI tree with a subordinate SGK key on CST:
./ahab_pki_tree.sh
[...]
Do you want to use an existing CA key (y/n)?: n
Key type options (confirm targeted device supports desired key type):
Select the key type (possible values: rsa, rsa-pss, ecc)?: ecc
Enter length for elliptic curve to be used for PKI tree:
Possible values p256, p384, p521: p384
Enter the digest algorithm to use: sha384
Enter PKI tree duration (years): 10
Do you want the SRK certificates to have the CA flag set? (y/n)?: n
Generate the Signing Root Keys (SRK) Table and SRK Hash for 64-bit Linux machines:
cd ../crts/
../linux64/bin/srktool -a -d sha256 -s sha384 -t SRK_1_2_3_4_table.bin \
-e SRK_1_2_3_4_fuse.bin -f 1 -c \
SRK1_sha384_secp384r1_v3_usr_crt.pem,\
SRK2_sha384_secp384r1_v3_usr_crt.pem,\
SRK3_sha384_secp384r1_v3_usr_crt.pem,\
SRK4_sha384_secp384r1_v3_usr_crt.pem
Do not enter spaces between the commas when specifying the SRKs in the "-c" or
"--certs" option. Otherwise, the certificates specified after the first space
will be excluded from the table.
Regenerate the SRK HASH (SRK_1_2_3_4_fuse.bin) using SHA256 with the
SRK_1_2_3_4_table.bin:
openssl dgst -binary -sha256 SRK_1_2_3_4_table.bin
Optionally, verify that the sha256sum of SRK_1_2_3_4_table matches the SRK_1_2_3_4_fuse.bin:
od -t x4 SRK_1_2_3_4_fuse.bin
0000000 29eec727 eaed9aa7 c7e53bc0 36835f78
0000020 6901bc47 b244753c f78d3162 27ae36b9
0000040
Bootloader Signature
The CST uses CSF description files to sign (and encrypt) containers generated by
imx-mkimage with OEM keys. When imx-mkimage generates containers, it also
specifies the block offsets to be used in the CSF description files. For
example, imx-mkimage returns the following values for your bootloader:
CST: CONTAINER 0 offset: 0x0
CST: CONTAINER 0: Signature Block: offset is at 0x190
CST: CONTAINER 0 offset: 0x400
CST: CONTAINER 0: Signature Block: offset is at 0x490
Where 0x190 is the block offset for the second container header and 0x490 is
the block offset for the third container header.
The CSF description file used to sign a container contains three sections:
- [Header]: Information about the HAB version to use for signing.
- [Authenticate Data]: Information about the key used to sign.
- [Install SRK]: Information about the container being signed.
The following CSF description files were used to sign the
u-boot-atf-container.img in our example:
[Header]
Target = AHAB
Version = 1.0
[Install SRK]
# SRK table generated by srktool
File = "SRK_1_2_3_4_table.bin"
# Public key certificate in PEM format
Source = "SRK1_sha384_secp384r1_v3_usr_crt.pem"
# Index of the public key certificate within the SRK table (0 .. 3)
Source index = 0
# Type of SRK set (NXP or OEM)
Source set = OEM
# bitmask of the revoked SRKs
Revocations = 0x0
[Authenticate Data]
# Binary to be signed generated by mkimage
File = "u-boot-atf-container.img"
# Offsets = Container header Signature block (printed out by mkimage)
Offsets = 0x0 0x190
The following CSF description files were used to sign flash.bin in our
example:
[Header]
Target = AHAB
Version = 1.0
[Install SRK]
# SRK table generated by srktool
File = "SRK_1_2_3_4_table.bin"
# Public key certificate in PEM format
Source = "SRK1_sha384_secp384r1_v3_usr_crt.pem"
# Index of the public key certificate within the SRK table (0 .. 3)
Source index = 0
# Type of SRK set (NXP or OEM)
Source set = OEM
# bitmask of the revoked SRKs
Revocations = 0x0
[Authenticate Data]
# Binary to be signed generated by mkimage
File = "flash.bin"
# Offsets = Container header Signature block (printed out by mkimage)
Offsets = 0x400 0x490
The first step is to generate a u-boot-atf-container.img, then copy the block
offsets into the CSF description file to sign it:
make SOC=iMX9 REV=A1 dtbs=imx93-11x11-evk.dtb u-boot-atf-container.img
Next, sign it with the following command and replace the unsigned version:
cst -i u-boot-atf-container.img.csf -o u-boot-atf-container.img.signed
mv u-boot-atf-container.img.signed u-boot-atf-container.img
Then generate a flash.bin containing the signed u-boot-atf-container.img:
make SOC=iMX9 REV=A1 V2X=NO dtbs=imx93-11x11-evk.dtb flash_singleboot
Finally, sign the resulting flash.bin:
cst -i flash.bin.csf -o flash.bin.signed
Burn Fuses
Once the signed flash.bin is flashed, you need to burn the public keys used to
sign the bootloader into the i.MX93 fuses to finalize AHAB secure boot. This
requires using a U-Boot that provides AHAB functionalities, such as checking ELE
events during bootloader authentication and securing the device.
Program SRK
The following commands enable AHAB secure boot by programming the
SRK_HASH[255:0] fuses on i.MX93, ensuring that only bootloaders signed with
keys matching the SRK hash programmed into the fuses will be accepted:
fuse prog -y 16 0 0x29eec727
fuse prog -y 16 1 0xeaed9aa7
fuse prog -y 16 2 0xc7e53bc0
fuse prog -y 16 3 0x36835f78
fuse prog -y 16 4 0x6901bc47
fuse prog -y 16 5 0xb244753c
fuse prog -y 16 6 0xf78d3162
fuse prog -y 16 7 0x27ae36b9
Close the Device
Once the SRK fuses are programmed, you can "close" the device to allow only the
bootloader signed with keys matching the SRK table to boot:
Before closing the device, you can verify that the fuses have been written
correctly by checking that no ELE events are raised:
ahab_status
Lifecycle: 0x00000008, OEM Open
No Events Found!
=>
Lifecycle: 0x00000008, OEM Open
No Events Found!
Once the device is closed, the ahab_status command will show OEM closed:
ahab_status
Lifecycle: 0x00000020, OEM closed
No Events Found!
=>
Lifecycle: 0x00000020, OEM closed
No Events Found!
As long as OEM Open appears in the status, the device is not secured and can still
execute unsigned bootloaders or those signed with invalid keys.
Conclusion
By implementing AHAB on the i.MX93 platform, you can ensure that your boot
process is protected from unauthorized code. The use of public key cryptography
and secure containers adds an extra layer of security, making your device more
resilient to attacks. This process is crucial for applications where integrity
and authenticity from the very first boot stage are paramount.
Introduction
The goal of the Zephyr project, hosted by the Linux foundation, since 2016, is to provide a safe and secured real time operating system (RTOS) for connected devices that are too small for Linux, or for core companion, through the Apache 2.0 open source license.
It is designed for resource-constrained devices such as microcontrollers and Internet of Things (IoT) devices, to be modular and scalable. This makes it ideal for a wide range of devices, from simple sensors to complex systems. The operating system is written in C and is fully compatible with the C11 and C++17 standards.
One of the key benefits of the Zephyr device model is its small footprint, it can be configured to run on devices with as little as 10 KB of memory.
It supports multiple 32 bits and 64 bits architectures: Cortex-A, Cortex-M, Cortex-R, RISC-V, x86-64, etc.
But it also support several boards and extensions: Feather, nRF52840, ST Discovery, ST Nucleo, ESP-32, etc.
It is able to manage several kinds of connectivity: Bluetooth, ethernet, wifi, LoRa.
And it support some network protocols: IPv4, IPv6,UDP, TCP, CoAP, LWM2M, MQTT, DNS, etc.
As Linux, Zephyr use Kconfig, and its device model is mainly based on device tree.
Device tree
Device trees are tree data structures that describe the hardware components and their relationships in a system.
They are stored in a text file, named device tree sources (*.dts), and they written by developers to describe hardware architectures of SoCs and boards.
And they are used by the operating system to determine how to initialize and interact with the hardware.
Each node describe a device of the system, has its own properties that describe their characteristics, and they have only one parent (except for the root node).
Each device driver is associated with a specific device tree node, which represents a hardware component in the system. The device driver provides the necessary code and data to control the behavior of the hardware component.
test_i2c_bme280: bme280@6 {
compatible = "bosch,bme280";
reg = <0x6>;
};
In the Linux kernel, device tree sources are compiled to device tree binaries (dtb) that are parsed, at boot, by bootloader stages (U-Boot, TF-A...) and the kernel to allow support several hardware configuration with same binaries.
But in Zephyr, device tree sources are transformed to a "devicetree_generated.h" C header file at build, that contains macro definitions and data structures allowing device drivers to access information about the hardware components in the system, such as the memory mapping of a device, its pin assignments, and its IRQ numbers:
#define DT_COMPAT_HAS_OKAY_bosch_bme280 1
#define DT_N_INST_bosch_bme280_NUM_OKAY 1
#define DT_FOREACH_OKAY_bosch_bme280(fn) fn(DT_N_S_soc_S_i2c_40005400_S_bme280_77)
#define DT_FOREACH_OKAY_VARGS_bosch_bme280(fn, ...) fn(DT_N_S_soc_S_i2c_40005400_S_bme280_77, __VA_ARGS__)
#define DT_FOREACH_OKAY_INST_bosch_bme280(fn) fn(0)
#define DT_FOREACH_OKAY_INST_VARGS_bosch_bme280(fn, ...) fn(0, __VA_ARGS__)
#define DT_COMPAT_bosch_bme280_BUS_i2c 1
Where:
- DT_COMPAT_HAS_OKAY_bosch_bme280: indicates that there is at least one instance of BME280
- DT_N_INST_bosch_bme280_NUM_OKAY: defines the number of BME280 instances that are marked okay
- DT_FOREACH_OKAY_bosch_bme280: allows you to apply a function fn to each instance of the BME280
- DT_FOREACH_OKAY_VARGS_bosch_bme280: also allows you to apply a function fn to each instance of the BME280, but with additional arguments
- DT_FOREACH_OKAY_INST_bosch_bme280: allows you to apply a function fn to each instance of the BME280, passing the instance number as an argument
- DT_FOREACH_OKAY_INST_VARGS_bosch_bme280: is similar to the previous macro, but this one allows for additional arguments
- DT_COMPAT_bosch_bme280_BUS_i2c: indicates that the BME280 device is connected to an I2C bus.
- DT_N_S_soc_S_i2c_40005400_S_bme280_77: refers to a specific node in the device tree, here it refers to the BME280 sensor connected to the I2C controller with the base address 0x40005400 within the SoC. The sensor's address on this I2C bus is 0x77.
In addition, device tree sources can be extended or overridden, for example to connect additional devices to a board, or to disable board devices which will not be used:
/ {
aliases {
bme280 = &bme280;
};
};
&spi1 {
status = "disabled";
};
&i2c1 {
status = "okay";
bme280: bme280@77 {
compatible = "bosch,bme280";
reg = <0x77>;
};
};
Binding
Content of device tree sources is described in binding files, that are written in human readable and easy to parse YAML.
Binding files can be also used to validate device tree sources by comparing the information in the YAML file with the information in the device tree sources.
description: BME280 integrated environmental sensor
compatible: "bosch,bme280"
include: [sensor-device.yaml, i2c-device.yaml]
Device driver
In Zephyr, a device driver can access the properties of an associated node in the device tree using the macro that are defined in C header files.
For example, the following code can be used to initialize a BME280 sensor using properties defined in the device tree:
#include <device.h>
#include <drivers/i2c.h>
#include <devicetree.h>
#include <zephyr.h>
// Define the node identifier for the BME280 sensor
#define BME280_NODE DT_N_S_soc_S_i2c_40005400_S_bme280_77
// Function to initialize the BME280 sensor
static int bme280_init(const struct device *dev)
{
// Check if the node is available
if (!device_is_ready(dev)) {
printk("Device %s is not ready\n", dev->name);
return -ENODEV;
}
// Retrieve the I2C device associated with the BME280 node
const struct device *i2c_dev = DEVICE_DT_GET(DT_BUS(BME280_NODE));
if (!device_is_ready(i2c_dev)) {
printk("I2C device not ready\n");
return -ENODEV;
}
// Write some initialization code here, such as configuring registers
printk("BME280 sensor initialized\n");
return 0;
}
// Initialize the BME280 sensor at boot time
SYS_INIT(bme280_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
Conclusion
Those who have already implemented BSP or driver on Linux shouldn't encounter too much difficulty, but on the other hand, the step is a little higher for people coming from the world of micro-controllers.
Introduction
Since some years, I haven't built an embedded Linux without using a framework, like Open Embedded from the Yocto
project.
Then here, I wanted to make a guide to help you to build quickly, from "scratch" a very minimal embedded Linux to boot a
target.
The following examples have been written to boot a virtual Qemu target but, they can be adapted to boot a real target.
Moreover, the build environment will be bootstrapped with a prebuilt cross-toolchain, I have chosen to use one provided
by Bootlin and using glibc.
Setup the environment
First, it is required to install the packages that are needed to install and use the cross-toolchain but also to compile the host tools and to provide Qemu:
- The Ncurses libraries are only required to execute the command make menuconfig.
- The certificates and wget will be used to download the prebuilt toolchain.
- In the same way, git will be used to checkout the source of Busybox and Linux.
- The Qemu packages will be used to emulate system platform and to execute static binaries cross-compiled for aarch64 on the x86-64 host.
apt update
apt install -y --no-install-recommends \
bc \
build-essential \
ca-certificates \
cpio \
file \
flex \
git \
ipxe-qemu \
libncurses5-dev \
libncursesw5-dev \
libssl-dev \
qemu \
qemu-system-aarch64 \
qemu-user-static \
wget
Now, it is time to download and install the prebuilt toolchain:
mkdir ~/src
cd ~/src
wget https://toolchains.bootlin.com/downloads/releases/toolchains/aarch64/tarballs/aarch64--glibc--stable-2020.08-1.tar.bz2
tar xvjf aarch64--glibc--stable-2020.08-1.tar.bz2
Once the toolchain has been extracted you have to set the required environment variables to cross-compile binaries:
- PATH: It shall be extended so that the cross-tools from the cross-toolchain will be available from the environment
- CROSS_COMPILE: In order to clarify the prefix used by the cross-tools
- ARCH: The architecture of the target platform
ls ~/src/aarch64--glibc--stable-2020.08-1/bin/*gcc
~/src/aarch64--glibc--stable-2020.08-1/bin/aarch64-linux-gcc
export PATH=~/src/aarch64--glibc--stable-2020.08-1/bin:$PATH
export CROSS_COMPILE=aarch64-linux-
Now, it is possible to call the cross-tools from the shell:
aarch64-linux-gcc -v
Using built-in specs.
COLLECT_GCC=~/src/aarch64--glibc--stable-2020.08-1/bin/aarch64-linux-gcc.br_real
COLLECT_LTO_WRAPPER=~/src/aarch64--glibc--stable-2020.08-1/bin/../libexec/gcc/aarch64-buildroot-linux-gnu/9.3.0/lto-wrapper
Target: aarch64-buildroot-linux-gnu
<...>
Thread model: posix
gcc version 9.3.0 (Buildroot 2020.08-14-ge5a2a90)
Concerning the variable PATH this one will be set afterwards because its value depends on the binary that will be built.
Build the Linux kernel
So, the environment is ready to pull the sources of the latest stable branch of the kernel Linux and to build them:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux
git checkout -b local/linux-5.4.y origin/linux-5.4.y
# git show HEAD
export ARCH=arm64
make defconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTLD scripts/kconfig/conf
*** Default configuration is based on 'defconfig'
#
# configuration written to .config
#
# make menuconfig
make -j$(nproc)
<...>
AR drivers/net/ethernet/built-in.a
AR drivers/net/built-in.a
AR drivers/built-in.a
GEN .version
CHK include/generated/compile.h
LD vmlinux.o
MODPOST vmlinux.o
MODINFO modules.builtin.modinfo
LD .tmp_vmlinux.kallsyms1
KSYM .tmp_vmlinux.kallsyms1.o
LD .tmp_vmlinux.kallsyms2
KSYM .tmp_vmlinux.kallsyms2.o
LD vmlinux
SORTEX vmlinux
SYSMAP System.map
Building modules, stage 2.
MODPOST 531 modules
OBJCOPY arch/arm64/boot/Image
GZIP arch/arm64/boot/Image.gz
The command make defconfig will apply the default configuration for the target platform (cf. ARCH=arm64), and the
compilation will be performed by make -j$(nproc).
The commands git show HEAD and make defconfig are optional:
- the first is useful to verify that the latest commit corresponding to the latest tag of the branch linux-5.4.y.
- the second can be used if you want to customize the kernel configuration.
NB. The kernel Linux but also Busybox and some projects use Kbuild to manage the build options
Populate the sysroot
The easy way to bootstrap a sysroot is to use Busybox that has been created to offer common UNIX tools into a single
executable and it is size-optimized. To create a sysroot, it is only required to add a few configuration files.
The steps to pull and build Busybox are similar to those of the kernel Linux.
git clone git://git.busybox.net/busybox
cd busybox
git checkout -b local/1_32_stable origin/1_32_stable
# git show HEAD
export ARCH=aarch64
export LDFLAGS="--static"
make defconfig
# make menuconfig
make -j$(nproc)
make install
Here, the LDFLAGS is set to force static linking of Busybox quickly, but it is also possible to use
make menuconfig to set CONFIG_STATIC=y. The advantage of the static executable is that it can be tested with Qemu:
qemu-aarch64-static busybox echo "Hello!"
Hello!
qemu-aarch64-static busybox date
Sat Jun 27 15:06:41 UTC 2020
The binary qemu-aarch64-static allows to execute a binary built for another architecture on the host computer, for
example here it allows to execute the Busybox binary compiled for an aarch64 target on a x86-64 host.
The last command make install created a tree into the _install directory that can be used to populate the sysroot:
ls -l _install
total 4
drwxr-xr-x. 1 tperrot tperrot 974 Nov 30 15:22 bin
lrwxrwxrwx. 1 tperrot tperrot 11 Nov 30 15:22 linuxrc -> bin/busybox
drwxr-xr-x. 1 tperrot tperrot 986 Nov 30 15:22 sbin
drwxr-xr-x. 1 tperrot tperrot 14 Nov 30 15:22 usr
ls -l _install/bin
<...>
lrwxrwxrwx. 1 tperrot tperrot 7 Nov 30 15:22 umount -> busybox
lrwxrwxrwx. 1 tperrot tperrot 7 Nov 30 15:22 uname -> busybox
lrwxrwxrwx. 1 tperrot tperrot 7 Nov 30 15:22 usleep -> busybox
lrwxrwxrwx. 1 tperrot tperrot 7 Nov 30 15:22 vi -> busybox
lrwxrwxrwx. 1 tperrot tperrot 7 Nov 30 15:22 watch -> busybox
lrwxrwxrwx. 1 tperrot tperrot 7 Nov 30 15:22 zcat -> busybox
In order, to finalize this minimal sysroot, it is required to create a rcS init script:
mkdir _install/proc _install/sys _install/dev _install/etc _install/etc/init.d
cat > _install/etc/init.d/rcS << EOF
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s
[ ! -h /etc/mtab ] && ln -s /proc/mounts /etc/mtab
[ ! -f /etc/resolv.conf ] && cat /proc/net/pnp > /etc/resolv.conf
EOF
chmod +x _install/etc/init.d/rcS
Build the filesystem
The target of this step is to package the sysroot tree into a filesystem that can be mounted by the kernel.
There is two available possibilities, either build a ramfs or a rootfs.
Globally, the difference between both is that:
- the ramfs is a very simple filesystem that can be used by the kernel to create a block device into the RAM space from an archive.
- the rootfs is a filesystem mounted from a non volatile device by the kernel.
For more information about the difference between the ramfs and the rootfs, you can you refer to the kernel documentation.
Build a ramfs
To build the ramfs we will use cpio and gzip to construct the compressed archive after modifying the rights:
mkdir _rootfs
rsync -a _install/ _rootfs
chown -R root:root _rootfs
cd _rootfs
find . | cpio -o --format=newc > ../rootfs.cpio
cd ..
gzip -c rootfs.cpio > rootfs.cpio.gz
Build a rootfs
To build the rootfs, the first step is to create an empty binary blob that will be mounted into a loop device to be
formatted to create a ext3 filesystem. Then the tree can be copied and the rights updated.
dd if=/dev/zero of=rootfs.img bs=1M count=10
mke2fs -j rootfs.img
mkdir _rootfs
mount -o loop rootfs.img _rootfs
rsync -a _install/ _rootfs
chown -R root:root _rootfs
sync
umount _rootfs
Boot the target
Following, the qemu commands to boot the minimal embedded Linux system that has been built.
# With the ramfs
qemu-system-aarch64 -nographic -no-reboot -machine virt -cpu cortex-a57 -smp 2 -m 256 \
-kernel ~/src/linux/arch/arm64/boot/Image \
-initrd ~/src/busybox/rootfs.cpio.gz \
-append "panic=5 ro ip=dhcp root=/dev/ram rdinit=/sbin/init"
# With the rootfs
qemu-system-aarch64 -nographic -no-reboot -machine virt -cpu cortex-a57 -smp 2 -m 256 \
-kernel ~/src/linux/arch/arm64/boot/Image \
-append "panic=5 ro ip=dhcp root=/dev/vda" \
-drive file=~/src/busybox/rootfs.img,format=raw,if=none,id=hd0 -device virtio-blk-device,drive=hd0
Then the target will be boot to shell, "It's alive!":
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[ 0.000000] Linux version 5.10.0-rc5 (tperrot@27ea4a863f61) (aarch64-linux-gcc.br_real (Buildroot 2020.08-14-ge5a2a90) 9.3.0, GNU ld (GNU Binutils) 2.33.1) #1 SMP PREEMPT Mon Nov 30 14:40:05 UTC 2020
[ 0.000000] Machine model: linux,dummy-virt
<...>
[ 0.858346] Sending DHCP requests ., OK
[ 0.870558] IP-Config: Got DHCP answer from 10.0.2.2, my address is 10.0.2.15
[ 0.870909] IP-Config: Complete:
[ 0.871199] device=eth0, hwaddr=52:54:00:12:34:56, ipaddr=10.0.2.15, mask=255.255.255.0, gw=10.0.2.2
[ 0.871566] host=10.0.2.15, domain=, nis-domain=(none)
[ 0.871825] bootserver=10.0.2.2, rootserver=10.0.2.2, rootpath=
[ 0.871866] nameserver0=10.0.2.3
[ 0.872389]
[ 0.875863] ALSA device list:
[ 0.876151] No soundcards found.
[ 0.879353] uart-pl011 9000000.pl011: no DMA platform data
[ 0.920237] Freeing unused kernel memory: 5952K
[ 0.921223] Run /sbin/init as init process
Please press Enter to activate this console.