What actually happens when a computer boots
From the reset vector at 0xFFFFFFF0 to PID 1: every handoff in the chain, the address each one uses, and the exact message you get when one of them fails.
Booting is a bootstrapping problem in the literal sense: a computer that can only run programs loaded into memory has to load a program into memory without running a program. The solution is a chain of increasingly capable loaders, each just clever enough to fetch the next.
What is striking is how little each link knows about the next. The firmware does not know what an operating system is. The boot sector does not know what a filesystem is. Each stage's job is to be replaced by the next, at an address fixed decades ago.
The first instruction, sixteen bytes below the ceiling
An x86 processor comes out of reset in real mode: 16-bit, no memory protection, about a megabyte addressable. The first instruction is fetched from CS:IP = F000:FFF0. On an 8086 that gives physical address 0xFFFF0. On a 386 and everything since, the processor cheats: CS still reads F000, but the hidden base register behind it is loaded at reset with 0xFFFF0000, so the fetch happens at 0xFFFFFFF0 — sixteen bytes below 4 GB, where the chipset maps the firmware ROM.
Sixteen bytes is not room for a program, so what lives there is a far jump into the ROM, which reloads CS the ordinary way and drops the processor back into the one-megabyte world. There is no stack yet, and no interrupt table.
POST, and the tables the BIOS leaves behind
The power-on self test sizes memory, configures the chipset, enumerates PCI, then hunts for option ROMs: card firmware mapped from 0xC0000 upward and scanned every 2 KB for the same 0x55 0xAA signature a boot sector uses. Each one found is called so it can install its own handlers. The video BIOS is first, which is why the screen wakes part-way through.
Then it builds the two structures all real-mode software depends on. The interrupt vector table fills the first kilobyte of RAM, 0x0000–0x03FF: 256 entries of four bytes, a segment and an offset each. The BIOS data area starts at 0x0400 with the equipment word, port addresses, the keyboard buffer and the tick count. Point those vectors into ROM and the firmware has published an API: INT 10h video, INT 13h disk, INT 16h keyboard, INT 15h memory map — the subject of BIOS and UEFI.
The boot order, and the 512 bytes at 0x7C00
Now the BIOS walks its boot order — floppy, hard disk, CD-ROM. For each device the procedure is identical and almost insultingly simple: read the first sector, 512 bytes, LBA 0, to 0x0000:0x7C00; look at the last two bytes; if they are 0x55 0xAA — offsets 510 and 511 — jump to 0x7C00 with DL holding the drive number. No filesystem, no verification, nothing cryptographic.
The address is a fossil: 0x7C00 is 32 KiB minus 1,024 bytes — 512 for the sector, 512 for its stack — at the top of the smallest memory an early IBM PC might have. CD-ROMs are the one exception, and it is a workaround: their sectors are 2,048 bytes, so El Torito has the firmware pretend the boot image is a 1.44 MB floppy.
$ xxd -s 0x1b0 -l 80 disk.img
000001b0: 0000 0000 0000 0000 0000 0000 0000 8020
000001c0: 2100 83df 130c 0008 0000 0020 0300 0000
000001d0: 0000 0000 0000 0000 0000 0000 0000 0000
000001e0: 0000 0000 0000 0000 0000 0000 0000 0000
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa
0x1be 80 status: 0x80 = active, 0x00 = not
0x1bf 20 21 00 first sector, as cylinder/head/sector
0x1c2 83 partition type: 0x83 = Linux
0x1c3 df 13 0c last sector, as cylinder/head/sector
0x1c6 00 08 00 00 first sector, as a 32-bit LBA = 2048
0x1ca 00 20 03 00 length in sectors = 204800 (100 MiB)
0x1ce .. three more entries, all zero here
0x1fe 55 aa the only thing the BIOS checks
Why half a kilobyte is not enough
There is less room than it looks. The last two bytes are the signature; the 64 before them, 0x1BE to 0x1FD, are the partition table — four entries of 16 bytes, each with a status byte, a start and end in cylinder/head/sector, a type, a 32-bit LBA and a sector count. That leaves 446 bytes of code.
Classic MBR code moves itself out of the way, usually to 0x0600, walks the four entries, finds the one whose status byte is 0x80, loads that partition's own first sector into the vacated 0x7C00 and jumps. That second sector is the partition boot record, and unlike the MBR it belongs to a filesystem. This is chainloading: DOS, Windows up to XP, every 1990s boot manager.
GRUB 2 splits the job differently. boot.img is the 512-byte sector and has no filesystem code; it reads one sector number hardcoded at install time — the first sector of core.img. That loads the rest of core.img, which carries a filesystem driver, so from there GRUB can open /boot/grub, load modules and read grub.cfg. core.img lives in the unpartitioned gap after the MBR, which is why partitioning tools now leave 1 MiB empty there.
The handover to a kernel
A Linux kernel image is not a plain binary. Its front is a small real-mode program with a documented header. The setup header starts at offset 0x01F1; 0x0202 holds the magic HdrS (0x53726448), 0x0206 the protocol version, and 0x01FE holds 0xAA55 — the kernel begins with a legal, useless boot sector, left over from when you could write one straight onto a floppy.
Into that header the loader writes what the kernel cannot know: the command line address, the ramdisk's address and size, which loader this is, the video mode. The real-mode part goes to 0x90000, the compressed kernel to 0x100000 — the first byte above 1 MB. That is the bz in bzImage: big zImage. The older zImage loaded at 0x10000 and had to fit below 640 KB.
What sits at 0x100000 is still not the kernel. It is a decompressor with the kernel attached as a payload — gzip historically, XZ or zstd now. First it opens the A20 gate, a line the IBM AT bolted on in 1984 to preserve 8086 address wraparound; while A20 is masked, address bit 20 is forced to zero. Then it loads a GDT, sets the protection bit in CR0 and far-jumps into protected mode: 32-bit, flat, all memory reachable — see real mode and protected mode. Only then does it unpack the kernel and jump to startup_32.
start_kernel, and the first process
start_kernel() in init/main.c is where a loaded blob becomes an operating system. It prints the banner, parses the command line, builds page tables and the allocator, starts the scheduler, calibrates timers against the 8253 ticking at 1.193182 MHz, brings up the interrupt controller and runs the initcalls — the driver registrations behind most of the messages scrolling past. All of it runs as PID 0, swapper, which ends as the idle task.
Near the end it calls rest_init(), which spawns the thread that becomes PID 1. That thread mounts a root filesystem and tries to execute a userland program: whatever init= named, then /sbin/init, /etc/init, /bin/init, /bin/sh. If none can be run, the kernel panics with No working init found.
Why initramfs exists
Mounting that root filesystem is another chicken-and-egg problem. To reach the disk the kernel needs a driver for its controller, then one for the filesystem, and possibly code to assemble a RAID array or unlock LUKS first. Those are modules, and modules live in /lib/modules — on the root filesystem it is trying to mount.
The way out is a small filesystem carried in memory, loaded by the bootloader. initrd, the older form, is a compressed disk image mounted as /dev/ram0, running /linuxrc. initramfs, the modern form, is a cpio archive the kernel unpacks straight into rootfs, an always-present tmpfs, then runs /init from it. That /init loads modules, finds the real root and calls switch_root to replace itself. A distribution that must boot on unknown hardware cannot do without one; a system built for one known machine, drivers compiled in, does not need one. Buildroot here boots with no initramfs, no bootloader and no disk.
The whole sequence
- Reset. Real mode; first instruction fetched from
0xFFFFFFF0, mapped to the firmware ROM. It is a far jump. - POST. Memory sized, chipset configured, PCI enumerated, option ROMs from
0xC0000scanned. - Tables. Interrupt vectors at
0x0000–0x03FF, BIOS data area at0x0400.INT 10h,13h,16hnow work. - Boot order. Each device in turn: first sector, 512 bytes, read to
0x0000:0x7C00. - Signature. Bytes 510–511 checked for
0x55 0xAA; on a match, jump to0x7C00,DL= drive number. - Stage one. The MBR relocates to
0x0600, reads the table at0x1BE, loads the0x80entry's sector — or GRUB readscore.imgfrom the gap after the MBR. - Stage two. A filesystem is mounted and the kernel loaded: real-mode part at
0x90000, compressed image at0x100000. - Header. The setup header at
0x01F1is filled in — command line, ramdisk, loader type — and jumped into. - Protected mode. A20 opened, GDT loaded,
CR0protection bit set; the kernel decompresses itself. start_kernel(). Memory, scheduler, timers, interrupts, initcalls. This is the wall of messages.- Root. initramfs unpacked into
rootfs; its/initloads modules andswitch_roots onto the real one. - PID 1.
/sbin/initruns. The boot is over; everything after this is userland.
Where it breaks, and what you see
Boot failures are unusually legible: each stage fails in only a few ways, and each has its own vocabulary. The firmware messages below are literal — SeaBIOS, which runs here, prints exactly these strings.
| What is wrong | What you see | Stage |
|---|---|---|
No 0x55 0xAA at offset 510 | Boot failed: not a bootable disk, then the next device; if none work, No bootable device. | Firmware |
| Sector unreadable: truncated image, wrong media type | Boot failed: could not read the boot disk | Firmware |
No partition marked active (0x80) | Whatever the MBR's 446 bytes decide: classically Missing operating system, often a blinking cursor | Stage one |
core.img overwritten by another installer | error: unknown filesystem and a grub rescue> prompt | Stage two |
root= names a device that is not there | VFS: Cannot open root device, then Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) | Kernel |
| initramfs missing the driver for the root disk's controller | A shell inside the initramfs: (initramfs) on Debian, dracut:/# on Fedora | initramfs |
| Root mounted, no init binary runs | Kernel panic - not syncing: No working init found. Try passing init= option to kernel. | PID 1 |
Watching it happen
On real hardware most of this is invisible: under a second, with the video card asleep for the first part of it. Under emulation it is slow enough to read. Boot Buildroot from the machine builder and you skip stages four to eight — the emulator implements the boot protocol itself — so the first thing on screen is the kernel banner, and all of start_kernel scrolls past on the serial console, here a text pane in the page. Its command line is editable: add init=/bin/sh and land in a shell with no init at all.
You can also stop it. Pausing halts the emulator between two instructions; nothing times out, because the guest's clock is derived from the emulator's and stops with it. A snapshot taken mid-boot restores into the same half-finished state, making one failure reproducible as often as you like. Boot FreeDOS from a floppy for the classic chain in miniature, and BootROM to stop at step five. The glossary has the rest.