R0KernelPath
module 054 / 262

The Boot Process: Firmware to init

Trace the exact path from electrical power to PID 1 — every stage, every memory address, every handoff — so you understand what the kernel inherits and why it is designed to start from nothing.

~18h focused study11 concepts

Overview

When you press the power button on a Linux machine, something almost miraculous happens: within seconds, a sophisticated operating system is fully alive and ready to serve you. But the machine starts in a state of near-total ignorance — the CPU has no idea what program to run, what the disk looks like, or even how much RAM exists. Everything must be discovered and bootstrapped from scratch, one carefully ordered step at a time.

This module traces that journey from the very first electrical signal to the moment the first user-space process takes over. We will see how the firmware initializes hardware, how a bootloader finds and loads the kernel, how the kernel wakes up and builds its own world, and how the first process — PID 1 — takes the baton and brings the rest of the system to life.

Understanding the boot process is not just trivia. It is the foundation for debugging systems that refuse to start, writing embedded firmware, understanding security vulnerabilities (Secure Boot, bootkits), and comprehending why the Linux kernel is designed the way it is. Every architectural decision in the kernel — from the memory map to the initramfs design — makes more sense once you have seen the full picture.

We will move carefully through each stage, armed with real code, real memory addresses, and a healthy respect for the fact that at the very beginning, the hardware itself is barely functional — and someone has to teach it to think.

why this matters for the kernel

Kernel contributors regularly need to touch code that runs at or near boot time: early printk infrastructure, device tree parsing, initcall ordering, early memory allocators (memblock), and driver probe sequences all live in the boot path. If you do not understand the sequence in which the kernel initializes itself, you will write patches that panic the machine or silently corrupt early-boot state. More practically, when a kernel patch breaks boot on a hardware platform, you are the person who has to read the early console output and know exactly which phase failed.

Beyond kernel contribution, the boot process is a microcosm of all of systems programming: real-mode vs. protected-mode x86, ELF binary loading, page-table construction from scratch, filesystem mounting without a filesystem driver, and process creation from nothing. Every one of these topics appears again later in deeper form. Students who understand boot deeply find that memory management, process scheduling, and driver models all click into place much faster, because they have already seen the ground floor being poured.

interactive visualization

Six handoffs take a cold chip to a running Linux system. Click each stage.

The Full Linux Boot Chain

rendering diagram…

Each arrow represents a transfer of control — from firmware to bootloader to kernel to userspace. The kernel itself (start_kernel) spawns PID 1 via exec, the only time in kernel history when a kernel thread becomes a userspace process.

The concepts

01Power-On and the CPU Reset Vector

When power is applied, the CPU does not start executing arbitrary code. It has a hardwired reset vector — a fixed physical address baked into silicon that the program counter (instruction pointer) is set to on reset. On x86/x86-64 processors, this address is 0xFFFFFFF0, just 16 bytes below the top of the 32-bit address space. The CPU starts in real mode (a 16-bit mode inherited from the 8086), but thanks to a special trick called the "real-mode memory hole", the memory controller maps that address to the firmware ROM chip.

Real mode means the CPU can only address 1 MB of memory (20-bit addresses, formed by segment × 16 + offset). There are no memory protection rings, no virtual memory, and no concept of processes. You are running directly on bare metal with essentially the capabilities of a 1981 IBM PC.

On ARM processors, the reset vector is typically at 0x00000000 or a configurable exception base address. On RISC-V, the reset vector is implementation-defined but commonly 0x80000000 for machines with DRAM starting there, or a mask ROM address for SoCs.

The key insight: the CPU is stateless at reset. Every register is in a known but useless state. The CPU blindly fetches the instruction at the reset vector and begins executing. At that moment, only one tiny piece of non-volatile memory — the firmware ROM — contains any executable code.

/* What the CPU "sees" conceptually at reset on x86.
   Physical address 0xFFFFFFF0 in ROM contains a far JMP
   instruction that sends execution into the main BIOS/UEFI code. */

/* In real mode, a physical address = (CS << 4) + IP.
   At reset: CS = 0xF000, IP = 0xFFF0
   Physical address = 0xF000 << 4 + 0xFFF0 = 0xFFFF0
   (This is the BIOS entry in the classic 1MB map.)

   On modern systems, the chipset temporarily maps 0xFFFFFFF0
   to firmware ROM until the firmware remaps things. */

