UNIT 15 · LESSON 1 OF 6

Application Images and Bootloaders

What has to be on the device from the day it ships so that new firmware can be installed, and so that a failed installation can still be recovered?

INTERACTIVEWhere the application lives behind a bootloader
Flash map with a bootloader, two image slots and the start of the primary slotflash from 0x1000_0000 (RP2040 XIP window)primary slot 256 KiBsecondary slot 256 KiBbootloaderstart of the primary slotheader 512 Bvector tablecode …0x1000_8200slot starts at 0x1000_8000; vector table at slot + header = 0x1000_82000x1000_8200 is a multiple of 256: VTOR can point at it, and the bootloader jumpsthrough itroom for the application: 256 KiB − header − records (≈152 B) − trailer (≈1.5 KiB) =253.8 KiB120 KiB fits, with 133.8 KiB to spare
Flash map with a bootloader, two image slots and the start of the primary slotflash from 0x1000_0000 (RP2040 XIP window)primary slot 256 KiBsecondarybootloaderstart of the primary slotheader 512 Bvector tablecode …0x1000_8200slot starts at 0x1000_8000; vector table at slot +header = 0x1000_82000x1000_8200 is a multiple of 256: VTOR can point atit, and the bootloader jumps through itroom for the application: 256 KiB − header − records(≈152 B) − trailer (≈1.5 KiB) = 253.8 KiB120 KiB fits, with 133.8 KiB to spare

Try this

Bootloader region
Header size (imgtool -H)
Vector table at 0x1000_8200: aligned; image fits.

A bootloader occupies the start of flash, so the application is linked to run from its slot instead. With MCUboot the slot begins with the image header (32 bytes of fields, padded with imgtool -H), then the application’s vector table and code; hash, key and signature records follow the image, and the slot ends with the trailer that records swap progress. The vector table must sit where VTOR can point: on a Cortex-M0+ such as the RP2040, VTOR has no bits 7:0, so the table must be 256-byte aligned. Trailer and record sizes are approximate.

What you will be able to do
  • Explain why updating firmware in the field needs a separate bootloader, and which part of the job it does.
  • Describe an MCUboot image: its header, payload, protected and unprotected records, and the slot trailer.
  • Compute where an application’s vector table lands in its slot and check it against the VTOR alignment rule.
  • Compute the largest application that fits a slot after the header, records and trailer.
  • List the steps of a clean hand-off from bootloader to application and what goes wrong when one is skipped.
Before you start
  • The reset sequence, boot ROMs and VTOR (unit 5, lessons 1–3).
  • Linker scripts and memory regions (unit 4, lesson 4).
Steps in this lesson
  1. Why updates need a bootloader
  2. The image in its slot
  3. Linking for the slot
  4. The hand-off
  5. A path back
  6. Worked example: placing an application behind a 32 KiB bootloader
  7. Common misconceptions

The puzzle

A thousand devices are installed in buildings you will never visit, and a bug fix is ready. The fix cannot be installed by the program it replaces, at least not safely: erase the flash you are executing from and the processor has nothing left to run. What has to be on the device from the day it ships so that new firmware can be installed, and so that a failed installation can still be recovered?

STEP 1

Why updates need a bootloader

After reset the processor starts at a fixed place: the boot ROM, then (on the RP2040) boot2, then the vector table of whatever is in flash (unit 5, lesson 2). An update needs a small program that also runs at every reset, is never replaced during an update, and decides what to start: a bootloader. It sits at the start of flash, where the reset path lands, and the application moves to a region of its own.

Bootloaders divide the work. MCUboot, the bootloader Zephyr and Trusted Firmware-M use, only verifies images at startup and switches to a new one; downloading it is the application’s job. The running application receives the new image over whatever link the product has and writes it to a second flash region, the secondary slot. At the next reset the bootloader checks it and installs it (lessons 2 and 3). Other designs put the download in the bootloader too, or rely on a boot ROM that can load firmware over USB or a UART, like the RP2040’s USB mode.

STEP 2

The image in its slot

A slot is a flash region that holds one image. MCUboot’s usual flash map has a bootloader area, a primary slot the application runs from and a secondary slot for the incoming image, each erasable without affecting the others. An MCUboot image is the application binary wrapped by the signing tool, imgtool:

  • a header at the start of the slot: a 32-bit magic number 0x96F3B83D, the header size, the image size, flags and a version such as 1.2.3+4, 32 bytes of fields in all;
  • the payload, which begins with the application’s vector table;
  • records (type-length-value entries) after the payload: a SHA-256 hash, the hash of the signing key and the signature, plus protected entries such as a security counter;
  • a trailer at the very end of the slot, written mostly by the bootloader and the update code (imgtool’s --pad can pre-fill it): swap progress, “copy done” and “image OK” flags and a 16-byte magic.

