Operating System Boot Process
The **booting process in operating system** terms is a chain of increasingly capable programs, each loading the next. The boot sequence runs in stages because no single stage is capable enough to do the whole job. The CPU begins executing firmware at a fixed address knowing nothing about disks or file systems, and ends with a full kernel and user space.
Why Booting Needs Stages At All
The booting process in operating system design is a bootstrap problem: to load an operating system you need code that can read a disk, understand a file system, and place data in memory — but that code is itself part of the operating system you have not loaded. The solution is a chain, each link just capable enough to load a larger one.
Power-on begins with the CPU in a defined state, executing at a hardwired physical address called the reset vector — on x86, 0xFFFFFFF0, sixteen bytes below 4 GB, which the chipset maps to flash memory. No RAM is initialised, no disk is accessible, no file system exists. The first instruction runs from ROM because there is nowhere else it could run from.
Firmware then performs POST, initialises the memory controller so RAM becomes usable, enumerates buses, and locates a boot device. Only then can something be loaded from storage — and what gets loaded is a bootloader, not the kernel, because firmware's ability to interpret disk layouts is limited and the kernel is large, compressed, and needs a prepared environment.
- The CPU starts at a hardwired reset vector in ROM
- No RAM exists until the controller is trained
- Each stage loads one large enough to do more
- Firmware locates a boot device, not an OS
BIOS and UEFI Differ in What Firmware Can Read
Legacy BIOS loads exactly 512 bytes — the master boot record from the first sector — to physical address 0x7C00 and jumps to it. Five hundred and twelve bytes, minus the partition table, leaves about 440 bytes of code, which is nowhere near enough to parse a file system. Hence the multi-stage bootloader: stage 1 in the MBR loads stage 1.5 from the sectors immediately after it, which understands ext4 or NTFS well enough to load stage 2 from an actual file.
BIOS vs UEFI comes down to that limitation. UEFI firmware understands GPT partitioning and can read a FAT32 EFI System Partition directly, so it loads a normal file — \EFI\BOOT\BOOTX64.EFI — with no sector chaining. It also starts in 32- or 64-bit mode rather than 16-bit real mode, provides runtime services the OS can call, and keeps boot entries in NVRAM instead of a fixed sector.
The practical consequence is that a UEFI bootloader is an ordinary program in a filesystem that can be replaced by copying a file, whereas the MBR scheme required writing raw sectors — which is why dual-boot setups broke so easily when one OS overwrote the MBR.
- BIOS loads one 512-byte sector and chains stages
- UEFI reads GPT and FAT32 and loads a real file
- UEFI starts in 32- or 64-bit mode, not real mode
- Boot entries live in NVRAM rather than a fixed sector
Bootloader, Kernel, Init
The bootloader — GRUB2, systemd-boot, Windows Boot Manager, U-Boot on embedded systems — presents a menu if configured, then loads the kernel image and an initial ramdisk into memory, assembles a command line, and transfers control to the kernel's entry point with a defined register and memory state.
The initramfs exists for a specific reason worth understanding: the kernel must mount the root filesystem, but the driver needed to reach it may be a module stored on that filesystem. The initramfs is a small archive unpacked into RAM containing exactly the modules needed — the NVMe or RAID driver, LVM tools, disk encryption — so the kernel can assemble access to the real root and then pivot onto it.
The kernel decompresses itself, sets up page tables and interrupt handlers, brings up secondary CPUs, initialises drivers, mounts the root filesystem, and finally executes PID 1. On most Linux systems that is systemd, which starts services according to their declared dependencies; the traditional alternative was SysV init running numbered scripts in sequence. When PID 1 reaches its default target, boot is complete.
- The bootloader loads kernel and initramfs into RAM
- The initramfs carries the driver for the real root
- The kernel decompresses, pages, and brings up SMP
- pivot_root switches to the real filesystem
Power-on to login prompt, one handoff at a time
# The boot chain as a sequence of handoffs. Each stage can only do
# enough to load and verify the next one -- that is why stages exist.
STAGES = [
("reset vector", "CPU fetches 0xFFFFFFF0 from flash", "no RAM yet", False),
("UEFI firmware", "POST, train DRAM, enumerate PCIe", "RAM usable", True),
("boot manager", "read BootOrder, open the ESP", "found .EFI", True),
("shim", "MS-signed, carries the distro key", "GRUB verified", True),
("GRUB2", "load vmlinuz + initramfs", "kernel in RAM", True),
("kernel", "decompress, paging, IDT, SMP", "initramfs root", True),
("initramfs", "load the driver for the real root", "root reachable", False),
("pivot_root", "switch root, free the ramdisk", "real fs", False),
("PID 1", "systemd starts units by dependency", "graphical.target", False),
]
def boot(secure_boot=True):
trusted, verified = True, 0
for name, action, result, verifies_next in STAGES:
if secure_boot and not trusted:
return "refused at " + name # chain of trust broken
if verifies_next:
verified += 1 # signature checked before the jump
return "login prompt (%d signature checks)" % verified
print("stages:", len(STAGES))
print("boot result:", boot(secure_boot=True))
print("the initramfs exists because the root driver may live on the root")
// The handoff chain. Each entry can only reach the next one.
#include <iostream>
#include <string>
#include <vector>
struct Stage {
std::string name, action, result;
bool verifiesNext;
};
const std::vector<Stage> CHAIN = {
{
"reset vector", "CPU starts at 0xFFFFFFF0 in flash", "no RAM yet", false
},
{
"UEFI", "POST, DRAM training, PCIe scan", "RAM usable", true
},
{
"boot manager", "read BootOrder, open the ESP", "found .EFI", true
},
{
"shim", "MS-signed, carries the distro key", "GRUB verified", true
},
{
"GRUB2", "load vmlinuz + initramfs", "kernel in RAM", true
},
{
"kernel", "decompress, paging, IDT, SMP", "initramfs root", true
},
{
"initramfs", "load the driver for the real root", "root reachable", false
},
{
"pivot_root", "switch root, free the ramdisk", "real filesystem", false
},
{
"PID 1", "systemd starts units", "graphical.target",false
},
};
bool checkSignature(const Stage&) {
return true;
}
// RSA/ECDSA in reality
// Secure Boot: refuse to transfer control to an unverified image.
std::string boot(bool secureBoot) {
int verified = 0;
for (const auto& s : CHAIN) {
if (secureBoot && s.verifiesNext) {
if (!checkSignature(s)) return "refused at " + s.name;
++verified;
}
}
return "login prompt after " + std::to_string(verified) + " signature checks";
}
int main() {
std::cout << boot(true) << '\n';
}// Boot as a verified chain of handoffs.
import java.util.*;
class Boot {
record Stage(String name, String action, boolean verifiesNext) {
}
static final List<Stage> CHAIN = List.of(
new Stage("reset vector", "CPU fetches 0xFFFFFFF0 from flash", false),
new Stage("UEFI", "POST, DRAM training, PCIe scan", true),
new Stage("boot manager", "BootOrder in NVRAM -> open the ESP", true),
new Stage("shim", "MS-signed, carries the distro key", true),
new Stage("GRUB2", "load vmlinuz + initramfs", true),
new Stage("kernel", "decompress, paging, IDT, SMP", true),
new Stage("initramfs", "load driver for the real root", false),
new Stage("pivot_root", "switch root, drop the ramdisk", false),
new Stage("PID 1", "systemd resolves and starts units", false));
static String boot(boolean secureBoot) {
for (Stage s : CHAIN)
if (secureBoot && s.verifiesNext() && !verify(s))
return "refused at " + s.name(); // trust chain broken
return "login prompt";
}
static boolean verify(Stage s) {
return true;
}
// RSA/ECDSA in reality
}Step through it
Running on power button pressed on a UEFI x86-64 machine with Secure Boot enabled
Read all 15 Steps
- power on — the CPU has no idea what a disk is Power stabilises and the CPU leaves reset in a defined state. It begins executing at the reset vector, a hardwired physical address: 0xFFFFFFF0 on x86-64, sixteen bytes below the 4 GB mark, which the chipset routes to flash memory rather than RAM. Nothing has initialised the memory controller yet, so there is no usable RAM. Execution runs directly from ROM because there is nowhere else it could run from.
- POST and DRAM training make memory usable Firmware runs power-on self test, then trains the memory controller — calibrating timings against the installed DIMMs, which is genuinely the slowest part of modern boot and the reason a server takes a minute before showing anything. Only after this does RAM exist as far as software is concerned.
- enumerate buses and build the hardware description Firmware walks PCIe, identifies devices, assigns memory-mapped I/O ranges, and constructs ACPI tables describing the machine — how many CPUs, where the interrupt controllers are, what power states exist. The kernel will read these tables later rather than probing hardware blindly.
- the boot manager finds the EFI System Partition UEFI reads BootOrder from NVRAM — a persistent variable listing boot entries in priority order — and opens the EFI System Partition, a FAT32 partition it can read natively. This is the concrete difference from legacy BIOS: firmware here loads a normal file from a real filesystem, where BIOS could only load 512 raw bytes from sector zero.
- Secure Boot verifies shim before executing it With Secure Boot enabled, firmware checks the loader's signature against keys held in NVRAM before transferring control. Linux distributions ship shim — a small loader signed by Microsoft's UEFI CA, whose certificate is pre-installed on essentially all consumer hardware. Firmware trusts Microsoft's signature; shim then carries the distribution's own key.
- shim verifies GRUB with the distribution's key shim exists purely to extend the chain without every distribution needing a Microsoft signature for every build. It holds Fedora's or Ubuntu's certificate embedded in its signed body, and uses that to verify GRUB2. If GRUB fails the check, execution stops here rather than continuing unverified.
- GRUB reads its config and loads two files GRUB2 parses grub.cfg, draws the menu if one is configured, and loads two things into RAM: the compressed kernel image vmlinuz, and the initramfs archive. It also assembles the kernel command line — root=UUID=..., quiet, any parameters set by the administrator — and verifies the kernel's signature before jumping to it.
- the kernel decompresses itself and takes over GRUB jumps to the kernel entry point with a defined register and memory state. The kernel's first job is decompressing its own body, then it establishes real page tables, installs the interrupt descriptor table, and starts the scheduler. Up to this moment only one CPU has been running.
- secondary CPUs come online The boot CPU sends startup IPIs to the other cores, which come out of reset, initialise their own local state, and enter the scheduler's idle loop. The machine goes from one active core to all of them. From here the kernel is genuinely parallel.
- initramfs solves the chicken-and-egg problem The kernel must mount the root filesystem, but the driver needed to reach it — NVMe, RAID, LVM, LUKS — may be a module stored on that very filesystem. The initramfs is the answer: a small archive already in RAM containing exactly those modules. The kernel unpacks it, mounts it as a temporary root, and runs its init script.
- the real root is assembled and unlocked The initramfs script loads the NVMe driver, activates LVM volume groups, and prompts for the disk encryption passphrase if the root is on LUKS. Only when these steps succeed does a block device representing the real root filesystem exist.
- pivot_root switches to the real filesystem The kernel mounts the real root, moves the running process tree onto it with pivot_root, and frees the initramfs memory entirely — the ramdisk has done its one job and is reclaimed. The temporary root disappears.
- PID 1 starts, and userspace begins The kernel executes /sbin/init as process 1 — systemd on most distributions. Unlike the old SysV scheme of numbered scripts run in sequence, systemd builds a dependency graph of units and starts everything it can in parallel, which is most of why boot times fell so sharply.
- services reach the default target systemd walks the dependency graph: mount local filesystems, start the network, start the logging daemon, start the display manager. When every unit required by graphical.target is active, boot is complete and a login prompt appears.
- what the chain of trust actually proved Each stage verified the next before transferring control: firmware checked shim against a platform key, shim checked GRUB against the distribution key, GRUB checked the kernel, and the kernel checks every module it loads. The reason this matters is that anything running before the OS can compromise everything after it, invisibly — a bootkit sees every key the kernel handles and can hide itself from any tool running under that kernel. Measured Boot goes one step further, hashing each stage into TPM platform configuration registers, so the machine can later prove what it booted rather than merely having refused to boot something unsigned.
Secure Boot and the Chain of Trust
Secure Boot addresses the obvious attack: code that runs before the OS can compromise anything the OS later does, invisibly. A bootkit installed in the bootloader sees every key the kernel handles and can hide itself from any tool running under that kernel.
The defence is a signature chain. UEFI firmware holds platform keys and verifies the signature of the bootloader before executing it; the bootloader verifies the kernel; the kernel verifies module signatures. Each stage validates the next before transferring control, so trust extends from a root held in firmware.
Linux distributions participate through shim, a small loader signed by Microsoft's UEFI CA — since that certificate is pre-installed on essentially all consumer hardware — which then verifies a distribution-signed GRUB using the distribution's own embedded key. Measured Boot goes further, recording a hash of each stage into TPM platform configuration registers, so the system can later prove what it booted rather than merely refusing to boot something unsigned.
- Firmware verifies the loader against platform keys
- shim carries the distribution key under a Microsoft signature
- Each stage checks the next before transferring control
- Measured Boot records hashes into TPM registers