; Conceptual x86 real-mode far jump at reset vector:
; db 0xEA         ; far JMP opcode
; dw 0xE05B       ; offset into segment
; dw 0xF000       ; segment = 0xF000 (the BIOS ROM segment)

02BIOS: Legacy Firmware and the POST

BIOS (Basic Input/Output System) is the legacy x86 firmware standard, unchanged in concept since 1981. When the CPU jumps to the reset vector, the BIOS code begins the Power-On Self-Test (POST):

  1. CPU test: verify the CPU itself works (registers, arithmetic).
  2. Memory detection: probe RAM by writing and reading test patterns to identify how much RAM exists and whether it is functional.
  3. Hardware enumeration: discover PCI/PCI-Express devices, initialize the memory controller, set up interrupt controllers (PIC/APIC).
  4. Video init: bring up the display so error messages can appear.
  5. Storage detection: probe IDE/SATA/NVMe controllers and find bootable devices.

After POST, BIOS provides a set of interrupt-based services (INT 10h for video, INT 13h for disk, INT 15h for memory maps, etc.) that early bootloaders use. These are called in real mode by triggering software interrupts.

The BIOS then searches for a bootable disk. On each candidate disk, it reads the first 512 bytes — the Master Boot Record (MBR) — into memory at physical address 0x7C00 and checks that the last two bytes are 0x55 0xAA (the boot signature). If found, the BIOS jumps to 0x7C00 and execution passes to the bootloader. The BIOS itself is now largely irrelevant — it provided its services, loaded the first stage, and stepped aside.

The MBR contains a tiny bootloader (446 bytes max — the rest holds the partition table), which is far too small to load a full OS. This is why bootloaders have multiple stages.

/* Minimal MBR bootloader (x86 real mode assembly).
   Loaded at 0x7C00. Prints 'B' to the screen and halts.
   Compiled with: nasm -f bin -o mbr.bin mbr.asm */

BITS 16
ORG 0x7C00

start:
    mov ah, 0x0E      ; BIOS teletype output function
    mov al, 'B'       ; character to print
    int 0x10          ; call BIOS video service

.halt:
    hlt               ; halt the CPU
    jmp .halt         ; in case of NMI, halt again

    TIMES 510-($-$$) DB 0   ; pad to 510 bytes
    DW 0xAA55               ; boot signature (little-endian)

03UEFI: Modern Firmware

UEFI (Unified Extensible Firmware Interface) replaced BIOS in mainstream systems starting around 2010. It is a complete miniature operating system in firmware, with its own executable format (PE/COFF — the same format Windows uses), a rich API, a filesystem driver (FAT32), network stack, and even a shell.

Key differences from BIOS:

No more MBR magic. UEFI uses the GPT (GUID Partition Table) disk format and a dedicated EFI System Partition (ESP) — a FAT32 partition where bootloaders are stored as ordinary .efi files. On a 64-bit system, UEFI looks for /EFI/BOOT/BOOTX64.EFI by default.

64-bit from the start. UEFI starts in a 32 or 64-bit mode ("Protected Mode" or "Long Mode"), skipping the real-mode phase entirely. No INT 13h disk tricks.

Boot Services and Runtime Services. UEFI firmware exports two categories of functions: Boot Services (available only before the OS takes over — memory allocation, file I/O, device handles) and Runtime Services (available even after the OS starts — setting system time, reading NVRAM variables). A crucial event is ExitBootServices(), the moment the bootloader tells UEFI "I'm done using your services; hand me the hardware." After this call, UEFI is essentially out of the picture.

Secure Boot. UEFI includes a signature-verification mechanism. Each .efi binary must be signed by a key in the firmware's trusted key database. GRUB and the Linux kernel can be signed, creating a chain of trust from firmware to kernel.

The UEFI specification is published by the UEFI Forum and is over 2,000 pages. Firmware vendors implement it; you should think of UEFI as a well-specified contract between hardware and software.