↑ This step uses the figure at the top of the page.

The payload starts right after the header, so the vector table address is

Avector table=Aslot+HA_{\text{vector table}} = A_{\text{slot}} + H

and it must be one that VTOR can hold. On a Cortex-M0+ VTOR has no bits 7:0 (CMSIS puts its offset field at bit 8), so the table must be 256-byte aligned; on the Cortex-M4 the field starts at bit 7, and larger tables need more alignment (unit 5, lesson 3). That is why imgtool pads the header (-H 0x200, say) instead of using the bare 32 bytes. The records and trailer take space at the end, so

Sapp, max=Sslot−H−Srecords−StrailerS_{\text{app, max}} = S_{\text{slot}} - H - S_{\text{records}} - S_{\text{trailer}}

(for swap using scratch and overwrite; swap using move also needs one spare sector, so the trailer is rounded up to whole sectors and one sector less is available).

and imgtool’s --slot-size option refuses an image that would overflow into the trailer.

STEP 3

Linking for the slot

The application is an ordinary program, but it no longer starts at the beginning of flash. MCUboot requires images “built to run from a fixed location”, not position-independent code, so the linker script’s flash origin must be the slot address plus the header (unit 4, lesson 4): every absolute address in the binary, starting with the vector table entries, depends on it. Zephyr does this when the application is configured with CONFIG_BOOTLOADER_MCUBOOT and its code partition is set to slot0_partition; its build also prepends the zeroed header that imgtool fills in. A binary linked for the start of flash and copied into a slot has vector entries that point into the bootloader, and crashes on its first exception or its first call.

STEP 4

The hand-off

Starting the application is not a function call. MCUboot’s Cortex-M port reads the first two words after the header, the application’s initial stack pointer and reset vector, and then:

  1. quiets what the bootloader used: the system timer and USB are stopped, NVIC enables and pending bits cleared, the MPU and stack-limit registers reset;
  2. points VTOR at the application’s table, when so configured (otherwise the application’s startup code sets VTOR itself);
  3. loads MSP with the application’s stack pointer and branches to its reset handler.
