You’re probably familiar with the steps required to boot Linux from U-Boot: you first load several binaries into memory, perhaps a device tree, a kernel, maybe even an initrd. You then invoke a command such as bootm or booti with arguments providing memory addresses for the binaries you’ve just loaded. However there is a much better way – in this post we’re going to explore Flattened Image Tree (FIT) Images and show you the benefits of using them.
Let’s start with the typical steps required to TFTP boot Linux on a Nitrogen8M i.MX8M reference board with an initrd.
=> tftp 0x40480000 Image
=> tftp 0x43000000 imx8mq-nitrogen8m.dtb
=> tftp 0x44000000 core-image-minimal-nitrogen8m.ext2.gz.u-boot
=> booti 0x40480000 0x44000000 0x43000000
With this approach you have to explicitly determine a load address for each component and ensure the binaries don’t overlap in memory. You’ll also need to make similar considerations if you have an update mechanism or store the binaries in raw flash. It’s a little inconvenient and easy to make a mistake.
Fortunately, the U-Boot community extended their uImage format (typically used for wrapping kernels and ram disks with a header so that U-Boot knew what to do with it) so that it could support multiple binaries (IH_TYPE_MULTI) in a single uImage. Let’s use the mkimage utility to create an multi-file uImage:
$ mkimage -C none -A arm64 -O linux -T multi -a 0x40480000 -e 0x40480000 -d Image:core-image-minimal-nitrogen8m.ext2.gz.u-boot:imx8mq-nitrogen8m.dtb mImage
Image Name:
Created: Sat Mar 6 22:04:41 2021
Image Type: AArch64 Linux Multi-File Image (uncompressed)
Data Size: 33569915 Bytes = 32783.12 KiB = 32.01 MiB
Load Address: 40480000
Entry Point: 40480000
Contents:
Image 0: 21600768 Bytes = 21094.50 KiB = 20.60 MiB
Image 1: 11912092 Bytes = 11632.90 KiB = 11.36 MiB
Image 2: 57039 Bytes = 55.70 KiB = 0.05 MiB
Notice how we provided multiple binaries separated by colons in the ‘-d’ argument and set the image type as ‘multi’. Let’s boot it:
=> tftp 0x43000004 mImage ; bootm 0x43000004 [160/46625]
Using FEC device
TFTP from server 192.168.1.198; our IP address is 192.168.1.224
Filename 'mImage'.
Load address: 0x43000004
Loading: #################################################################
9.7 MiB/s
done
Bytes transferred = 33569979 (2003cbb hex)
## Booting kernel from Legacy Image at 43000004 ...
Image Name:
Image Type: AArch64 Linux Multi-File Image (uncompressed)
Data Size: 33569915 Bytes = 32 MiB
Load Address: 40480000
Entry Point: 40480000
Contents:
Image 0: 21600768 Bytes = 20.6 MiB
Image 1: 11912092 Bytes = 11.4 MiB
Image 2: 57039 Bytes = 55.7 KiB
Verifying Checksum ... OK
## Loading init Ramdisk from multi component Legacy Image at 43000004 ...
## Flattened Device Tree from multi component Image at 43000004
Booting using the fdt at 0x0000000044ff5df0
Loading Multi-File Image
ERROR: reserving fdt memory region failed (addr=b8000000 size=400000)
Using Device Tree in place at 0000000044ff5df0, end 0000000045006cbe
Starting kernel ...
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Linux version 4.14.98-2.0.0-ga+yocto+gd18974845042 (oe-user@oe-host) (gcc version 8.3.0 (GCC)) #1 S
MP PREEMPT Fri Mar 5 17:26:18 UTC 2021
We now have a single multi-file uImage encapsulating all of our boot binaries. One additional benefit to this approach is that there is now checksum validation for our entire uImage.
Unfortunately this approach doesn’t quite provide the flexibility we need. A requirement of ARM64 platforms is that the device tree must be located on an 8 byte boundary – unfortunately IH_TYPE_MULTI images don’t provide any support for specifying a load address or alignment constraints. In fact, our boot was only successful because we tweaked the address of where we TFTP’d the uImage to until the device tree address inside it lined up. This is an example of the lack of flexibility offered by IH_TYPE_MULTI images which resulted in the Flattened uImage Tree (FIT) format being born.
Rather than reinvent the wheel the FIT format makes use of the libfdt library and the device tree compiler (dtc) – essentially a FIT image is a device tree blob (DTB) with your binaries embedded inside – but represented in a structure that allows you to provide additional information. To create a FIT image you create an ‘Image Tree Source’ file (akin to a DTS file) – and pass this to the mkimage command which internally uses the dtc to construct a FIT image (akin to a DTB). Let’s try this out by creating an image tree source file (.its):
/dts-v1/;
/ {
description = "Nitrogen8M i.MX8M FIT Image";
#address-cells = <1>;
images {
kernel {
description = "Kernel";
data = /incbin/("Image");
type = "kernel";
arch = "arm64";
os = "linux";
compression = "none";
load = <0x40480000>;
entry = <0x40480000>;
hash {
algo = "sha1";
};
};
fdt {
description = "DTB";
data = /incbin/("imx8mq-nitrogen8m.dtb");
type = "flat_dt";
arch = "arm64";
compression = "none";
load = <0x43000000>;
entry = <0x43000000>;
hash {
algo = "sha1";
};
};
initrd {
description = "Initrd";
data = /incbin/("core-image-minimal-nitrogen8m.ext2.gz");
type = "ramdisk";
arch = "arm64";
os = "linux";
compression = "none";
hash {
algo = "sha1";
};
};
};
configurations {
default = "standard";
standard {
description = "Standard Boot";
kernel = "kernel";
fdt = "fdt";
ramdisk = "initrd";
hash {
algo = "sha1";
};
};
};
};
As you can see we use a tree-like structure to describe the binaries that are to be contained in our FIT image. The sub-nodes of the ‘images’ node describes the individual binaries and make use of the /incbin/ directive to include those binaries. Each of the ‘images’ sub-nodes have some common properties including a type (e.g. kernel, flat_dt, ramdisk), information on the compression used (so that U-Boot can decompress) and hash information (such that U-Boot can perform verification). You can find more information about the supported properties and nodes here. You can see that we’ve added a load/entry address to the device tree sub-image to ensure that it is aligned correctly. There is also a ‘configurations’ node which we’ll come onto shortly – however let’s create a FIT image from this as follows:
$ mkimage -f nitrogen8M.its nitrogen8M.itb
FIT description: Nitrogen8M i.MX8M FIT Image
Created: Wed Mar 10 00:36:52 2021
Image 0 (kernel)
Description: Kernel
Created: Wed Mar 10 00:36:52 2021
Type: Kernel Image
Compression: uncompressed
Data Size: 21600768 Bytes = 21094.50 KiB = 20.60 MiB
Architecture: AArch64
OS: Linux
Load Address: 0x40480000
Entry Point: 0x40480000
Hash algo: sha1
Hash value: f6bfbebc237be13320a205a74db7d7c5ceca6823
Image 1 (fdt)
Description: DTB
Created: Wed Mar 10 00:36:52 2021
Type: Flat Device Tree
Compression: uncompressed
Data Size: 57039 Bytes = 55.70 KiB = 0.05 MiB
Architecture: AArch64
Load Address: 0x43000000
Hash algo: sha1
Hash value: 6ab42f1887c609b9a90bfab77718cc07c1d831b0
Image 2 (initrd)
Description: Initrd
Created: Wed Mar 10 00:36:52 2021
Type: RAMDisk Image
Compression: uncompressed
Data Size: 11912441 Bytes = 11633.24 KiB = 11.36 MiB
Architecture: AArch64
OS: Linux
Load Address: unavailable
Entry Point: unavailable
Hash algo: sha1
Hash value: e92aea4e83d07f1d0e6e03b485f89088d9a98e99
Default Configuration: 'standard'
Configuration 0 (normal)
Description: Standard Boot
Kernel: kernel
Init Ramdisk: initrd
FDT: fdt
Hash algo: sha1
Hash value: unavailable
We now have an ‘itb’ file (also know as a FIT image) which we can boot on our board – let’s try it:
=> tftp 0x48000000 nitrogen8M.itb
Using FEC device
TFTP from server 192.168.1.198; our IP address is 192.168.1.224
Filename 'out.itb'.
Load address: 0x48000000
Loading: ############################################################
10.5 MiB/s
done
Bytes transferred = 33572236 (200458c hex)
=> bootm 0x48000000
## Loading kernel from FIT Image at 48000000 ...
Using 'standard' configuration
Verifying Hash Integrity ... OK
Trying 'kernel' kernel subimage
Description: Kernel
Type: Kernel Image
Compression: uncompressed
Data Start: 0x480000c0
Data Size: 21600768 Bytes = 20.6 MiB
Architecture: AArch64
OS: Linux
Load Address: 0x40480000
Entry Point: 0x40480000
Hash algo: sha1
Hash value: f6bfbebc237be13320a205a74db7d7c5ceca6823
Verifying Hash Integrity ... sha1+ OK
## Loading ramdisk from FIT Image at 48000000 ...
Using 'standard' configuration
Verifying Hash Integrity ... OK
Trying 'initrd' ramdisk subimage
Description: Initrd
Type: RAMDisk Image
Compression: uncompressed
Data Start: 0x494a7b3c
Data Size: 11912441 Bytes = 11.4 MiB
Architecture: AArch64
OS: Linux
Load Address: unavailable
Entry Point: unavailable
Hash algo: sha1
Hash value: e92aea4e83d07f1d0e6e03b485f89088d9a98e99
Verifying Hash Integrity ... sha1+ OK
## Loading fdt from FIT Image at 48000000 ...
Using 'standard' configuration
Verifying Hash Integrity ... OK
Trying 'fdt' fdt subimage
Description: DTB
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x49499b9c
Data Size: 57039 Bytes = 55.7 KiB
Architecture: AArch64
Load Address: 0x43000000
Hash algo: sha1
Hash value: 6ab42f1887c609b9a90bfab77718cc07c1d831b0
Verifying Hash Integrity ... sha1+ OK
Loading fdt from 0x49499b9c to 0x43000000
Booting using the fdt blob at 0x43000000
Loading Kernel Image
ERROR: reserving fdt memory region failed (addr=b8000000 size=400000)
Using Device Tree in place at 0000000043000000, end 0000000043010ece
Starting kernel ...
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Linux version 4.14.98-2.0.0-ga+yocto+gd18974845042 (oe-user@oe-host) (gcc version 8.3.0 (GCC)) #1 SMP PREEMPT Fri Mar 5 17:26:18 UTC 2021
[ 0.000000] Boot CPU: AArch64 Processor [410fd034]
[ 0.000000] Machine model: Boundary Devices i.MX8MQ Nitrogen8M
As you can see we were able to construct a single image that contained all of our binaries which is booted with a simple command.
Let’s take another look at that ‘configurations’ node:
configurations {
default = "standard";
standard {
description = "Standard Boot";
kernel = "kernel";
fdt = "fdt";
ramdisk = "initrd";
hash {
algo = "sha1";
};
};
};
The FIT image can contain lots of binaries, more binaries than are needed for a single boot – thus this required ‘configurations’ node lets you group related binaries together. In our case we’ve created a configuration called ‘standard’ and indicated that it consists of a kernel, device tree and initrd. It’s possible to create multiple configurations and to choose which configuration to use at boot time. This is where we begin to see the power that FIT images provide.
Let’s see what happens if we add an additional sub-node of type ‘ramdisk’ named ‘initrdRecovery’ and add an additional configuration named ‘recovery’ that uses this initrd instead of the one used earlier. Let’s tftp this new FIT image and run the U-Boot utility ‘iminfo‘ to see what this FIT image contains:
=> tftp 0x48000000 nitrogen8M.itb
=> iminfo 0x48000000
## Checking Image at 48000000 ...
FIT image found
FIT description: Nitrogen8M i.MX8M FIT Image
Image 0 (kernel)
Description: Kernel
Type: Kernel Image
Compression: uncompressed
Data Start: 0x480000c0
Data Size: 21600768 Bytes = 20.6 MiB
Architecture: AArch64
OS: Linux
Load Address: 0x40480000
Entry Point: 0x40480000
Hash algo: sha1
Hash value: f6bfbebc237be13320a205a74db7d7c5ceca6823
Image 1 (fdt)
Description: DTB
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x49499b9c
Data Size: 57039 Bytes = 55.7 KiB
Architecture: AArch64
Load Address: 0x43000000
Hash algo: sha1
Hash value: 6ab42f1887c609b9a90bfab77718cc07c1d831b0
Image 2 (initrd)
Description: Initrd
Type: RAMDisk Image
Compression: uncompressed
Data Start: 0x494a7b3c
Data Size: 11912441 Bytes = 11.4 MiB
Architecture: AArch64
OS: Linux
Load Address: unavailable
Entry Point: unavailable
Hash algo: sha1
Hash value: e92aea4e83d07f1d0e6e03b485f89088d9a98e99
Image 3 (initrdRecovery)
Description: Recovery Initrd
Type: RAMDisk Image
Compression: uncompressed
Data Start: 0x4a00410c
Data Size: 11912441 Bytes = 11.4 MiB
Architecture: AArch64
OS: Linux
Load Address: unavailable
Entry Point: unavailable
Hash algo: sha1
Hash value: e92aea4e83d07f1d0e6e03b485f89088d9a98e99
Default Configuration: 'standard'
Configuration 0 (standard)
Description: Standard Boot
Kernel: kernel
Init Ramdisk: initrd
FDT: fdt
Hash algo: sha1
Hash value: unavailable
Configuration 1 (recovery)
Description: Standard Boot
Kernel: kernel
Init Ramdisk: initrdRecovery
FDT: fdt
Hash algo: sha1
Hash value: unavailable
## Checking hash(es) for FIT Image at 48000000 ...
Hash(es) for Image 0 (kernel): sha1+
Hash(es) for Image 1 (fdt): sha1+
Hash(es) for Image 2 (initrd): sha1+
Hash(es) for Image 3 (initrdRecovery): sha1+
Here we can see the additional initrd and configuration. We can choose which configuration we wish to boot by specifying it via the bootm command:
=> bootm 0x48000000#recovery
## Loading kernel from FIT Image at 48000000 ...
Using 'recovery' configuration
Verifying Hash Integrity ... OK
Trying 'kernel' kernel subimage
Description: Kernel
Type: Kernel Image
Compression: uncompressed
Data Start: 0x480000c0
Data Size: 21600768 Bytes = 20.6 MiB
Architecture: AArch64
OS: Linux
Load Address: 0x40480000
Entry Point: 0x40480000
Hash algo: sha1
Hash value: f6bfbebc237be13320a205a74db7d7c5ceca6823
Verifying Hash Integrity ... sha1+ OK
## Loading ramdisk from FIT Image at 48000000 ...
Using 'recovery' configuration
Verifying Hash Integrity ... OK
Trying 'initrdRecovery' ramdisk subimage
Description: Recovery Initrd
Type: RAMDisk Image
Compression: uncompressed
Data Start: 0x4a00410c
Data Size: 11912441 Bytes = 11.4 MiB
Architecture: AArch64
OS: Linux
Load Address: unavailable
Entry Point: unavailable
Hash algo: sha1
Hash value: e92aea4e83d07f1d0e6e03b485f89088d9a98e99
Verifying Hash Integrity ... sha1+ OK
## Loading fdt from FIT Image at 48000000 ...
Using 'recovery' configuration
Verifying Hash Integrity ... OK
Trying 'fdt' fdt subimage
Description: DTB
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x49499b9c
Data Size: 57039 Bytes = 55.7 KiB
Architecture: AArch64
Load Address: 0x43000000
Hash algo: sha1
Hash value: 6ab42f1887c609b9a90bfab77718cc07c1d831b0
Verifying Hash Integrity ... sha1+ OK
Loading fdt from 0x49499b9c to 0x43000000
Booting using the fdt blob at 0x43000000
Loading Kernel Image
ERROR: reserving fdt memory region failed (addr=b8000000 size=400000)
Using Device Tree in place at 0000000043000000, end 0000000043010ece
Starting kernel ...
[ 0.000000] Booting Linux on physical CPU 0x0
You can see from the above output that we specified the ‘recovery’ configuration and that’s what U-Boot loaded (e.g. notice the line “Using ‘recovery’ configuration”).
Configurations offer lots of flexibility – you could distribute a single FIT image, yet can use that in different ways, e.g. a production/debug build, a normal/recovery build, an image with lots of DTBs which can be used on multiple hardware platforms, etc.
We’ve only scratched the surface and there is a lot more to love – here are some additional capabilities of FIT images:
- FIT images can contain signatures for both individual binaries and configurations – these signatures can be created by mkimage and are verified by U-Boot. Thus aiding a secure boot – read more via this LWM article, this excellent presentation and this documentation.
- FIT images can also contain arbitrary firmware – for example you may want to use it to provide an FPGA image for U-Boot to load – take a look at the imxtract command.
- Yocto can generate FIT images for you – we achieved this by adding the following to our local.conf, however you may also want to read the source as there are many variables that can have an effect.
- KERNEL_CLASSES = “kernel-fitimage”
- KERNEL_IMAGETYPES = “fitImage”
FIT images offer many features and provide more flexibility. Given its integration with Yocto, it’s also really easy to begin using it. It’s certainty something to be considered for new projects and for implementations of secure boot.