/* Minimal UEFI application in C (using GNU-EFI library).
   Prints "Hello from UEFI" to the firmware console.
   Build: x86_64-w64-mingw32-gcc or with gnu-efi toolchain */

#include <efi.h>
#include <efilib.h>

EFI_STATUS
UEFI_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    InitializeLib(ImageHandle, SystemTable);  /* gnu-efi helper */
    Print(L"Hello from UEFI\n");             /* wide-char string */
    return EFI_SUCCESS;
}

04GRUB: The Grand Unified Bootloader

GRUB2 (GRand Unified Bootloader, version 2) is the most common Linux bootloader. Its job sounds simple — load the kernel — but the reality involves several stages and a remarkable amount of complexity.

Why multiple stages? GRUB must be able to read a filesystem (ext4, Btrfs, XFS, etc.) to find the kernel image (/boot/vmlinuz). But at the point GRUB runs, the kernel is not yet loaded. GRUB must contain its own filesystem drivers. These cannot fit in 446 bytes, so GRUB splits into stages:

  • Stage 1 (the MBR, or the UEFI .efi file): tiny, just enough to find and load Stage 1.5/2.
  • Stage 1.5 / core.img: embedded in the 30KB gap between the MBR and the first partition on MBR-disks, or as a module on UEFI. Contains the minimal filesystem driver to read the /boot/grub/ directory.
  • Stage 2: the full GRUB environment, loaded from /boot/grub/. Has a scripting language, menu, command line, module loader, and all filesystem drivers.

The GRUB menu lets you choose which kernel to boot. The configuration is in /boot/grub/grub.cfg, auto-generated by update-grub or grub-mkconfig. A typical entry looks like:

menuentry 'Linux 6.8' {
    linux   /boot/vmlinuz-6.8 root=/dev/sda2 ro quiet
    initrd  /boot/initramfs-6.8.img
}

The linux command loads the kernel image into memory; the initrd command loads the initial RAM disk. The kernel command line (everything after the path in the linux line) is passed to the kernel as its argument string.

Finally, GRUB calls the kernel's entry point. On x86-64, this means switching the CPU to 64-bit long mode (if not already there under UEFI), setting up a minimal stack and segment registers, and jumping to the kernel's start symbol. GRUB's job is done.

# /boot/grub/grub.cfg excerpt — what GRUB parses
# Generated by grub-mkconfig; do not edit by hand.

set default=0
set timeout=5

menuentry 'Ubuntu 24.04 LTS, kernel 6.8.0-31' {
    # 'linux' loads the kernel image + sets cmdline
    linux   /boot/vmlinuz-6.8.0-31-generic \
            root=UUID=3a1b2c3d-... ro quiet splash
    # 'initrd' loads the initial ramdisk
    initrd  /boot/initrd.img-6.8.0-31-generic
    # GRUB jumps to the kernel entry point after 'boot'
}

05The Kernel Image: vmlinux, vmlinuz, bzImage

The Linux kernel is an ELF binary at heart — but what GRUB loads is not a plain ELF file. It is a compressed, self-extracting executable with a special header. Understanding the naming is the first step:

  • vmlinux: the raw, uncompressed ELF kernel binary produced by make. Contains full debug symbols. Too large to load directly on most systems. Lives in the build directory, not in /boot.
  • vmlinuz: the compressed kernel. The z hints at compression (gzip historically, now often LZ4 or zstd). A generic name for "compressed vmlinux".
  • bzImage: the "big zImage" — the standard x86 bootable kernel image. It has a small setup header at the start (at offset 0x1F1 in the file) that tells the bootloader about the kernel's requirements (memory layout, load address, protocol version). The rest is a compressed payload containing the real kernel.

Kernel boot protocol (x86): GRUB reads the setup header, places the compressed kernel body at a suitable physical address (usually 0x100000 = 1 MB for the protected-mode code, though modern kernels support KASLR randomization), places the kernel command line string somewhere, and jumps to the kernel's 32-bit or 64-bit entry point.

The first thing the kernel does is decompress itself — the startup code in arch/x86/boot/compressed/head_64.S calls extract_kernel(), which uses the embedded decompressor to inflate the real kernel image into memory. Only then does the real start_kernel() function run.

