If you’ve taken our free Linux development course on cross toolchains, you already know how to produce a binary
for your target board. But how does that binary — or more importantly, the Linux kernel itself — actually start
running on real silicon? The answer is the bootloader, and understanding an embedded Linux
bootloader is the natural next step after building your toolchain. In this lecture we start our free embedded
systems course chapter on bootloaders by looking at exactly what a bootloader is responsible for, what the CPU
sees the instant power is applied, and why a “load and jump” model — not the kernel itself — is always the very
first software on the board.
reset vector
embedded Linux bootloader
free embedded systems course
free linux development course
power-on reset
What You Will Learn
- The two core jobs every embedded Linux bootloader performs
- What state the CPU and memory are actually in at power-on
- Why the bootloader — not the kernel — has to run first
- How a reset vector redirects execution to bootloader code
- A hands-on demo: building a tiny ARM stub and inspecting its reset vector
Prerequisites
- Comfortable with basic C and the command line
- A working cross toolchain (see our free linux development course toolchain chapter) —
arm-none-eabi-gccor similar - No prior bootloader experience needed
The Two Jobs of a Bootloader
Strip away every board-specific detail and an embedded Linux bootloader really only has two jobs:
- Bring up just enough hardware to make the next stage runnable.
- Load and hand off control to whatever comes next — another bootloader stage, or finally the Linux kernel.
Notice that the first job exists purely in service of the second. The bootloader doesn’t initialize hardware
because initialization is inherently valuable — it initializes exactly as much hardware as the loading step needs,
and not one peripheral more. This “minimum viable hardware” philosophy is what makes bootloader code so different
from application code: every line has to justify its existence in a memory- and time-constrained environment.
The State of the World at Power-On
It’s worth spending a moment appreciating just how little is available when the very first instruction executes.
Immediately after a power-on or reset event:
- The DRAM controller has not been configured, so external RAM is not addressable yet
- Storage controllers (NAND, MMC/SD, SPI flash) are not configured, so nothing can be read from mass storage
- Typically only a single CPU core is running, executing from a small on-chip memory
| AT POWER-ON (before bootloader runs) | | AFTER BOOTLOADER’S EARLY PHASE COMPLETES |
+————————————————–+ +————————————————–+
| [CPU core 0] running, executing from on-chip ROM| | [CPU core 0] running application-class code |
| [DRAM ctrl] NOT configured — DRAM inaccessible| | [DRAM ctrl] configured — DRAM fully usable |
| [NAND/MMC/SPI] NOT configured — storage unusable | | [Storage] configured — kernel image readable|
| [Other cores] held in reset / not started | | [Other cores] may now be released (SMP boot) |
+————————————————–+ +————————————————–+
Because so little works at first, real-world bootstrap is almost never a single leap from “power applied” to
“kernel running.” Instead it’s a sequence of small stages, each one bringing a bit more of the system online until
there’s enough working hardware to load and start the kernel. We’ll map out that full multi-stage sequence in the
next lecture — for now, the important idea is simply that the bootloader’s early phase ends the moment the kernel
image and the memory to hold it are both reachable.
Handing Off to the Kernel
Once main memory and whatever interface holds the kernel image (storage or network) are working, the bootloader’s
final act is straightforward in concept: copy the kernel into RAM and transfer control to it. The exact mechanics —
what registers are set, what data structure describes the hardware, how the kernel command line is passed — are
architecture-specific, and we’ll cover those details (including the device tree) later in this course. What matters
here is the handoff itself: once the kernel begins executing, it never returns to the bootloader, and every byte of
memory the bootloader was using becomes free for the kernel and applications to reclaim.
Maintenance Mode: The Bootloader’s Second Life
Most embedded Linux bootloaders don’t just boot once and disappear — they also expose a maintenance mode, usually
reachable through a command-line interface over a serial console. This mode lets a developer or field technician:
- Flash a new kernel or filesystem image without booting Linux first
- Inspect or modify boot configuration (environment variables, boot order)
- Run low-level diagnostics on memory or storage
This is a subsidiary job compared to loading the kernel, but it’s the feature you’ll interact with most often
during development, since it’s usually how you get a new kernel image onto the board in the first place.
Hands-On: Watching a Reset Vector Redirect Execution
Let’s make the “load and jump” idea concrete with a tiny, original example. We’ll write a minimal ARM stub that
represents nothing more than a reset vector containing a jump instruction, build it with the cross toolchain from
our earlier chapter, and then disassemble it to see the jump for ourselves.
/* ep_reset_stub.S -- a minimal illustration of a reset-vector jump */
.syntax unified
.cpu cortex-a9
.text
.global _reset_vector
_reset_vector:
b ep_start @ jump instruction at the reset vector
.global ep_start
ep_start:
ldr r0, =ep_start @ pretend "bootloader" entry point
loop:
b loop @ spin here -- stands in for real init code
Assemble and link it, placing the vector at a fixed address the way a real boot ROM would expect:
$ arm-none-eabi-as ep_reset_stub.S -o ep_reset_stub.o
$ arm-none-eabi-ld -Ttext=0x00000000 ep_reset_stub.o -o ep_reset_stub.elf
$ arm-none-eabi-objdump -d ep_reset_stub.elf
Expected output:
ep_reset_stub.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_reset_vector>:
0: ea000000 b 8 <ep_start>
00000004 <ep_start>:
4: e59f0000 ldr r0, [pc, #0]
8: eafffffe b 8 <loop>
That single b 8 <ep_start> instruction at address 0x00000000 is, conceptually, exactly
what sits at a real reset vector: the hardware’s program counter starts at a fixed address, and the very first
instruction it fetches immediately redirects execution into the bootloader’s actual entry point. Everything a
bootloader does — from the earliest ROM stage to the final kernel handoff — starts with a jump just like this one.
Common Mistakes and Troubleshooting
| Mistake | Why It Happens | Fix |
|---|---|---|
| Assuming DRAM is usable immediately | Application-level habits carry over from hosted environments | Remember DRAM requires controller init first — nothing in that phase can use heap/stack in DRAM |
| Linking code to the wrong load address | Forgetting the reset vector is a fixed hardware address | Check the SoC reference manual for the actual reset address before linking |
| Confusing the bootloader with the kernel | Both are “low-level” code, easy to conflate | Remember: the bootloader’s job ends the moment it jumps into the kernel and never returns |
Best Practices
- Keep early-stage bootloader code as small and dependency-free as possible — you don’t have a working heap yet
- Always verify what your SoC’s reset vector address and initial CPU mode actually are before writing any stub code
- Treat the bootloader’s maintenance/console mode as a first-class development tool, not an afterthought
Summary and Key Takeaways
- A bootloader has exactly two jobs: bring up minimal hardware, then load and hand off to the next stage
- At power-on, DRAM and storage controllers are not yet configured — only on-chip resources are available
- The handoff to the kernel is a one-way jump; the bootloader is never returned to
- Maintenance mode is the bootloader’s ongoing developer-facing feature, separate from the boot path itself
Conclusion
Every embedded Linux system, no matter how simple or complex its final boot sequence turns out to be, is built on
this same foundation: a reset vector, a jump, and a bootloader whose only real purpose is to get just enough
hardware working to load what comes next. In the next lecture of this free embedded systems course, we’ll build on
this foundation and walk through the full multi-stage boot sequence used by modern SoCs — ROM code, secondary
program loaders, and how they cooperate to finally reach a full bootloader capable of starting Linux.
Frequently Asked Questions
What is the main job of an embedded Linux bootloader?
Its main job is to initialize just enough hardware to load the next stage — ultimately the Linux kernel — into
memory and transfer control to it.
Is the bootloader part of the Linux kernel?
No. The bootloader is entirely separate software that runs before the kernel and hands off control to it. Once
the kernel starts, the bootloader is never invoked again.
Why can’t the kernel run directly at power-on?
Because at power-on, DRAM and storage interfaces are not yet configured, and the kernel is far too large and
complex to run from the tiny amount of on-chip memory available immediately after reset.
What is a reset vector?
It’s a fixed memory address the CPU is hardwired to fetch its first instruction from after a reset or power-on
event. That instruction typically jumps into the actual start of bootloader code.
What is bootloader maintenance mode used for?
It provides a command-line interface, usually over serial, for flashing new images, inspecting boot
configuration, and running diagnostics without needing Linux to be running.
Do all SoCs boot the same way?
No. The high-level two-job model is universal, but the exact number of stages and mechanisms are SoC-specific —
we cover this variation in the next lecture.
Does the bootloader ever run again after Linux boots?
Typically not until the next reset or power cycle. Its memory is reclaimed once the kernel takes over.
Ready for the Full Boot Sequence?
Continue this free embedded systems course with the next lecture, where we walk through every stage of a
modern SoC boot sequence — ROM code, SPL, and beyond.