INTERACTIVEWhat the bootloader does before your code runs
The steps a bootloader takes from reset to the application0Core loads SP and PC from the bootloader’s own vector table1Recovery pin not held: continue2boot_go(): no swap pending; image header, hash and signature valid3Stop the system timer and USB, clear NVIC enables and pending bits4VTOR ← slot + header (the application’s vector table)5MSP ← word 0 of that table (the application’s initial stack)6Branch to word 1: the application’s reset handler runsThe application starts with a quiet core, exactly as if it had come out of reset atits own vector table.
The steps a bootloader takes from reset to the application0Core loads SP and PC from the bootloader’s own v…1Recovery pin not held: continue2boot_go(): no swap pending; image header, hash a…3Stop the system timer and USB, clear NVIC enable…4VTOR ← slot + header (the application’s vector t…5MSP ← word 0 of that table (the application’s in…6Branch to word 1: the application’s reset handle…The application starts with a quiet core, exactly asif it had come out of reset at its own vector table.
Application running.

A bootloader is an ordinary program that ends by starting another one. MCUboot’s Zephyr port first checks for a recovery request, then lets boot_go() finish or start any swap and validate the image. Before jumping it quiets the hardware it used, points VTOR at the application’s vector table (a configuration option in MCUboot; otherwise the application’s startup code does it), loads the application’s initial stack pointer and branches to its reset handler: the same two loads a reset performs, without resetting anything else.

The last two steps are exactly what boot2 does on the RP2040 in a handful of instructions, and what the hardware does at reset: load SP and PC from a vector table. What a jump does not do is reset the peripherals, so anything the bootloader turned on is still on when the application starts, unless the bootloader turns it off first.

STEP 5

A path back

A bootloader that is never replaced must never need replacing, so it is kept small, well tested and able to recover a device whose application is broken. MCUboot can enter serial recovery (an image upload over a UART) when a pin is held at reset or when no valid image is found. The RP2040 boot ROM falls back to USB boot when the boot2 CRC fails (unit 5, lesson 2). Whichever you use, the recovery path must not depend on the application working.

STEP 6

Worked example: placing an application behind a 32 KiB bootloader

An RP2040-style map: flash is executed in place from 0x1000_0000, and the first 32 KiB (boot2 plus a bootloader) are reserved. The primary slot is 256 KiB and starts at

0x1000_0000+32×1024=0x1000_80000x1000\_0000 + 32 \times 1024 = 0x1000\_8000

With a 0x200-byte header the vector table is at 0x1000_8200. Its offset 0x8200 = 33 280 = 130 × 256, so it is 256-byte aligned and the Cortex-M0+ VTOR can hold it. With the bare 32-byte header the table would be at 0x1000_8020, 32 bytes past a 256-byte boundary: unusable.

The largest application, taking the records as about 152 bytes (a SHA-256, a key hash and an ECDSA P-256 signature, each with a 4-byte record header, plus a 4-byte area header) and the trailer as about 1584 bytes (the 1536-byte swap status plus flags and magic):

262 144−512−152−1584=259 896 bytes≈253.8 KiB262\,144 - 512 - 152 - 1584 = 259\,896\ \text{bytes} \approx 253.8\ \text{KiB}

A 120 KiB application fits with room to grow; the application must be linked at 0x1000_8200.

MYTHS AND FACTS

Common misconceptions

The bootloader is part of the application

It is a separate program with its own vector table, linker script and build, and it runs first at every reset.

Any binary can be copied into the slot

It must be linked for the slot’s address and carry the header and records the bootloader expects.

Starting the application is just a jump

The stack pointer, VTOR and the state of every peripheral the bootloader touched must be set up first.

If an update fails, the device goes back to the factory

Only if nobody designed a recovery path: a second slot, serial recovery or a ROM loader.

Check yourself

Answer in your head, then open the card.

The bootloader region is 64 KiB at 0x1000_0000 and imgtool adds a 0x100-byte header. Where is the vector table, and can a Cortex-M0+ use it?

At 0x1001_0000 + 0x100 = 0x1001_0100. The offset is a multiple of 256, so VTOR can hold it.

Why does MCUboot read two words right after the image header before jumping?

They are the first two entries of the application’s vector table: the initial stack pointer, loaded into MSP, and the reset vector, the address it branches to.

An application built with the default linker script (flash origin 0x1000_0000) is signed and written to a slot at 0x1000_8000. What happens when the bootloader starts it?

Every address in the image was computed for an image at 0x1000_0000, so the reset vector and handler addresses point into the bootloader region (with the default pico-sdk script, the words right after the header are not even a vector table but boot2’s code). The processor runs the wrong code and crashes; the image must be linked for the slot.

A bootloader uses a timer interrupt for an LED blink and jumps to the application with that timer still running. What can happen?

The timer keeps raising its request. When the application enables interrupts, the NVIC takes it through the application’s vector table, whose handler may be a default fault loop or may touch a driver that is not initialised yet.

Sources (6)
  1. MCUboot v2.1.0, docs/design.md (“Image format”, “Image slots”, “Image trailer”, “Limitations”) — IMAGE_MAGIC 0x96f3b83d, IMAGE_HEADER_SIZE 32; struct image_header (ih_magic, ih_load_addr, ih_hdr_size, ih_protect_tlv_size, ih_img_size, ih_flags, ih_ver); TLVs “placed after the end of the image”; images must be “Built to run from a fixed location (i.e., not position-independent)”; the trailer holds swap status, swap info, copy done, image OK and a 16-byte magic, and the swap status “for 128 slot sectors with a 4-byte alignment … would become 1536 B”
  2. MCUboot v2.1.0, docs/imgtool.md (“Signing images”) — imgtool sign “adds a header and trailer that the bootloader is expecting”; -H/--header-size and --pad-header add a zeroed header; “The --slot-size argument is required and used to check that the firmware does not overflow into the swap status area”
  3. MCUboot v2.1.0, boot/zephyr/main.c (do_boot and main) — “The beginning of the image is the ARM vector table, containing the initial stack pointer address and the reset vector”; reads two words at ih_hdr_size; sys_clock_disable(), usb_disable(), cleanup_arm_nvic(), MPU config cleared, PSPLIM/MSPLIM zeroed (or only irq_lock() without CONFIG_MCUBOOT_CLEANUP_ARM_CORE); SCB->VTOR set under CONFIG_BOOT_INTR_VEC_RELOC; __set_MSP(vt->msp); jump to vt->reset; serial recovery on a GPIO or when no bootable image is found
  4. Zephyr v3.7.0, doc/services/device_mgmt/dfu.rst (“MCUboot”) — define the flash partitions, set chosen zephyr,code-partition = &slot0_partition, enable CONFIG_BOOTLOADER_MCUBOOT so the application is built in an MCUboot-compatible manner, and flash it “at the correct offset (right after the bootloader)”
  5. Raspberry Pi Ltd, pico-sdk 1.5.1, boot_stage2/asminclude/boot2_helpers/exit_from_boot2.S — the smallest hand-off: VTOR ← XIP_BASE + 0x100, then ldmia loads the stack pointer and reset vector from that table, msr msp, bx to the reset handler
  6. Arm, CMSIS 6 v6.1.0, CMSIS/Core/Include/core_cm0plus.h and core_cm4.h — SCB_VTOR_TBLOFF_Pos is 8 on the Cortex-M0+ (VTOR optional, __VTOR_PRESENT) and 7 in core_cm4.h: the low bits of the table address do not exist