This self-extracting design means the bootloader does not need to know anything about the kernel's internal format — it just needs to know the ABI described in the setup header (Documentation/arch/x86/boot.rst in the kernel tree).

/* Kernel setup header fields (arch/x86/include/uapi/asm/bootparam.h).
   The bootloader reads/writes these fields at specific offsets
   in the bzImage file. This is the x86 boot protocol ABI. */

struct setup_header {
    __u8  setup_sects;       /* number of setup sectors */
    __u16 root_flags;
    __u32 syssize;           /* size of protected-mode kernel */
    __u16 ram_size;
    __u16 vid_mode;
    __u16 root_dev;
    __u16 boot_flag;         /* must be 0xAA55 */
    __u16 jump;              /* first instruction: short jmp */
    __u32 header;            /* magic: 0x53726448 = "HdrS" */
    __u16 version;           /* boot protocol version */
    /* ... many more fields ... */
    __u64 pref_address;      /* preferred load address */
    __u32 init_size;         /* space needed for decompression */
} __attribute__((packed));

06initramfs: The Temporary Root Filesystem

Here is a chicken-and-egg problem: to mount the real root filesystem (say, an ext4 partition on an NVMe drive), the kernel needs the NVMe driver. But where do the drivers come from? Normally from the filesystem. And the filesystem is on the NVMe drive that needs the driver.

The solution is initramfs (initial RAM filesystem). It is a compressed cpio archive containing a minimal filesystem: a few directories, a BusyBox (or systemd-based) userspace, and — crucially — the kernel modules needed to mount the real root filesystem.

How it works:

  1. GRUB loads the initramfs archive into RAM alongside the kernel image.
  2. The kernel extracts the archive into a temporary in-memory filesystem (a tmpfs or the special rootfs).
  3. The kernel executes /init (or /sbin/init) from within this temporary filesystem.
  4. This init script (or binary) loads drivers, activates LVM, assembles RAID arrays, decrypts LUKS volumes — whatever is needed to access the real root device.
  5. The script calls pivot_root or switch_root to replace the temporary filesystem with the real root filesystem.
  6. The real /sbin/init (or systemd) is then executed, completing the handoff.

Why not just bake the drivers into the kernel? You can, but it makes the kernel enormous and non-portable. The same kernel binary can boot on wildly different hardware by customizing the initramfs. This is how Linux distributions ship a single kernel that works on laptops, servers, and VMs.

The initramfs is built by tools like mkinitramfs (Debian) or dracut (Red Hat/Fedora). You can inspect it:

cpio -itv < /boot/initrd.img | head -40
#!/bin/sh
# Simplified /init script inside an initramfs.
# The real ones (from dracut/mkinitramfs) are far more complex.

# Mount essential virtual filesystems
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev

# Load the storage driver needed to see the disk
modprobe nvme

# Wait for the device to appear
until [ -b /dev/nvme0n1p2 ]; do sleep 0.1; done

# Mount the real root filesystem (read-only at first)
mount -o ro /dev/nvme0n1p2 /mnt/root

# Switch to the real root and exec the real init
exec switch_root /mnt/root /sbin/init

07start_kernel(): How the Kernel Initializes Itself

start_kernel() in init/main.c is one of the most important functions in all of systems software. It is the first C function the Linux kernel calls after the early architecture-specific assembly setup. Everything from here is portable C (mostly).

The call sequence is a careful ordering of dependencies. You cannot set up the scheduler before you have memory allocation. You cannot have memory allocation before you know what memory exists. The order is roughly:

  1. setup_arch(): architecture-specific initialization — parse the device tree (ARM/RISC-V) or ACPI tables (x86), detect CPU type and features, set up the early memory map.
  2. mm_init() / mem_init(): initialize the page allocator (the buddy allocator), replace the early memblock allocator that was used before the full page allocator was ready.
  3. sched_init(): set up the task scheduler data structures.
  4. trap_init(): install the interrupt descriptor table (IDT on x86) — now the CPU can handle exceptions and hardware interrupts.
  5. time_init(): set up timekeeping — the hardware timer (TSC, HPET, clocksource).
  6. console_init(): register the early console — printk output starts appearing on the screen.
  7. rest_init(): spawn kernel thread 1 (which becomes init, the ancestor of all user processes), then become the idle thread (PID 0).

