Booting embedded Linux: from reset to the first process, stage by stage
An embedded Linux that fails to boot always stops at a precise point, and that point can be deduced from the last message printed. You just need to know what should have come next. Here is the full sequence on a typical 64-bit ARM platform, with what breaks at each stage.
Stage 0: the ROM code
Out of reset, the processor runs code burned into the silicon. It initialises the bare minimum, reads configuration pins or OTP memory to find out which medium to look at, then loads a small binary into internal SRAM, because DRAM is not configured yet.
That constraint explains the intermediate stage: the first loader must fit in a few tens of kilobytes.
What breaks here: nothing is printed at all, not even one character. The causes are hardware or configuration: wrong boot selection pins, unsigned image while secure boot is enabled, header in the wrong format. Diagnosis goes through the SoC datasheet, not through software.
Stage 1: the first-level loader
On modern 64-bit ARM platforms this is TF-A, Trusted Firmware-A, in its BL2 role. It configures the DRAM controller, which is the delicate part, sets up the secure world with BL31, then loads U-Boot into DRAM and hands over.
On older 32-bit platforms that role is played by U-Boot's SPL.
What breaks here: a truncated message, or a hang after a few lines. The most frequent cause is DRAM timing, which depends on the exact memory part. A board that boots warm but not cold, or one in ten only, almost always points to marginal memory calibration.
Stage 2: U-Boot
U-Boot has the full DRAM. It initialises the peripherals needed for loading, network, eMMC, SD, loads the kernel, the devicetree and possibly an initramfs, then builds the boot arguments and jumps into the kernel.
Two details matter. The bootargs variable carries the command line, including the console and the root filesystem. And U-Boot can modify the devicetree on the fly before passing it, which explains some differences between the compiled file and what the kernel sees.
What breaks here: the U-Boot prompt appears but the kernel does not start. Check the load addresses; a kernel and a devicetree overlapping in memory produce a silent crash. bdinfo and printenv give you most of the answer.
Stage 3: decompression and kernel entry
The 64-bit ARM kernel is usually a compressed Image. The decompression code runs, then the kernel starts with the devicetree pointer in register x0.
This is the most disconcerting silence: between U-Boot's jump and the first kernel messages there is no output at all until the console is initialised.
What breaks here: U-Boot says "Starting kernel..." and nothing follows. Three causes dominate. The console named in bootargs does not match the hardware, in which case the kernel runs but stays mute; enable earlycon to get output very early. The devicetree does not match the SoC, and the kernel panics before the console exists. Or the decompression address overwrites something.
The earlycon option is the first reflex; it turns silence into a diagnosis.
Stage 4: the kernel, up to mounting root
The kernel initialises virtual memory, brings up secondary cores, probes the peripherals declared in the devicetree, mounts the root filesystem, then runs the first process.
What breaks here: the well-known Kernel panic - not syncing: VFS: Unable to mount root fs. The cause is almost always one of three. The storage driver is not built into the kernel, it is a module, therefore unavailable before root is mounted. The root= parameter names a partition that does not exist; on eMMC the numbering shifts with enumeration order, which makes root=PARTUUID= more reliable than root=/dev/mmcblk0p2. Or the filesystem is not the expected type.
Stage 5: PID 1
The kernel runs /sbin/init, or whatever init= names. Depending on the system that is BusyBox init, systemd, or a script.
What breaks here: Kernel panic - not syncing: Attempted to kill init!. The binary does not exist, is not executable, or is missing a shared library. On a hand-built root filesystem the classic omission is the dynamic linker. The decisive test is booting with init=/bin/sh: if you get a shell, the root filesystem is sound and the problem is in init.
Quick diagnosis table
| Symptom | Stage | First check |
|---|---|---|
| No character at all | ROM | Boot select pins, signature, header format |
| A few lines then a hang | TF-A or SPL | DRAM calibration, cold-boot behaviour |
| U-Boot prompt but no kernel | U-Boot | Load addresses, memory overlap |
| "Starting kernel" then silence | Kernel entry | earlycon, console in bootargs, devicetree |
Unable to mount root fs | Kernel | Driver built in, root=, filesystem type |
Attempted to kill init | PID 1 | init=/bin/sh, missing libraries |
Three options that save hours
earlycon prints kernel output before the console is fully initialised. Always enable it while bringing a board up.
initcall_debug traces every init function with its duration. Essential when boot is slow rather than stuck, and to spot a driver waiting on a timeout.
ignore_loglevel forces every message to be shown, including those the default level hides, and often reveals a warning that explains the next hang.
Why the build system changes the diagnosis
On a Yocto or Buildroot image, every stage is reproducible and you can rebuild a single component. On an image received from a vendor without sources, you have neither kernel symbols nor the exact configuration, and diagnosis is limited to observation.
That argument is often overlooked when choosing between taking the vendor BSP as is and rebuilding the chain: the ability to diagnose a boot failure is a property of the build system, not of the product.
References
- Trusted Firmware-A, BL2 and BL31 roles
- U-Boot documentation,
bootargsand load addresses - Kernel.org, Kernel parameters,
earlycon,initcall_debug,ignore_loglevel,root= - Kernel.org, Booting AArch64 Linux, kernel entry state and register x0
Going further
This sequence makes sense once you have walked it on a real board, deliberately breaking each stage to see the resulting message. That is what our embedded Linux courses and our Linux drivers course cover.