Hey there, welcome back! In Day 4, BL2 successfully initialized the DRAM and the UART console. The platform is stable. Now, BL2 must execute its primary mission: loading the remaining firmware stages into memory and passing the baton to the Secure Monitor.
The Firmware Image Package (FIP)
BL2’s most complex logic lies inside bl2_load_images() (defined in bl2/bl2_image_load_v2.c).
To load the images, BL2 uses TF-A’s load_auth_image() framework. It reads the FIP (Firmware Image Package) from flash memory.
What is a FIP?
The FIP is an open-source standard format designed by the TF-A project (originally ARM) to package multiple binaries and certificates into a single continuous blob. Instead of managing half a dozen separate flash partitions for each individual payload, everything is bundled together. This dramatically simplifies the boot media layout, the flashing process, and the bootloader’s I/O drivers, as BL2 only needs to read from one partition.
At build time, TF-A uses a host-side C utility called fiptool (located in tools/fiptool) to concatenate all these separate files into the final fip.bin. A complete build command looks something like this:
fiptool create --tb-fw bl2.bin --soc-fw bl31.bin --tos-fw bl32.bin --nt-fw u-boot.bin fip.bin.
[!TIP] Wait, is BL2 also in the FIP? Yes! You might notice
--tb-fw bl2.binin the command above. In the standard TF-A boot architecture, the BootROM loads and executes BL1. BL1 then initializes the flash drivers, parses this exact same FIP to extract the BL2 binary, and executes it. BL2 then takes over, parses the same FIP, and extracts the remaining payloads (BL31, BL32, BL33).
[!IMPORTANT] Binary Format (No ELFs!) You might wonder if the FIP contains standard
.elffiles. The answer is no. TF-A does not parse ELF files in BL2 because BL2 is designed to be as minimal as possible. During the build process, the Makefiles runobjcopy -O binaryto strip all ELF headers and sections, leaving only the raw, executable machine code (.bin). BL2’s loader then blindly copies these raw bytes from the FIP offset directly into the target memory address.
The FIP On-Disk Format
The FIP’s binary layout is defined in include/tools_share/firmware_image_package.h. It starts with a small fixed-size header, followed by an array of ToC (Table of Contents) entries:
| |
So the FIP is essentially a flat binary: a fip_toc_header_t followed by an array of fip_toc_entry_t records (terminated by a null UUID entry), followed by the raw payload data blobs one after another. Each ToC entry maps a standard 16-byte UUID to a byte offset and size within the package.
The IO Storage Abstraction
If you look closely at drivers/io/io_fip.c, you will notice it follows a classic filesystem-like design. It registers an io_dev_funcs_t struct containing function pointers like .open, .read, .size, and .close:
| |
This is part of TF-A’s highly abstracted generic IO storage framework. This framework allows TF-A’s higher-level load_auth_image() function to be completely agnostic to the storage medium—it treats a FIP exactly the same way it treats an eMMC block device or a Memmap!
The Complete Loading Flow
Here is the complete bootloader execution flow from platform setup to bytes in RAM:
- Registration: During platform setup, the platform registers the FIP driver by calling
register_io_dev_fip(). For example, QEMU does this inplat/qemu/common/qemu_io_storage.c. - Routing: The platform uses an IO policy framework to map Image IDs (e.g.,
BL31_IMAGE_ID) to this FIP IO device. - Loading:
bl2_load_images()(inbl2/bl2_image_load_v2.c) iterates through the platform’s list of required images. For each one, it callsload_auth_image(). - FIP Init & Validation: Inside the IO framework,
fip_dev_init()reads the FIP header from the backend storage and validates the magic number:
- UUID Lookup: When
load_auth_image()calls.openfor a specific image,fip_file_open()seeks past the header and iterates through the ToC entries, comparing each entry’s UUID against the requested one:
| |
- Read: Once the entry is found, subsequent
.readcalls (fip_file_read()) seek to the payload’soffset_addresswithin the FIP and copy the raw bytes into the target memory buffer.
What Does BL2 Load?
BL2 systematically searches the FIP for the following critical binaries:
- BL31 (EL3 Runtime Software): The Secure Monitor. This is the TF-A component that provides PSCI and SMC routing. (Note: On some platforms like NXP i.MX or Allwinner, U-Boot SPL acts as the loader (replacing BL1/BL2), but SPL itself is not the EL3 runtime monitor. It loads TF-A’s BL31.)
- BL32 (Secure-EL1 Payload): The Trusted OS. Common examples include OP-TEE (maintained by TrustedFirmware.org) or Trusty (Google’s Little Kernel-based TEE). We will dive deep into OP-TEE in upcoming articles.
- BL33 (Non-Trusted Firmware): The Normal World Bootloader (e.g., U-Boot, UEFI).
Loading and Memory Layout
As BL2 iterates through the FIP, it loads each binary into a highly specific memory region.
[!NOTE] How does BL2 know where to load them? The FIP itself does not contain load addresses, only offsets within the blob. The memory map is dictated by the platform. BL2 uses
bl_mem_params_node_tdescriptors that are either defined statically by the platform port (e.g., inplat/<vendor>/<board>/include/platform_def.h) or discovered dynamically via the Firmware Configuration (fconf) framework.
This is where the TrustZone architecture shines:
- BL31 is loaded into Secure SRAM (or highly restricted Secure DRAM). Because it will run at EL3, it must be protected from everything else in the system.
- BL32 is loaded into Secure DRAM. It needs more memory than SRAM can provide because it’s a full OS, but it still must be isolated from the Normal World.
- BL33 is loaded into Non-Secure DRAM. This is the wild west. BL33 has no access to Secure memory.
Authentication and Hardware Encryption Hooks (TBBR)
BL2 doesn’t call a simple load_image() and hope for the best. The actual function it calls for each image is load_auth_image() (defined in common/bl_common.c). This function wraps the raw loading with a recursive authentication framework.
If Trusted Board Boot Requirements (TBBR) are enabled, load_auth_image() calls load_auth_image_recursive() which walks up the certificate chain — loading and verifying parent certificates before the image itself:
| |
Notice the security discipline: if authentication fails, the loaded image bytes are immediately zeroed and flushed from cache before returning the error. This prevents a partially-loaded, unauthenticated image from lingering in memory where it could be exploited.
TF-A provides a highly modular cryptography framework (drivers/auth/crypto_mod.c) allowing SoC vendors to either use a software fallback or wire up their physical hardware accelerators.
Software Authentication (Raspberry Pi Example)
By default, TF-A uses an embedded software crypto library (mbedTLS). For example, the Raspberry Pi 3/4 ports simply include drivers/auth/mbedtls/mbedtls_crypto.mk.
When included, the mbedTLS wrapper registers its software hooks into TF-A’s crypto framework via the REGISTER_CRYPTO_LIB macro. The actual registration call has multiple variants depending on the configured CRYPTO_SUPPORT level. Here is the most common one, with full auth + hash + AES-GCM decryption:
| |
The 8 arguments to REGISTER_CRYPTO_LIB are: name, init, verify_signature, verify_hash, calc_hash, auth_decrypt, convert_pk, and finish. BL2 then uses these software hooks to verify the RSA/ECDSA signature of the certificate, and checks the SHA-256 hash of the binary.
Hardware Crypto (STM32MP1 Example)
However, relying purely on software crypto leaves the symmetric decryption key vulnerable in memory. High-security platforms instead use physical hardware to fetch keys (like from OTP fuses) and dedicated hardware accelerators for cryptographic operations.
The STM32MP1 platform is a fantastic example. Instead of registering the generic mbedTLS library, STMicroelectronics registers its own crypto library that uses hardware peripherals directly:
| |
For firmware encryption, TF-A provides the plat_get_enc_key_info() hook so platforms can securely fetch decryption keys from hardware. STM32MP1 implements this to read the key from OTP (One-Time Programmable) fuses word-by-word:
| |
In contrast, the Raspberry Pi ports have no hardware crypto — they rely entirely on the software mbedTLS library. You can verify this: plat/rpi/rpi3/platform.mk includes only drivers/auth/mbedtls/mbedtls_crypto.mk and never overrides plat_get_enc_key_info().
If any image fails authentication or decryption, the boot process halts immediately to protect the system.
The Handoff to BL31
With BL31, BL32, and BL33 securely in memory, BL2 needs to hand over control to BL31.
Flushing and Preparing the Parameters
Before jumping, BL2 must ensure that all loaded image data and the parameter structures are visible in physical RAM (not trapped in cache lines that BL31’s fresh MMU context won’t see). This happens in two stages:
- Per-image flush: Inside
load_auth_image()(common/bl_common.c), after each image is successfully loaded and authenticated, TF-A immediately flushes that image’s data from cache to main memory:
- Parameter descriptor flush: After all images are loaded,
bl2_load_images()callsplat_flush_next_bl_params(), which flushes thebl_params_tdata structures. This structure is how BL2 tells BL31 where BL32 and BL33 were loaded:
The bl_params_t structure is a linked list of bl_params_node_t entries, each containing the image_id, image_info, and ep_info (entry point info) for one executable image. BL31 will receive the address of this structure in register x0 and parse it using bl31_params_parse_helper() to extract the BL32 and BL33 entry points.
The Two Handoff Paths
Finally, BL2 triggers the handoff. The mechanism depends entirely on which Exception Level BL2 is running in. You can see both paths clearly in bl2/bl2_main.c:
Path 1: BL2 runs at Secure-EL1 (Classic Flow)
In the traditional flow, BL1 is still resident in EL3, waiting. BL2 (running at S-EL1) has no way to directly jump to BL31 because it lacks the privilege to set elr_el3. Instead, BL2 executes a Secure Monitor Call (SMC) to trap back up into EL3. BL1 intercepts this SMC, reads the provided entry point info, and jumps to BL31.
Here is the exact C code from bl2_main.c that triggers this trap:
| |
Path 2: BL2 runs directly at EL3 (Modern Flow)
In modern flows (like RESET_TO_BL2 where BL1 is completely bypassed), BL2 runs directly in EL3. Since it’s already at the highest privilege level, it won’t execute an SMC. Instead, it calls bl2_run_next_image() — a small assembly routine that tears down the EL3 environment and jumps to BL31.
Here is the C code that initiates this path:
| |
And here is the complete assembly routine that performs the actual jump — this is bl2/aarch64/bl2_run_next_image.S in its entirety:
| |
A few things to appreciate about this assembly:
- The arguments are loaded in reverse order (x6/x7 first, x0/x1 last) because loading x0 last means x0 contains the
bl_params_tpointer that BL31 expects as its first argument. exception_return(which expands to theeretinstruction) atomically restoresSPSR_EL3intoPSTATEand branches to the address inELR_EL3— dropping execution into BL31.bl2_plat_prepare_exitgives the platform one last hook to do any final cleanup (e.g., disabling DMA engines, securing memory regions) before control is irrevocably transferred.
And with that eret, BL2’s job is complete. It has provisioned the entire memory landscape and handed over the keys to the castle. Tomorrow, in Day 6, we pivot slightly: I will show you how the Raspberry Pi 5 completely bypasses BL1 and BL2! Catch you in the next one!