The initcall mechanism (the module_init(), fs_initcall(), device_initcall() macros) creates a table of function pointers in a special ELF section. do_initcalls() walks this table in priority order, invoking every registered initialization function. This is how drivers, filesystems, and subsystems announce themselves to the kernel without requiring explicit calls in start_kernel().

/* Simplified view of start_kernel() in init/main.c.
   The real function is ~100 lines. This shows the key ordering. */

asmlinkage __visible void __init start_kernel(void)
{
    /* Very early: set up kernel stack canary, lockdep, etc. */
    set_task_stack_end_magic(&init_task);

    /* Architecture setup: CPU features, early memory map */
    setup_arch(&command_line);

    /* Build the memory page allocator */
    mm_init();

    /* Set up the process scheduler */
    sched_init();

    /* Install interrupt/exception handlers */
    trap_init();

    /* Start timekeeping */
    time_init();

    /* Now printk works reliably */
    console_init();

    /* Run all registered init functions (drivers, filesystems...) */
    do_initcalls();

    /* Mount the initramfs / root filesystem, exec /sbin/init */
    rest_init();  /* spawns kthread -> kernel_init() */

    /* start_kernel() never returns; becomes the idle thread */
    cpu_startup_entry(CPUHP_ONLINE);
}

08PID 1: From kernel_init() to Userspace

After rest_init() creates kernel thread 1, that thread runs kernel_init(). This function completes the transition from pure kernel world to userspace:

  1. Mount the root filesystem: using the root= kernel parameter, the kernel mounts the real root device (or stays on initramfs if rootfstype=ramfs).
  2. Run /init from initramfs: if an initramfs was provided, it is unpacked and /init is executed as the first userspace process (PID 1). This /init is typically a shell script or systemd compiled into the initramfs.
  3. switch_root: after the initramfs init prepares the real root, switch_root (from util-linux) deletes the initramfs, moves the real root to /, and execs the real /sbin/init.
  4. PID 1 as the true init: the resulting process — whether it is traditional SysV init, upstart, OpenRC, or systemd — is PID 1. It is special: it is the ancestor of every user process, it cannot be killed (sending SIGKILL to PID 1 is ignored), and it is responsible for reaping orphan processes (zombie collection).

kernel_init() tries several fallback commands to find a valid init: init= kernel parameter, then /sbin/init, /etc/init, /bin/init, /bin/sh. If none of these can be executed, the kernel panics with "No init found" — one of the most instructive kernel panics, because it means the kernel is perfectly healthy but cannot find any userspace to run.

The call kernel_execve() at the end of kernel_init() is profound: it replaces the kernel thread's address space with a userspace process image. The kernel thread becomes the init process. From this moment, the kernel never "runs" in the traditional sense of a program; it only runs when userspace makes system calls or hardware raises interrupts.

/* Simplified kernel_init() path — init/main.c */

static int __ref kernel_init(void *unused)
{
    /* Try to run userspace init from initramfs */
    if (ramdisk_execute_command) {
        if (!run_init_process(ramdisk_execute_command))
            return 0;  /* success: we are now the init process */
    }

    /* Try locations from the 'init=' kernel parameter */
    if (execute_command) {
        if (!run_init_process(execute_command))
            return 0;
        panic("Requested init %s failed.", execute_command);
    }

    /* Try standard locations in order */
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init")  ||
        !try_to_run_init_process("/bin/init")  ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found. Try passing init= option "
          "to kernel. See Linux Documentation/admin-guide/"
          "init.rst for guidance.");
}

09systemd: Modern PID 1

On most modern Linux distributions, PID 1 is systemd. It is not a simple script runner — it is a sophisticated service manager that parallelizes system startup aggressively using dependency graphs.

Core concepts:

  • Units: systemd manages "units" — files describing services (.service), mount points (.mount), sockets (.socket), timers (.timer), and more. Unit files live in /lib/systemd/system/ (distribution) and /etc/systemd/system/ (administrator overrides).
  • Targets: groups of units, analogous to SysV runlevels. multi-user.target is the normal fully-booted state; graphical.target adds a desktop environment. The default target is a symlink at /lib/systemd/system/default.target.
  • Dependency graph: Requires=, Wants=, After=, Before= directives form a DAG. systemd computes which units can start in parallel and which must wait.
  • Cgroups integration: every service is placed in its own cgroup, enabling reliable process tracking and resource limiting.
  • Journal: systemd runs journald, which collects logs from all services into a structured binary log queryable with journalctl.

