# Linux Kernel Startup

Jesús Espino's "useful map" of the Linux kernel boot path on x86_64, threading a space-colony metaphor — advance team lands on a barren planet, builds infrastructure in a specific order, eventually transforms itself into the standby maintenance crew while a civilian government takes over. The six phases ([[linux-boot-phases]]) are the structural takeaway. References Linux v7.0 source throughout.

## The handoff

GRUB hands control to the kernel. The CPU is running but in one of several modes (UEFI lands us in Long Mode; legacy BIOS in Protected Mode). Memory is awkward: the kernel was loaded low in RAM (around `0x1000000`) but compiled to run at a high virtual address (`0xffffffff81000000`). All we have: an **E820 memory map** from firmware, ACPI tables, boot parameters. No console, no allocator, no interrupts, no log.

## Phase 1: Assembly trampoline

The `bzImage` is compressed. A decompressor in `arch/x86/boot/compressed/` unpacks the real image at a random base address — that randomization is **KASLR** (Kernel Address Space Layout Randomization). Then everything converges at `startup_64`: a working stack pointer gets installed, minimal **GDT** and **IDT** tables go in place.

A few specialized detours follow. SME/SEV/TME/TDX memory-encryption gets turned on right now — you can't go back and encrypt data already written in the clear. `verify_cpu` checks for Long Mode and SSE2 — this is why you can't run a 64-bit kernel on a 32-bit CPU; it fails the very first checklist item.

Then the **page-table fixup**: KASLR broke the linker's assumptions about where the kernel was loaded. `__startup_64()` computes the offset between "where the code thinks it is" and "where we actually landed" and patches every page-table entry. Virtual addresses translate to the right physical bytes again. Jump into the first C function, `x86_64_start_kernel`.

## Phase 2: Early C

We *are* the C runtime now — no library, no allocator. `clear_bss()` zeroes uninitialized globals (in a normal program, the runtime would). KASAN gets a placeholder: every byte of its shadow region points at a single zero page, so instrumented code reads zeros instead of crashing until real shadow exists.

`idt_setup_early_handler()` installs minimal handlers for Page Faults and General Protection Faults. Without one, a fault triggers a fault triggers a fault — three failures in a row and the CPU **triple-faults** into a silent reboot. `copy_bootdata()` saves the command line, memory map, and initrd location into kernel-owned `boot_params` before they get overwritten.

The surprising final step of Phase 2: `load_ucode_bsp()` patches the **CPU's own microcode**. Some CPU bugs (Spectre, MDS) are fixed by microcode updates themselves.

## Phase 3: setup_arch

`early_cpu_init()` calls **CPUID** and dumps the answers into `boot_cpu_data` — the lookup the kernel uses later for "do we have AVX-512?" feature checks. `e820__memory_setup()` cleans up the firmware's memory map (firmware is famously unreliable about it). `e820__memblock_setup()` feeds it into **`memblock`**, a primitive early allocator that just tracks "this range is free, this range is reserved." That's all we'll have for a while.

`parse_early_param()` sets up early printk if the command line requested it — the first time the kernel can talk to the outside world. `efi_init()`, `dmi_setup()`, `init_hypervisor_platform()` figure out what kind of machine and hypervisor we're on. `init_mem_mapping()` builds the **kernel direct map** — a region of virtual addresses where every physical page gets a fixed virtual address; after this the kernel can reach any byte of RAM by computing an offset. `reserve_initrd()` and `arch_reserve_crashkernel()` rope off the regions that can't be reused. `kasan_init()` finally sets up KASAN's real shadow, replacing the zero-page placeholder.

## Phase 4: start_kernel

The longest function in the boot path. `set_task_stack_end_magic()` arms a stack-smash tripwire. Interrupts are turned off — almost everything that follows runs with them disabled until much later. The Linux version banner gets pushed into the printk ring buffer (no console yet, so it sits there waiting).

Two interesting calls early: `jump_label_init()` and `static_call_init()` patch the kernel's own code so `if (some_flag)` checks become NOP or JMP at boot, and many indirect calls become direct. This is the start of [[kernel-self-patching]]. `early_security_init()` brings up the **LSM** (Linux Security Module) framework's most fundamental hooks.

`mm_core_init()` is the big one — the moment the colony stops surviving on emergency rations. After it returns, `kmalloc()`, `vmalloc()`, and `alloc_pages()` all work. Three layered allocators come up: the **buddy allocator** (page-grained, power-of-two blocks), the **slab allocator** SLUB (sits on top, specializes in objects like `task_struct` and `inode` — same trick as Go's `mspan`), and **vmalloc** (virtual-contiguous when physical can't be). `memblock_free_all()` hands every unreserved page to the buddy.

`sched_init()` sets up runqueues. Quietly, it turns `init_task` — the original boot thread — into the **boot CPU's idle task**. Everything after this point in `start_kernel()` is the idle task pretending to be init code.

`workqueue_init_early()` sets up the workqueue subsystem but doesn't hire workers yet — scheduled work piles up like tickets at an unstaffed help desk. (Workqueues hold pending function calls; runqueues hold tasks the scheduler picks from. Different things.) `rcu_init()` brings up **RCU (Read-Copy-Update)** — readers proceed completely lock-free, writers build a new copy and swap a pointer.

