Map

Linux Kernel Startup

Wiki summarylinuxkernelbootx86_64 ↳ show in map Markdown
title
Linux Kernel Startup
type
summary
summary
Espino's six-phase x86_64 boot map β€” assembly trampoline, early C, setup_arch, start_kernel, rest_init splitting into PID 0/1/2, kernel_init exec'ing /sbin/init
tags
linux, kernel, boot, x86_64
created
2026-05-14
updated
2026-07-22

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
Sub-pages