Boot sequence with systemd:

  1. Kernel starts systemd as PID 1.
  2. systemd reads its configuration and resolves the dependency graph for the default target.
  3. Units start in parallel where dependencies allow.
  4. sysinit.target completes basic OS setup (mounting filesystems, loading modules).
  5. basic.target starts timers and sockets.
  6. multi-user.target starts network, login manager, etc.
  7. graphical.target (if applicable) starts the display manager.

You can observe this entire process with systemd-analyze and systemd-analyze plot to generate an SVG timeline of exactly how long each service took to start and what was blocking what.

# Example systemd service unit file
# /etc/systemd/system/myapp.service

[Unit]
Description=My Application
After=network.target        # wait for network
Requires=network.target     # fail if no network

[Service]
Type=simple
ExecStart=/usr/bin/myapp --config /etc/myapp.conf
Restart=on-failure          # restart if it crashes
User=myapp                  # run as non-root user
ProtectSystem=strict        # read-only filesystem view
PrivateTmp=true             # isolated /tmp

[Install]
WantedBy=multi-user.target  # enable as part of normal boot

# Commands:
# systemctl enable myapp     # create symlink so it starts on boot
# systemctl start myapp      # start it now
# journalctl -u myapp -f     # follow its logs

10The Memory Map During Boot

During the boot process, the physical memory map changes radically at each stage. Understanding this is essential for debugging boot issues and for kernel development.

Physical memory at BIOS/UEFI time (x86):

  • 0x00000 – 0x003FF: Real Mode Interrupt Vector Table (1 KB)
  • 0x00400 – 0x004FF: BIOS Data Area
  • 0x07C00 – 0x07DFF: MBR bootloader (512 bytes)
  • 0x0A000 – 0x0FFFF: Video RAM and BIOS ROM (upper memory area)
  • 0x100000+: Extended memory (RAM)

After GRUB loads the kernel (typical layout):

  • 0x100000 (1 MB): compressed kernel image
  • Somewhere above 1 MB: initramfs
  • Near top of usable RAM: kernel command line string

After kernel decompression: The kernel uses memblock — a simple early memory allocator that just tracks ranges of physical memory as "available" or "reserved". Before the buddy allocator is up, all early allocations go through memblock.

KASLR (Kernel Address Space Layout Randomization): Modern kernels are not loaded at a fixed physical address. The decompressor picks a random base address within the available physical RAM (aligned to 2 MB). This makes it harder for attackers to predict kernel addresses. The virtual address at which the kernel maps itself is also randomized.

Physical vs virtual addresses: Once the kernel sets up its own page tables (in setup_arch()), it runs at virtual addresses (on x86-64, the kernel is mapped at 0xFFFFFFFF80000000 and above in a direct physical mapping region). Physical addresses below 1 MB are still accessible via the 1:1 mapping for legacy reasons.

After free_initmem() runs, the memory used for the kernel's own __init sections is released back to the page allocator — those sections only needed to exist during boot.

/* In the kernel, the __init annotation marks functions
   that are only needed during initialization.
   After boot, this memory is freed by free_initmem(). */

/* arch/x86/kernel/setup.c */
void __init setup_arch(char **cmdline_p)
{
    /* This entire function runs once during boot,
       then its memory is reclaimed. */
    detect_memory();      /* use BIOS/UEFI memory map */
    init_mem_mapping();   /* build initial page tables */
    /* ... */
}

/* You can see this in dmesg: */
/* "Freeing unused kernel image (initmem) memory: 2652K" */

11Early Kernel Console and dmesg

One of the most powerful debugging tools for boot issues is dmesg — the kernel's ring buffer of messages from boot time forward. But how do messages appear before any filesystem is mounted or any terminal is set up?

