Embedded Systems · Firmware Updates

What a Bootloader Must Establish Before Jumping to an Application

The register values, memory layout, and validation a bootloader owes an application image, and why the transfer has to be a tail branch rather than an ordinary call.

A bootloader and an application are two programs that share one address space and never speak to each other at run time. The entire interface between them is the machine state at the instant control transfers. That state is not negotiated, not versioned, and not checked by either side. It is whatever the bootloader leaves behind, and the application either survives it or does not.

Because the interface is implicit, it is usually documented nowhere and reconstructed by whoever debugs the first failure. What the state consists of, and what the application is entitled to assume about it, is the whole of the contract.

The transfer is a branch, not a load

On a larger system, a bootloader loads a program: it reads an executable from storage, maps it into memory at the address the program expects, resolves its relocations, and jumps to an entry point. On a microcontroller the program is already in the only memory it will ever occupy. Flash is mapped at a fixed address, the image was linked for that address, and no relocation is possible. The bootloader’s job is therefore much smaller and much less forgiving. It decides whether the image may run, and then it branches.

The branch is the whole handoff. Nothing is copied, so nothing can be fixed up afterwards. A link-time address baked into a literal pool is correct or it is wrong, and if the image was linked for offset 0x08010000 while the bootloader places it at 0x08008000, the first indirect call through a function pointer jumps into the wrong place. There is no loader to notice.

This is the reason a bootloader update and an application update are different operations with different risk profiles. Replacing the bootloader changes the address at which everything else is assumed to live. Replacing the application changes only the contents of a region whose base address is fixed by the bootloader’s linker script.

Four values the application inherits

On a Cortex-M core the image begins with two words that hardware reads at reset, and a bootloader reproducing that entry has to place the same two values into the same registers by hand. Word zero of the application image is the initial main stack pointer. Word one is the reset handler address, with bit 0 set because the low bit of a value written to the program counter selects Thumb state.

So the bootloader loads the main stack pointer from the image’s first word, and branches to the address in the second. Two more pieces of state are not in the image at all. On the ARMv7-M cores the vector table offset register must be pointed at the application’s table, or exception dispatch continues to use the bootloader’s table, which contains handlers written against a different stack and a different set of initialised peripherals. Cortex-M0, which predates that register, instead relocates the table by remapping the memory at address zero, and a bootloader on that core has to perform the remap rather than a register write. Interrupts must also be disabled across the window in which the stack pointer has changed but the table base has not, because an interrupt taken in that window runs bootloader code on the application’s stack.

The order matters and is easy to get wrong. The safe sequence is: mask interrupts, validate the image, set the vector table base, set the stack pointer, then branch. Setting the stack pointer first is the common mistake. It looks harmless because nothing runs in between, but a pending interrupt that was already latched can be taken at the next instruction boundary, and the handler will push onto a stack it does not own.

A fifth value is not a register at all but a convention: the bootloader must have left the memory protection unit and any security attribution unit in a state the application expects. If the bootloader configured a region as read-only for its own protection and the application needs to write there, the fault appears later, in unrelated code, with no indication of where the configuration came from.

/* One possible image layout. The first two words are the only ones the
   architecture constrains; the rest is a private contract between the
   two programs. */
struct image_header {
    uint32_t initial_sp;      /* word 0, loaded into MSP before the branch */
    uint32_t reset_handler;   /* word 1, bit 0 set for Thumb */
    uint32_t image_length;    /* bytes covered by image_crc */
    uint32_t image_crc;
    uint32_t version;
};

static void start_image(const struct image_header *hdr)
{
    /* Masking first: a latched interrupt taken after the MSP reload but
       before the vector base moves would run bootloader code on the
       application's stack. */
    __disable_irq();

    SCB->VTOR = (uint32_t)hdr;          /* table lives at the image base */
    __set_MSP(hdr->initial_sp);

    void (*entry)(void) = (void (*)(void))hdr->reset_handler;
    entry();                             /* never returns */
}

The final call is a branch in the sense that matters, even though the compiler emits a call instruction: the application’s entry point is expected never to return, so the return address left in the link register is dead. If the application ever does return, control arrives back in the bootloader with the application’s stack pointer and the application’s vector table still installed, which is a state neither program was written for.

