Skip to content

Week 3 — The Linux Boot Process

Goal

Understand every stage from power-on to a running userspace. When your custom kernel hangs during boot, you'll know which stage failed and where to look.

Why This Matters

Kernel development means you'll regularly boot broken kernels. A kernel that panics at boot is not a crisis — it's Tuesday. But you need to know whether the problem is in early boot, driver initialization, filesystem mounting, or init. The boot process also shows you how the kernel initializes the networking subsystem, which is your area of interest.


The Big Picture

Power On
  → Firmware (UEFI or BIOS)
    → Bootloader (GRUB)
      → Kernel (bzImage decompression → start_kernel)
        → initramfs (early userspace)
          → Real root filesystem
            → Init system (systemd/init)
              → Login prompt

Stage 1: Firmware (UEFI/BIOS)

The firmware is the first code that runs. It initializes hardware at the lowest level: CPU, RAM, PCI bus enumeration, basic I/O. On your QEMU VM, this is SeaBIOS (legacy BIOS) or OVMF (UEFI).

The firmware finds the bootloader on disk by looking at the boot sector (BIOS) or the EFI System Partition (UEFI), then jumps to it.

You rarely deal with this stage unless you're doing EFI stub work.

Stage 2: Bootloader (GRUB)

GRUB loads the kernel image and optionally an initramfs into memory. It passes a command line to the kernel.

See your current GRUB config:

cat /boot/grub/grub.cfg | grep -A5 menuentry | head -30

The important line is:

linux /vmlinuz-6.x.x root=/dev/sda1 ro quiet
  • /vmlinuz-6.x.x — the compressed kernel image
  • root=/dev/sda1 — tells the kernel where the root filesystem is
  • ro — mount root read-only initially
  • quiet — suppress most boot messages (remove this for debugging!)

See your current kernel command line:

cat /proc/cmdline

For QEMU: You often bypass GRUB entirely and pass the kernel directly with -kernel, -append, and -initrd flags. We'll do this in Week 4.

Stage 3: Kernel Early Boot

When the bootloader jumps to the kernel, execution starts in architecture-specific assembly code:

arch/x86/boot/header.S       → Real mode setup
arch/x86/boot/compressed/    → Kernel decompression
arch/x86/kernel/head_64.S    → 64-bit mode setup, page tables
  → start_kernel()           → The first C function

start_kernel() — Where C Begins

Open the file:

nvim init/main.c

Search for start_kernel. This is the most important function in the kernel. It initializes everything in a specific order:

asmlinkage void __init start_kernel(void)
{
    // ... (simplified)
    setup_arch(&command_line);    // Architecture-specific init
    setup_log_buf(0);            // Kernel log buffer
    mm_core_init();              // Memory management
    sched_init();                // Scheduler
    workqueue_init_early();
    rcu_init();                  // Read-Copy-Update
    early_irq_init();           // Interrupts
    init_timers();              // Timers
    console_init();             // Console output
    mem_init();                 // Full memory init
    kmem_cache_init();          // Slab allocator
    net_ns_init();              // Network namespaces (!)
    // ... many more ...
    rest_init();                // Spawns kernel threads, starts init
}

Notice net_ns_init() — network namespace initialization happens early. Networking is fundamental to the kernel, not an afterthought.

rest_init() and the Birth of Process 1

rest_init() creates two kernel threads:

  1. kernel_init (PID 1) — becomes the init process
  2. kthreadd (PID 2) — parent of all kernel threads

kernel_init eventually calls run_init_process() which executes /sbin/init (or whatever is specified via init= on the command line).

Stage 4: initramfs (Initial RAM Filesystem)

Most modern Linux systems use an initramfs — a small filesystem loaded into RAM that contains just enough to mount the real root filesystem.

Why it exists: The real root might be on LVM, RAID, an encrypted disk, or an NFS share. The kernel needs drivers and userspace tools to access these, but those tools live on the root filesystem. Chicken-and-egg. The initramfs breaks the cycle.

See yours:

ls -lh /boot/initrd.img-$(uname -r)

# Peek inside
lsinitramfs /boot/initrd.img-$(uname -r) | head -30

For QEMU development, you can skip the initramfs entirely by building all needed drivers into the kernel (=y) and pointing directly to a disk image root.

Stage 5: Init System

Once the real root is mounted, the kernel executes /sbin/init. On Debian 13 this is systemd. From here on, it's userspace — systemd starts services, networking, and eventually presents a login.

Observing the Boot Process

Remove quiet from the kernel command line to see all boot messages:

# See the boot log after the fact
dmesg | head -100

# See timestamps
dmesg -T | head -50

# See only networking-related boot messages
dmesg | grep -i -E 'net|eth|ip |tcp|udp'

Key things to look for in dmesg:

[    0.000000] Linux version 6.x.x ...       ← Kernel starts
[    0.000000] Command line: root=...         ← Boot parameters
[    0.xxxxx]  Memory: ...                    ← RAM detected
[    1.xxxxx]  NET: Registered protocol ...   ← Networking init
[    1.xxxxx]  TCP established hash table ...  ← TCP init
[    2.xxxxx]  virtio_net virtio0: ...        ← Network driver
[    3.xxxxx]  EXT4-fs (sda1): mounted ...    ← Root mounted

Kernel Initialization Order for Networking

The networking stack initializes through a series of __init functions called via the initcall mechanism. Look at net/socket.c:

grep -n 'core_initcall\|subsys_initcall\|module_init' net/socket.c

The initcall levels (in order):

early_initcall       → Very early, before most subsystems
pure_initcall
core_initcall        → Core subsystem init
postcore_initcall
arch_initcall        → Architecture-specific init
subsys_initcall      → Subsystem init (networking goes here)
fs_initcall          → Filesystem init
device_initcall      → Device/driver init
late_initcall        → Late initialization

Networking sockets are initialized at core_initcall level. Protocol families (IPv4, IPv6) register at fs_initcall. Drivers initialize at device_initcall.

You can see the actual order:

# This shows all initcalls compiled into the kernel
cat System.map | grep '__initcall' | head -40

Exercises

  1. Read init/main.c — focus on start_kernel(). List the first 10 things it initializes, in order. This tells you the kernel's priorities.
  2. Run dmesg | grep -i net on your Debian VM. Trace the networking initialization from the earliest message to when your network interface comes up.
  3. Look at /boot/grub/grub.cfg and find the kernel command line for your current kernel. What parameters are passed?
  4. Run lsinitramfs /boot/initrd.img-$(uname -r) | grep -E '\.ko' — these are the kernel modules included in your initramfs. How many are network-related?
  5. Open net/ipv4/af_inet.c and find inet_init(). What initcall level is it registered at? What does it register?

What's Next

Next week we put this into practice — you'll boot your custom kernel in QEMU, pass kernel command lines, set up virtual networking, and learn to iterate fast on kernel changes.