printk is the kernel's equivalent of printf. Unlike userspace printf, it works without the C library, without malloc, and without a process context. It writes to an in-memory ring buffer (typically 512 KB or configurable via LOG_BUF_SHIFT). Each message has a log level (0=KERN_EMERG through 7=KERN_DEBUG).

Early console drivers (earlycon): even before console_init(), the kernel can output messages through a hardware UART or VGA text buffer using the earlycon= kernel parameter. This uses a simplified driver registered before the full driver model is ready.

earlyprintk / earlycon: adding earlycon=uart8250,mmio,0x... or just earlyprintk to the kernel command line enables output from the very first printk call, before even the normal console drivers are loaded. This is invaluable when debugging panics that happen early in start_kernel().

Reading dmesg output is a skill. Key things to look for:

  • Booting Linux on physical CPU — confirms kernel start
  • Memory: Xk available — memblock result
  • PCI: Probing PCI hardware — device enumeration
  • VFS: Mounted root — root filesystem mounted
  • Run /sbin/init as init process — userspace starting

For kernel developers, dmesg -T --level=err,warn is your first stop when debugging boot regressions.

/* Using printk in kernel code — the basic API. */
#include <linux/printk.h>

/* Log levels as macros that prepend a priority byte */
pr_emerg("System is about to crash\n");   /* KERN_EMERG  */
pr_err("Something went wrong: %d\n", ret); /* KERN_ERR    */
pr_warn("This might be a problem\n");     /* KERN_WARNING*/
pr_info("Driver loaded successfully\n");  /* KERN_INFO   */
pr_debug("x = %d, y = %d\n", x, y);      /* KERN_DEBUG  */

/* In shell: read the ring buffer */
/* dmesg | grep -E 'error|warn|fail' */
/* journalctl -k    -- kernel messages via systemd journal */

Key insights

  • ✦The CPU has no idea what to do at power-on — it blindly executes whatever is at the reset vector in ROM. Every piece of intelligence must be bootstrapped one layer at a time.
  • ✦UEFI is a full miniature OS; the moment ExitBootServices() is called is the precise instant the firmware relinquishes hardware ownership to the bootloader/kernel.
  • ✦The initramfs solves a genuine chicken-and-egg problem: you need drivers to mount the filesystem, but drivers live on the filesystem. The RAM-based temporary filesystem breaks the cycle.
  • ✦start_kernel() is not magic — it is a carefully ordered sequence of dependency initialization, and understanding the order explains many kernel design decisions (like why memblock exists before the buddy allocator).
  • ✦PID 1 is immortal by design. Kernel_init() literally transforms a kernel thread into a userspace process via execve — the kernel never 'runs' as a normal program after that, only in response to interrupts and syscalls.
  • ✦The __init section annotation is not just a comment — it tells the linker to place that code in a separate section that gets physically freed from RAM after boot completes.
  • ✦KASLR makes the kernel load at a random physical address on every boot, which is a security property, not a coincidence — and it explains why you cannot hardcode physical kernel addresses.

Common pitfalls

  • ⚠Confusing physical addresses and virtual addresses during boot. Before the kernel sets up its own page tables, you are working with physical addresses. After setup_arch(), the kernel uses virtual addresses. Many boot bugs come from accessing the wrong address type.
  • ⚠Assuming the initramfs is optional. On systems with encrypted disks, LVM, RAID, or separate /usr partitions, boot will silently fail without the initramfs — the kernel simply cannot find the root device.
  • ⚠Forgetting that the 'init=' kernel parameter overrides systemd — useful for recovery but easy to accidentally leave in grub.cfg and wonder why the system boots strangely.
  • ⚠Editing /boot/grub/grub.cfg directly instead of using update-grub / grub-mkconfig. The file is auto-generated; manual changes will be overwritten on the next kernel update.
  • ⚠Treating dmesg output as reliable timing data. Timestamps are best-effort and can be skewed by TSC calibration; the order of messages is reliable but absolute times are not.
  • ⚠Assuming BIOS and UEFI systems behave identically. They do not — partition tables (MBR vs GPT), boot device discovery, and Secure Boot are fundamentally different. A GRUB configured for one will not work on the other.
  • ⚠Believing that 'Kernel panic: No init found' means the kernel is broken. It almost always means the root filesystem is not being found or the init binary path is wrong — the kernel itself is fine.