The interrupt/time/RNG stack comes up in a carefully ordered passage: `early_irq_init`, `init_IRQ` (per-interrupt bookkeeping; hardware *could* deliver but they're still masked); `tick_init`, `timers_init`, `hrtimers_init`, `softirq_init` (heartbeat and the two timer flavors); `timekeeping_init` (reads the real-time clock — first sensible answer to "what time is it?"); `random_init` (mixes early entropy from RDRAND, bootloader seed, boot-event timing). `boot_init_stack_canary()` finally installs a real random canary, replacing the predictable placeholder. Then `local_irq_enable()` — the colony just turned on its alarm system.

`console_init()` brings up the real console drivers and **drains the entire printk ring buffer** that's been piling up since the very first `pr_notice`. That's why the boot log appears in a sudden burst rather than a steady trickle.

`arch_cpu_finalize_init()` finishes CPUID-driven feature discovery, picks the right Spectre/Meltdown/MDS mitigations, sets up the FPU, and calls **`alternative_instructions()`** — patches every `ALTERNATIVE` macro site so the kernel is runtime-rewritten to be optimal for this exact CPU. Same `vmlinux` boots optimally on dozens of CPU generations. See [[kernel-self-patching]].

The last big block creates **slab caches** for every other subsystem: `fork_init()` (with `cred_init`, `signals_init`, `pagecache_init`) for `task_struct` and process plumbing — HR opens. `vfs_caches_init()` for the filesystem layer. `proc_root_init()` for `/proc`. `cgroup_init()`, `security_init()` (full LSM), `net_ns_init()` (namespace plumbing that lets containers exist).

## Phase 5: rest_init

`rest_init()` spawns two kernel threads: PID 1 (`kernel_init`, the future userspace init) and PID 2 (`kthreadd`, the kernel thread daemon that handles every `kthread_create()` for the lifetime of the system). Ordering trick: `kernel_init` is spawned first to get PID 1, then blocks on a completion until `kthreadd` is ready.

Then **the magic moment**: `rest_init()` calls `schedule_preempt_disabled()`. The scheduler context-switches away. When control eventually returns to this CPU, `cpu_startup_entry(CPUHP_ONLINE)` runs `do_idle()` forever. The original thread of execution — running since `startup_64` in raw assembly through every subsystem init — has permanently become the boot CPU's **idle task (PID 0)**. It only runs again when nothing else wants the CPU.

The advance team just clocked off and became the standby maintenance crew.

## Phase 6: kernel_init and userspace

`workqueue_init()` finally hires the `kworker/...` threads. `smp_init()` sends IPIs to wake the parked cores; each goes through its own simplified boot. `sched_init_smp()` builds the **scheduling domains** the load balancer uses — real multitasking begins.

`do_initcalls()` runs thousands of initcalls grouped into ordered levels (`early`, `core`, `subsys`, `fs`, `device`, `late`). This is where the boot log fills up with `PCI: Using ACPI for IRQ routing`, `ata1: SATA max UDMA/133 ...`, `ext4 filesystem driver registered`. `prepare_namespace()` mounts whatever `root=` specified — on modern distros, the initramfs unpacked into a `tmpfs`, with `/init` loading drivers and pivoting to the real root.

`free_initmem()` reclaims the `__init` section: `Freeing unused kernel memory: 2048K`. The setup-crew toolboxes get packed away. `mark_readonly()` flips `rodata` to actually read-only at the page-table level. `pti_finalize()` finalizes **PTI (Page Table Isolation)** — the Meltdown mitigation. The locks finally go on the doors now that the doors are installed.

The kernel walks candidate init paths in order: `rdinit=`, `init=` (if explicit and missing — panic), `CONFIG_DEFAULT_INIT`, `/sbin/init`, `/etc/init`, `/bin/init`, `/bin/sh` as last resort. `kernel_execve()` replaces the kernel thread with the userspace program. Same PID, same task, now running unprivileged code. The system is fully booted.

## What the article gets right that's worth keeping

- **Boot is a planned construction project, not a script.** Each phase unblocks a specific capability for the next one. Disabled interrupts protect the careful ordering; the first thing that runs with interrupts enabled is the console.
- **The kernel self-modifies aggressively at boot.** `jump_label`, `static_call`, `alternative_instructions` together rewrite the running text segment so a generic `vmlinux` becomes optimal for this specific CPU. This is the same `vmlinux` Debian ships boots on every x86_64 chip Debian supports.
- **PID 0 is the boot thread reincarnated.** Most "boot" content focuses on what got initialized; the article foregrounds where the boot thread *went* — it permanently became the idle task. This makes the `init_task → idle` relabeling stop feeling like a quirk.
- **`memblock` → buddy → SLUB → vmalloc is a real handoff**, not a layering. `memblock_free_all()` is the discrete moment ownership transfers.

## Cross-references

- [[linux-boot-phases]] — the six-phase model as a transferable concept
- [[kernel-self-patching]] — jump_label, static_call, alternative_instructions as one pattern
- [[linux-preemption-models]] — what happens to scheduling after boot completes
- [[linux-7-postgres-regression]] — why the post-boot preemption model matters for spinlock-heavy workloads
- [[spinlock]] — the synchronization primitive the disabled-interrupts era leans on
- [[linux-kernel-pgit]] — querying the kernel git history that produced this code
- [[internals-for-interns]] — Espino's blog, this article is the first in the kernel series
- [[operating-systems-three-easy-pieces]] — the subsystems this boot path is assembling in order (address spaces, the scheduler, allocators, the filesystem layer) get a chapter each in OSTEP; free online, and the right prerequisite if the phase-4 name-dropping went by too fast