Building an image that expects to be entered this way

The application’s linker script has to place its vector table at the start of the image, because the bootloader reads the stack pointer and reset handler from offsets 0 and 4 of whatever address it was told the image begins at. A script that emits a startup object first and the vector table afterwards produces an image whose word zero is an instruction encoding. Loading that into the stack pointer gives a value in the middle of the code, and the first push corrupts an instruction.

The link address must also be the slot address. If the application is linked at 0x08000000 but the bootloader runs it from 0x08010000, every absolute address in the image is off by the difference. Some toolchains can be told to link at an offset while still producing an image for a slot, and the offset has to match the bootloader’s slot table exactly, not approximately.

The stack pointer value in word zero deserves separate attention because it is the one field that a linker script computes rather than a programmer writes. It is normally set to the end of the RAM region reserved for the application. If the bootloader also uses RAM, and it does, the two regions must not overlap. The bootloader’s stack is abandoned at the branch, but nothing clears RAM: whatever its static buffers held is still there, and the application will reuse those addresses unless the two RAM regions are disjoint. Splitting RAM by linker script, with the bootloader’s data placed at the bottom and the application’s at the top, is the usual resolution, and it must be reflected in the application’s initial stack pointer value or the first deep call chain runs off the end of the part.

What the bootloader cannot hand over

Peripheral state is the largest part of the interface that is neither documented nor enforced. The bootloader has almost certainly enabled the clock the flash controller needs, may have configured a watchdog to cover its own boot time, and may have left a UART enabled because that is how the update arrived. The application receives all of it.

A watchdog is the dangerous case. If the bootloader starts a watchdog with a timeout that expires during the application’s initialisation, the application resets before it has done anything, and if the reset reason is not recorded, the failure looks like a spontaneous reboot. Bootloaders that start a watchdog conventionally disable it before the branch or set it to its longest period, and applications that expect to inherit a running watchdog must service it from the first instruction. Neither convention is universal.

The reset cause is the second thing that cannot be handed over cleanly. Reset status registers latch why the last reset happened, and they are read by whichever program runs first. If the bootloader reads and clears them, the information is lost unless it is passed forward through memory or through metadata. If the bootloader leaves them set, the application reads them and cannot tell whether a watchdog reset happened during its own previous run or during an update.

Flash access latency is the third. The number of wait states configured for the core clock is set by the bootloader at whatever frequency it chose. An application that raises the clock without adjusting wait states executes from flash at a speed the flash cannot support, and the failure mode is a corrupted instruction fetch that looks like random data corruption.

The cost of the extra stage

The arrangement exists because firmware has to be replaceable by something other than a debug probe. A part in an enclosure with no accessible connector still needs a way to receive a new image, and that way has to be a program that is never replaced. The price is paid in three places.

Flash is the first. The bootloader occupies a region the application cannot use, and on a small part that region is a meaningful fraction of the total. It also forces the slot layout described in dual-bank slots and boot selection, which doubles the requirement again.

Boot time is the second. Every reset now runs two programs, and the bootloader’s validation pass over the image sits in front of the application’s own initialisation. The validation is what makes the stage worth having, and it is the subject of image header and integrity validation.

Coupling is the third and least visible. The two programs share an address map, a RAM budget, a clock configuration, and a set of peripheral initial states, and every one of those is a place where a change to one breaks the other. The interface is real, it is just not written down, which is why the practical rule is to write it down: a header struct with a magic value and a version field, placed at a known offset in the image, turns an implicit contract into one that can at least be checked.

RISC-V parts avoid part of this. There is no initial stack pointer word, so the image’s entry point must establish the stack itself, and the trap vector base lives in a control register rather than at a fixed address in the image. The bootloader’s obligation shrinks to setting the entry address and, if it uses vectored dispatch, the trap base. Everything else the application does for itself, which is less to get wrong at the handoff and more to get wrong in the startup code.

References

  1. ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm
  2. The Definitive Guide to Arm Cortex-M3 and Cortex-M4 Processors — Joseph Yiu, Newnes
  3. MCUboot documentation: Design — MCUboot project