Practice

  1. 01
    medium

    Write a minimal x86 MBR bootloader in NASM assembly that: (1) sets up the stack at 0x7C00, (2) uses BIOS INT 0x10 to print the string 'BOOT OK' to the screen character by character, (3) halts. Boot it in QEMU with `qemu-system-x86_64 -drive format=raw,file=mbr.bin`. Verify it works.

  2. 02
    easy

    On a Linux system, extract and inspect your initramfs. Find and read the /init script (or binary). Draw a flowchart of what it does before calling switch_root. Identify which kernel modules it loads and explain why each is needed.

  3. 03
    easy

    Boot a Linux VM (QEMU or VirtualBox) with the kernel parameter `init=/bin/bash`. Observe what happens. Then add `break=premount` (on Ubuntu initramfs) to drop into the initramfs shell before the real root is mounted. Explore the initramfs filesystem. What processes are running? What filesystems are mounted?

  4. 04
    hard

    Build the Linux kernel from source (use a minimal config: `make tinyconfig`, then enable the options needed to boot in QEMU with a virtio disk). Boot it in QEMU. Read the full dmesg output and annotate at least 10 lines explaining exactly what kernel subsystem produced each line and at what boot phase.

  5. 05
    medium

    Write a minimal systemd service that runs a script at boot, logs the exact time (with nanosecond precision from /proc/uptime) to a file in /tmp, and has a hard dependency that ensures it runs after the network is up but before any application services. Use `systemd-analyze blame` and `systemd-analyze critical-chain your-service.service` to measure its boot-time impact.

Resources

📄Linux Kernel Documentation: x86 Boot Protocol
docsfree

The authoritative reference for the ABI between bootloader and kernel — essential for understanding bzImage and the setup header.

✎OSDev Wiki: Boot Sequence
articlefree

The best community-maintained reference for x86 boot from real mode through protected mode; full of working NASM examples.

📄UEFI Specification (official)
docsfree

The definitive UEFI specification from the UEFI Forum. Dense but authoritative for Boot Services, Runtime Services, and Secure Boot.

📄GNU GRUB Manual
docsfree

Complete GRUB2 documentation including configuration syntax, module system, and scripting language.

✎Linux Insides: Kernel booting process (0xAX / grnd0)
articlefree

An extraordinary series that walks through the actual kernel source code for each boot stage — goes deeper than any textbook.

✎LWN.net: The Linux kernel initramfs
articlefree

Al Viro's original article explaining the design rationale for initramfs and why it replaced initrd.

📕Understanding the Linux Kernel (3rd ed.)
bookpaidO'Reilly, by Bovet & Cesati — chapter on system initialization (Chapter 4)

The classic deep-dive; Chapter 4 on kernel initialization is the most thorough published treatment of start_kernel().

📄systemd documentation: systemd.unit, systemd.service
docsfree

The man pages for systemd unit files are excellent and contain every directive with precise semantics.

🎓Bootlin (Free Electrons) Kernel Training Materials
coursefree

Professional embedded Linux training slides (PDFs), freely downloadable. The kernel boot section is among the clearest available.

🔧QEMU: open-source machine emulator
toolfree

Essential for hands-on boot experiments without risking your real machine. Boot custom kernels and MBRs instantly.

Checkpoints — you can now…

  • ✓You can explain, without notes, what happens in the first 100 microseconds after power is applied to an x86 machine — including the reset vector address and what real mode means.
  • ✓You can write a minimal MBR bootloader in x86 NASM assembly that produces output, assemble it, and boot it in QEMU.
  • ✓You can explain the concrete difference between BIOS/MBR boot and UEFI/GPT boot, including why ExitBootServices() matters.
  • ✓You can inspect an initramfs on your own system, extract it, read the /init script, and explain why it exists.
  • ✓You can add a kernel parameter in GRUB, boot with init=/bin/bash, and explain what PID 1 being a shell means for the system.
  • ✓You can read dmesg output and identify which messages correspond to setup_arch, mm_init, device probe, and VFS mount phases.
  • ✓You can write and enable a custom systemd service unit, explain the After/Requires/Wants dependency directives, and use systemd-analyze to measure its boot cost.