§ 1.1 Bootloader & U-Boot
From power-on reset to start_kernel(): BIOS/UEFI handoff, U-Boot two-stage boot, Device Tree, and kernel image formats.
1. Overview
When a CPU powers on it has no OS, no filesystem, no device drivers — just ROM firmware and a program counter pointing at a fixed physical address. The boot chain progressively initializes hardware, locates the kernel image, loads it into RAM, hands it a hardware description (Device Tree), and jumps to its entry point. On x86 servers this is BIOS/UEFI → GRUB2 → vmlinuz. On embedded ARM/RISC-V boards it is ROM → SPL → U-Boot proper → kernel. Both paths converge at the same C function: start_kernel().
2. Key Data Structures
U-Boot Global Data — gd_t
The single most important U-Boot structure. A pointer to it is kept in a dedicated CPU register (r9 on ARM32, x18 on ARM64), so every driver and command can reach it without symbol lookup or function arguments.
| Field | Type | Purpose |
|---|---|---|
bd | struct bd_info * | Board info: DRAM base/size, flash geometry |
flags | unsigned long | GD_FLG_RELOC set after U-Boot relocates to top of DRAM |
relocaddr | unsigned long | Address U-Boot copied itself to in DRAM |
ram_size | phys_size_t | Total DRAM size detected during board_init_f() |
env_addr | unsigned long | Pointer to live ENV buffer (key=value\0…\0) |
fdt_blob | void * | DTB used by U-Boot itself for driver model |
new_fdt | void * | Modified DTB pointer passed to the Linux kernel |
dm_root | struct udevice * | Root of the driver-model device tree |
cur_serial_dev | struct udevice * | Active console UART device |
Device Tree Blob (DTB) — Hardware Description
A DTB is a compiled binary tree of hardware nodes and properties. U-Boot passes its physical address to the kernel in register x1 (ARM64) or r2 (ARM32). The kernel calls unflatten_device_tree() to expand it into an in-memory device_node tree that drivers probe against.
| DTB Node Path | Kernel Driver Bound | Key Property |
|---|---|---|
/cpus/cpu@0 | ARM core driver | compatible = arm,cortex-a53 |
/memory | Memory controller | reg = <base size> — physical DRAM range |
/soc/serial@addr | 8250 / ns16550 UART | compatible = ns16550a |
/soc/ethernet@addr | DWMAC / stmmac | compatible = snps,dwmac |
/chosen | (no driver — metadata) | bootargs, initrd-start/end |
Kernel Image Formats
| Format | Platform | Self-decompressing | Notes |
|---|---|---|---|
vmlinux | any | No | Raw ELF — for debugging / symbol lookup only |
vmlinuz / bzImage | x86 | Yes | Self-extracting; GRUB loads at 0x100000; entry at 0x1000000 |
zImage | ARM32 | Yes | Compressed binary; decompresses to 0x80008000 |
Image | ARM64 | No | Uncompressed; U-Boot booti validates 64-byte header |
uImage | U-Boot | Optional | Legacy: 64-byte header with load addr, entry addr, CRC |
fitImage (ITB) | U-Boot | Optional | Signed: kernel + DTB + initrd in one blob with hash nodes |
3. Core Mechanism — U-Boot Two-Stage Boot
Background: Embedded SoCs have only 192–256 KB of on-chip SRAM at power-on; the DDR controller is uninitialized so DRAM is inaccessible. The full U-Boot binary is 300–600 KB — it cannot fit. The two-stage design solves this: SPL fits in SRAM, initializes DDR, then loads U-Boot proper into DRAM.
Plan:
- ROM BootROM copies SPL from flash (NAND / SPI NOR / eMMC) into SRAM and jumps to
spl_start(). - SPL initializes PLLs (clocks) and DDR PHY. DDR training can take 10–100 ms (calibrates per-lane delays using test patterns).
- SPL calls
spl_load_image(), reads a fixed offset or partition table, and copies U-Boot proper into DRAM. - SPL calls
board_init_r()which tail-jumps to U-Boot proper's entry. SRAM is no longer needed. - U-Boot proper runs
board_init_f()(relocation), thenboard_init_r()(full init: USB, PCI, Ethernet…). - U-Boot reads ENV from flash (or TFTP), waits
bootdelayseconds, then runsbootcmd. bootcmdloads kernel + DTB into DRAM, setsbootargs, and callsbooti(ARM64) orbootz(ARM32).bootivalidates the ARM64 Image magic, sets x0 = 0, x1 = DTB address, and branches to the kernel's_text.
Example — booting an ARM64 board from eMMC:
| Step | U-Boot command | What happens |
|---|---|---|
| 1 | mmc dev 0 1 | Select eMMC device 0, boot partition 1 |
| 2 | fatload mmc 0:1 0x80200000 Image | Copy 22 MB kernel binary into DRAM at 0x80200000 |
| 3 | fatload mmc 0:1 0x80000000 rk3568.dtb | Copy 16 KB DTB into DRAM at 0x80000000 |
| 4 | setenv bootargs 'console=ttyS2,115200 root=/dev/mmcblk0p5 rw' | Store cmdline in ENV; booti copies it to /chosen/bootargs |
| 5 | booti 0x80200000 - 0x80000000 | Validate header magic 0x644d5241; set x1=0x80000000; branch |
After step 5, the CPU is in arch/arm64/kernel/head.S, MMU off, cache off, running in EL2 or EL1 depending on the firmware. The kernel will enable the MMU inside __enable_mmu (≈ 20 instructions later) and then call start_kernel().
4. Minimal C Demo
A DTB is a binary tree stored in a single contiguous blob called the Flattened Device Tree (FDT). Both U-Boot and the Linux kernel use libfdt to parse it. The demo below simulates parsing the 40-byte FDT header — the same validation logic in fdt_check_header().
U-Boot stores all persistent settings as a flat key=value\0key=value\0...\0\0 buffer in flash (with a leading CRC32). The demo below implements the same linear scan used by env_get().
5. Kernel & U-Boot Source Pointers
| File / Function | What it does |
|---|---|
arch/arm64/kernel/head.S :: _text | ARM64 kernel entry — first instruction; MMU off, sets up page tables |
arch/x86/boot/header.S :: _start | x86 real-mode entry — BIOS jumps here; starts 16-bit setup code |
arch/x86/boot/decompress.c :: decompress_kernel() | Self-decompression of bzImage; calls KASLR address selection |
drivers/of/fdt.c :: unflatten_device_tree() | Expand flat DTB blob into kernel device_node linked list |
drivers/of/fdt.c :: early_init_dt_scan() | Parse /memory and /chosen nodes before MM init |
u-boot: common/spl/spl.c :: spl_load_image() | SPL main loop: find U-Boot proper image and jump |
u-boot: common/board_f.c :: board_init_f() | First-stage init list: clock, DRAM, relocation |
u-boot: cmd/booti.c :: do_booti() | ARM64 boot: validate Image magic, set x0/x1, branch |
u-boot: lib/fdtdec.c :: fdtdec_get_addr() | Read 'reg' property from DTB node |
u-boot: env/common.c :: env_get() | Scan ENV buffer for key; return value pointer |
6. Interview Prep
| # | Question | Concise Answer |
|---|---|---|
| Q1 | Why does U-Boot need two stages (SPL + proper)? | At power-on only ≈192 KB of on-chip SRAM is available; DDR is not yet initialized. SPL fits in SRAM, initializes the DDR controller, then loads the 300–600 KB U-Boot proper into DRAM. |
| Q2 | What is a Device Tree and why does Linux need it? | A hardware-description blob (nodes + properties). Embedded SoCs have no standard bus enumeration (unlike PCI), so the bootloader passes the DTB to the kernel so it can instantiate drivers without hard-coding board layout — one kernel binary boots thousands of boards. |
| Q3 | Difference between zImage and uImage? | zImage is a self-decompressing ARM32 kernel image with no extra header. uImage wraps any image with a 64-byte U-Boot header that includes load address, entry address, OS type, compression type, and a CRC32 — so U-Boot can validate and place it correctly. |
| Q4 | How does U-Boot pass the DTB to the Linux kernel on ARM64? | booti sets register x0 = 0 (primary CPU, per ABI) and x1 = physical address of the DTB blob, then branches directly to the kernel's _text entry point. The kernel reads x1 immediately in head.S. |
| Q5 | What happens if the DTB is missing or corrupt? | unflatten_device_tree() panics with 'No DTB found' before the console is even up, so the system is silent. You need a JTAG or UART at 115200 to see early prints. On x86 a bad e820 memory map causes boot_params failures with similar silence. |