What Is a Bootloader? The Hidden Code That Starts Every Embedded Device
Day 70 of 100 Days of Tech | By Ameya Kshirsagar
What Is a Bootloader? The Hidden Code That Starts Every Embedded Device
Day 70 of 100 Days of Tech | By Ameya Kshirsagar

There is a moment, invisible to most developers, that happens every single time an embedded device powers on.
The chip gets voltage. Capacitors charge. The reset circuitry releases. And in the span of a few microseconds — before your main() function runs a single instruction, before your RTOS scheduler starts, before your sensors initialize — a small, quiet piece of code takes control.
It checks conditions. It makes decisions. It sets up the environment that everything else depends on. And then, having done its job, it hands control to your firmware and disappears.
This is the bootloader. And understanding it properly ties together almost everything we’ve covered in this series: the memory layout from Day 68, the vector table from Day 66, the flash regions and startup code, the OTA update patterns that modern IoT devices depend on.
This is the episode that answers the question that every previous episode quietly raised: how does it all actually start?
[embed]
Why Bootloaders Exist
In the earliest microcontroller systems, there was no bootloader. The CPU reset, jumped to address zero, and started executing whatever was there. To update the firmware, you physically removed the chip, put it in a programmer, erased it, wrote new code, and reinstalled it.
This was acceptable when embedded systems were in fixed installations, serviced by engineers with equipment. It became completely impractical when embedded systems started going into consumer products, remote installations, connected devices, and safety-critical systems that needed to be updated in the field.
The bootloader solves several problems simultaneously:
Firmware updates without physical access: A bootloader can receive new firmware over any communication interface — UART, USB, CAN, WiFi, cellular — and program it into flash. The device can be updated anywhere in the world.
Safe updates with rollback: A bootloader can verify that new firmware is valid before committing to it, and roll back to a known-good image if the new firmware fails to start correctly.
Security: A bootloader can verify cryptographic signatures on firmware images, ensuring that only authorized code ever runs on the device.
Hardware abstraction: The bootloader initializes the minimum hardware required, handing the application a known, clean environment to start from.
Recovery: An indestructible ROM bootloader provides a last resort for recovering devices with corrupted application firmware.
These are not nice-to-have features. For any device that ships to customers, operates remotely, or exists in a safety-critical context, they are requirements. The bootloader is where these requirements are implemented.
The Complete Boot Sequence
Let’s trace the complete sequence from power application to the first instruction of your application code running. This is the story nobody tells you in full — so we’re going to tell it properly.
Power-On and Reset
When voltage is applied to a microcontroller, several things happen in rapid sequence at the hardware level.
The power-on reset circuit monitors the supply voltage. Until the voltage reaches a stable threshold — typically around 1.8V to 3.3V depending on the chip — the reset signal is held asserted. This prevents the CPU from starting in an undefined power state.
Once voltage stabilizes, the reset is released. The CPU’s internal logic initializes: registers are cleared to their reset values, the pipeline is flushed, and the processor enters a known state.
At this point the CPU needs to know where to start executing. This is hardcoded in the silicon: the CPU reads its initial configuration from a fixed memory address.
The Vector Table — The CPU’s First Read
On ARM Cortex-M processors, the very first thing the CPU does after reset is read two 32-bit words from the base of the vector table — by default, address 0x00000000, which is aliased to flash at 0x08000000 on STM32 devices.
Word 0: The initial stack pointer value. The CPU loads this directly into the MSP (Main Stack Pointer) register. Your stack is now configured — the CPU has a valid stack and can make function calls.
Word 1: The reset handler address. The CPU loads this into the Program Counter and begins executing from this address.
These two reads happen in hardware, automatically, before any software has run. This is why the linker script must place the vector table at the exact start of flash — the CPU is hardwired to look there.
The vector table continues beyond these two entries with addresses for all exception handlers and interrupt service routines — as you saw in Day 66. But the reset sequence only cares about these first two words.
The Reset Handler
The reset handler is the first software that executes. On most STM32 projects, it’s in startup_stm32xxxx.s — a small assembly file provided by ST as part of the device support package.
It does exactly three things before calling main():
Copy .data section: Initialized global variables — like int counter = 100; — have their initial values stored in flash (since flash is non-volatile) but need to live in RAM at runtime (since the CPU operates on RAM). The startup code copies these values from their flash storage location to their RAM location. This is the _sidata to _sdata copy you see in every STM32 startup file.
Zero .bss section: Uninitialized global variables — like int counter; — are guaranteed by the C standard to be zero at program start. The startup code zeroes the entire .bss section in RAM to fulfill this guarantee.
Call main(): With the C runtime environment properly initialized, the startup code calls main(). By the time your first line of code runs, the stack is set, globals are initialized, and BSS is zeroed.
If a bootloader is present, this main() is the bootloader's main function — not your application's.
Bootloader Execution
The bootloader’s main() runs with the chip in a known state. It now makes decisions.
A typical bootloader decision sequence:
- Check update trigger: Is there a firmware update pending? Did the application request a bootloader entry? Is a specific pin held low (indicating a programmer is attached)? Is there a valid update image in a secondary flash partition?
- Check application validity: Is there a valid application in the expected flash location? This typically involves checking a magic number at the start of the application vector table, verifying a stored CRC or hash, or checking that the application size is within expected bounds.
- Enter update mode if needed: If an update is pending or no valid application exists, the bootloader enters update mode — waiting for firmware over whichever communication interface it supports.
- Jump to application: If the application is valid and no update is needed, jump.
The Firmware Jump
The firmware jump is technically the most delicate operation in the entire boot sequence. The bootloader and application are completely independent programs. Jumping between them requires careful orchestration.
c
typedef void (*pFunction)(void);
void boot_jump_to_application(uint32_t app_flash_address) {
uint32_t app_stack_pointer;
uint32_t app_reset_handler;
pFunction jump_to_app;
// Read initial SP from first word of application vector table
app_stack_pointer = *(__IO uint32_t*)app_flash_address;
// Read reset handler address from second word
app_reset_handler = *(__IO uint32_t*)(app_flash_address + 4);
// Sanity check — SP should point into RAM
if ((app_stack_pointer & 0x2FFE0000) != 0x20000000) {
// Invalid — no application present
return;
}
// Disable all interrupts
__disable_irq();
// Disable SysTick and clear pending interrupt
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
// Relocate vector table to application address
SCB->VTOR = app_flash_address;
// Set main stack pointer to application's SP
__set_MSP(app_stack_pointer);
// Create function pointer to application reset handler
jump_to_app = (pFunction)app_reset_handler;
// Enable interrupts and jump
__enable_irq();
jump_to_app();
// Never reaches here
}
Each step matters:
Disable interrupts: Any pending interrupt from the bootloader could fire in the application’s context before its vector table is set up — causing a fault.
Clear SysTick: If the bootloader used SysTick for timing, it must be stopped. SysTick firing in the application before the application initializes it causes immediate problems.
Relocate VTOR: The SCB->VTOR register tells the CPU where the vector table is. If it still points to the bootloader’s vector table, all interrupts in the application will jump to bootloader handlers — catastrophic.
Set MSP: The application’s stack pointer must be set before the jump. The application’s startup code assumes MSP is already initialized.
Jump: Call the application’s reset handler as a function. The application’s startup code runs, copies its own .data, zeroes its own .bss, and calls its own main().
Miss any of these steps and you get hard faults, corrupted state, or code executing from completely wrong addresses.
Primary and Secondary Bootloaders
On more complex systems, booting is a two-stage process.
Primary Bootloader (ROM Bootloader)
Every modern microcontroller has a small bootloader burned into read-only memory at the factory. This code is immutable — you cannot erase or modify it under any circumstances.
On STM32, the ROM bootloader lives in System Memory at 0x1FFF0000. It’s activated by holding the BOOT0 pin high during reset, which causes the CPU to boot from System Memory instead of user flash.
The STM32 ROM bootloader supports firmware programming over:
- UART (most commonly)
- USB DFU (Device Firmware Upgrade)
- SPI
- I2C
- CAN
This is the safety net that makes STM32 development boards essentially unbrickable. No matter how badly you corrupt your application flash, the ROM bootloader is always there. Connect over UART using STM32CubeProgrammer, erase the flash, and flash new firmware.
On ESP32, the ROM bootloader in internal ROM performs hardware initialization, loads the second-stage bootloader from flash, and verifies it if secure boot is enabled.
Secondary Bootloader (Second Stage)
The secondary bootloader lives in the first portion of user flash — the part you write and control. This is where all custom bootloader logic lives.
For the STM32, if you want OTA updates, secure boot, multi-image management, or rollback capability, you write a secondary bootloader and place it at the start of flash (0x08000000). Your application goes immediately after it (0x08004000 or wherever your bootloader ends).
For the ESP32, Espressif provides a full-featured second-stage bootloader as part of ESP-IDF. It handles:
- Partition table reading
- OTA partition selection
- Secure boot verification
- Flash encryption
- Anti-rollback version checking
You can modify or replace the ESP32 second-stage bootloader entirely — though most applications use Espressif’s implementation as-is.
Flash Partitioning for Bootloaders
A bootloader-based system requires careful flash partitioning — dividing the available flash into regions with specific purposes.
A typical STM32 layout for a system with OTA:
Flash Memory Map (1MB total):
┌─────────────────────────────────────┐ 0x08000000
│ Bootloader (32KB) │
│ - Reset handler │
│ - Update logic │
│ - Crypto verification │
├─────────────────────────────────────┤ 0x08008000
│ Bootloader Config (16KB) │
│ - Boot flags │
│ - Update state │
│ - Boot attempt counter │
├─────────────────────────────────────┤ 0x0800C000
│ Application Slot A (480KB) │
│ - Active firmware │
├─────────────────────────────────────┤ 0x08084000
│ Application Slot B (480KB) │
│ - Pending OTA image │
└─────────────────────────────────────┘ 0x080FFFFF
The ESP32 uses a partition table — a binary table stored in flash at a fixed offset — that defines named partitions with types and subtypes. The partitions.csv file in every ESP-IDF project defines this layout. Common partitions:
nvs— Non-Volatile Storage for application dataotadata— OTA status and boot selectionota_0— First OTA application slotota_1— Second OTA application slotfactory— Factory fallback image (optional)
The ESP32 bootloader reads the otadata partition to determine which OTA slot to boot, implements anti-rollback based on firmware version numbers, and handles the slot switching atomically.
OTA Firmware Updates — The Complete Flow
OTA updates are where bootloader design becomes genuinely complex — and genuinely important.
Here is the complete, production-grade OTA flow:
Phase 1 — Download The application firmware connects to an update server, checks if a newer firmware version is available, downloads the firmware image, and writes it to the inactive OTA slot (Slot B if Slot A is currently running).
During download, each chunk is written to flash incrementally. A download failure at this stage is safe — Slot B is incomplete and the bootloader will simply not boot from it.
Phase 2 — Verification After the complete image is written, the application verifies it:
- Check firmware size is within expected bounds
- Verify CRC32 or SHA256 hash matches the server-provided value
- Verify digital signature if secure boot is enabled
- Check firmware version is newer than current (anti-rollback)
Phase 3 — Commit The application writes a boot request to the bootloader config region in flash:
- Mark Slot B as pending boot
- Set boot attempt counter to 0
- Store expected firmware hash
- Trigger system reset
Phase 4 — Bootloader Decision On the next boot, the bootloader:
- Reads boot config — sees Slot B pending
- Verifies Slot B image hash
- Increments boot attempt counter
- Jumps to Slot B application
Phase 5 — Application Confirmation The new application (Slot B) starts. It performs self-checks:
- Hardware initialization succeeds
- Communication interfaces work
- Application logic starts correctly
- Server connection re-established (confirms network connectivity with new firmware)
If all checks pass, the application writes a “boot confirmed” flag to bootloader config. Boot attempt counter is cleared. Slot B is now the permanent active slot.
Phase 6 — Rollback (if needed) If the new application crashes before confirming, the bootloader sees the boot attempt counter exceed its threshold on the next reset. It marks Slot B as bad, restores Slot A as active, and the device comes back up running the previous firmware.
The device is never left unrecoverable. This is the guarantee that makes OTA safe to deploy to millions of devices in the field.
Secure Boot
For devices where security matters — and increasingly, all IoT devices should be considered security-relevant — secure boot provides cryptographic assurance that only authorized firmware runs.
The Chain of Trust
Secure boot works through a chain of trust, where each stage verifies the next:
Root of Trust: A public key hash is burned into one-time-programmable (OTP) fuses during manufacturing. Once burned, this cannot be changed. This is the anchor of the entire security model.
ROM Bootloader Verification: The ROM bootloader verifies the second-stage bootloader using the public key whose hash is in the fuses. If the signature is invalid, the ROM bootloader refuses to proceed.
Second-Stage Verification: The verified second-stage bootloader verifies the application firmware signature. Only if valid does it jump to the application.
Application: Runs knowing that every stage before it has been verified.
The private key — used to sign firmware images — never exists on the device. It lives on a secure signing server at the manufacturer’s facility. Signing a firmware release requires access to this server.
The practical result: even if an attacker has physical access to a device and can connect a debugger, they cannot make the device run unauthorized firmware. The cryptographic verification will reject it.
STM32 Secure Boot — SBSFU
ST provides the SBSFU (Secure Boot and Secure Firmware Update) framework for STM32 — a complete, production-ready implementation of secure boot, secure firmware update, and OTA capability.
It implements:
- Asymmetric cryptography (ECDSA) for firmware signature verification
- Symmetric cryptography (AES) for firmware image encryption
- Anti-rollback based on firmware version counters stored in OTP
- Full OTA update flow with integrity checks
ESP32 Secure Boot v2
ESP32 secure boot v2 uses RSA-PSS signature verification. The public key is burned into eFuses during manufacturing. Every firmware image must be signed with the corresponding private key. Once secure boot is enabled and eFuses are burned, the device permanently rejects unsigned firmware.
Writing Your Own Bootloader — Practical Considerations
If you’re building a product that needs OTA updates or secure boot, you’ll need a custom bootloader. Here are the practical considerations that matter.
Bootloader size budget: Your bootloader must fit within its flash partition. A minimal bootloader with UART update capability can fit in 8KB. A full-featured bootloader with crypto, OTA, and multiple interface support typically needs 32KB to 64KB.
Bootloader must never update itself: This is a critical rule. If your bootloader update process fails partway through, you have an unrecoverable device. The ROM bootloader is your recovery path — your secondary bootloader should never modify its own code.
Minimize hardware dependencies: Your bootloader should initialize only what it absolutely needs. Every peripheral you initialize in the bootloader is a potential failure point before the application even starts. Clock, flash access, and your update communication interface — nothing else.
Persistent boot state: The bootloader needs to communicate with the application across resets — storing boot flags, update status, attempt counters. Use a dedicated flash region or backup registers (RTC backup registers on STM32 survive resets without flash writes).
Watchdog in the bootloader: If your bootloader gets stuck during an update — waiting for data that never comes, stuck in an error loop — a watchdog timer will reset the device. Ensure the watchdog either resets to a safe state or is properly managed across the bootloader-to-application jump.
Timing of peripheral deinitialization: Before jumping to the application, deinitialize peripherals you initialized in the bootloader. An application that tries to initialize UART1 while it’s already initialized by the bootloader gets unexpected behavior. Alternatively, rely on the application’s initialization to simply reconfigure everything.
Bootloader Anti-Patterns — What Goes Wrong
These are the mistakes that cause real field failures:
No validity check before jumping: Jumping to application flash that contains 0xFFFFFFFF (erased flash) crashes immediately. Always verify the vector table looks sane before jumping.
Forgetting to relocate VTOR: The most common firmware jump bug. Interrupts fire into the bootloader’s handlers in the application’s context — instant hard fault.
Not clearing SysTick: SysTick firing before the application sets it up causes immediate SysTick handler execution at the wrong address.
Bootloader modifying its own flash sector: Any write to the bootloader’s own flash sector during an update operation risks corrupting the bootloader itself. Partition your flash so the bootloader can never write to its own region.
No rollback mechanism: Deploying OTA without rollback capability means a bad firmware update permanently bricks the device. Always implement rollback.
Boot loop without recovery: If the application crashes on startup, the bootloader retries it indefinitely. Without a boot attempt counter and rollback, this is an unrecoverable loop. Count boot attempts. Rollback after N failures.
Insecure update acceptance: A bootloader that accepts any firmware image over UART with no authentication is a significant security vulnerability. Any attacker with physical UART access can install malicious firmware.
The Bootloader in the Context of This Series
Look at how this episode connects everything we’ve built:
Day 66 (Interrupts) — The vector table that the bootloader reads is the same vector table that maps interrupt handlers. The first two entries — initial SP and reset handler — are what the bootloader uses to jump to the application.
Day 67 (DMA) — Production bootloaders often use DMA for high-speed firmware reception over SPI or SDMMC. Receiving a 512KB firmware image byte-by-byte over UART takes minutes; DMA makes it seconds.
Day 68 (Memory) — The entire bootloader design is built around memory layout. Flash partitioning, the linker script placing the bootloader at 0x08000000 and the application at 0x08004000, the startup code copying .data — all of this is memory architecture in action.
Day 69 (RISC-V) — The RISC-V boot sequence is conceptually identical. The machine-mode (M-mode) ROM bootloader initializes hardware and loads the next stage. The privilege transition from M-mode to S-mode (for Linux) or M-mode to U-mode (for bare-metal apps) is RISC-V’s version of the firmware jump.
Day 58 (RTOS) — The bootloader hands off to an application that may run FreeRTOS. The RTOS scheduler starts after main() — completely unaware that a bootloader ran before it. The bootloader is invisible to the RTOS.
The bootloader is not a standalone concept. It’s the thread that runs through all of embedded systems architecture — the point where hardware initialization meets software execution, where security meets convenience, where reliability meets updateability.
Wrapping Up
The bootloader is the most invisible and most essential piece of code in any embedded system.
It runs on every power cycle. It sets up the environment everything else depends on. It makes firmware updates possible without physical access. It keeps deployed devices recoverable when updates go wrong. It ensures that only authorized code ever runs on security-sensitive hardware.
Understanding it completely — the vector table reads, the startup code, the firmware jump, the OTA flow, the rollback mechanism, the secure boot chain — gives you a complete picture of embedded systems from the very first moment of power to the last instruction of your application.
We’ve crossed Day 70 of 100 Days of Tech. Seventy days of building up a complete picture of modern technology — from binary numbers and CPU architecture all the way to interrupt controllers, DMA engines, open-source chip architectures, and now the hidden code that starts it all.
The next phase begins tomorrow: embedded debugging, testing, and the tools that make reliable embedded systems possible.
메타데이터
- post_id
- 51497e3bcd35
- slug
- what-is-a-bootloader-the-hidden-code-that-starts-every-embedded-device-51497e3bcd35
- url
- https://medium.com/@ameyakshirsagar02/what-is-a-bootloader-the-hidden-code-that-starts-every-embedded-device-51497e3bcd35
- canonical_url
- https://medium.com/@ameyakshirsagar02/what-is-a-bootloader-the-hidden-code-that-starts-every-embedded-device-51497e3bcd35
- author_url
- https://medium.com/@ameyakshirsagar02
- status
- ok
- fetched_at
- 2026-08-22 08:43:29