Part VII · Chapter 18
Booting: Firmware, UEFI and the Bootloader
When you press the power button, the memory is empty, the devices know nothing, and the operating system is just a pattern of bits on a disk. Something has to load it, but loading needs a program, and programs need loading. This chapter follows how a computer pulls itself up by its own bootstraps, and you'll get to run a real boot sector.
The word boot comes from "pulling yourself up by your bootstraps", an impossible trick. A computer manages it in stages. A tiny program that's always there loads a slightly bigger one, which loads a bigger one still, until finally the whole operating system is running. Each stage knows just enough to find and start the next.
The very first instruction
When the power comes on, a processor doesn't start at random. Its circuits force the program counter (Chapter 4) to a fixed address, the reset vector, and it begins fetching instructions from there. Whatever is at that address must already be valid code, so it has to be in ROM: memory that keeps its contents without power.
On the IBM PC, the 8088 started at segment FFFF, offset 0000: physical address 0xFFFF0, just 16 bytes from the top of its 1 MB address space. In Chapter 8's memory map, that's right inside the BIOS ROM. Sixteen bytes isn't much room, so the instruction there is simply a jump to the real start of the BIOS.
Astonishingly, a modern x86-64 processor still wakes up in the 16-bit mode of the 8086, at address 0xFFFFFFF0: 16 bytes below 4 GB, where the motherboard maps its flash memory chip. A RISC-V chip starts wherever its designers decided; the QEMU "virt" machine of Chapter 11 starts in its little boot ROM at 0x1000.
Firmware and the self-test
The program in that ROM is the firmware. On the IBM PC it was called the BIOS (Basic Input/Output System): 8 KB of code that first ran a power-on self-test (POST). It checked the processor and the ROM, tested the memory, set up the interrupt controller, timer and DMA chips of Part IV, filled in the interrupt vector table (Chapter 12), and started the video card and keyboard. One short beep meant that all was well.
The BIOS also stayed around after booting, offering simple services through software interrupts: int 0x10 for the screen, int 0x13 for disks, int 0x16 for the keyboard. DOS and its programs used them constantly.
The boot sector
With the hardware ready, the BIOS has one last job: load the next stage. Its rule is wonderfully simple. Read the very first sector of the boot disk, 512 bytes, into memory at address 0x7C00, and jump to it. The firmware has no idea what's in those bytes. It just runs them.
There's one safety check. Except on the very first PCs, the BIOS insists that the sector's last two bytes are 55 AA, the boot signature. Otherwise it assumes there's no operating system on that disk and moves on to the next one.
Here is a complete, real boot sector. It's a tiny program, just 24 bytes of code, that uses the BIOS to print a message and then goes to sleep. It was assembled with the same tools as every example in this book:
That isn't just a simulation. Here's the same 512-byte file booted on an emulated PC with QEMU, a free program that imitates a whole computer, and its real BIOS:
00 00: the BIOS refuses to run it.You can try it yourself. Download boot.s: its first lines give the two commands that turn it into the 512-byte file boot.bin, and the command that boots it: qemu-system-i386 -drive format=raw,file=boot.bin,if=floppy.
A real operating system's boot sector does something more useful with its 512 bytes: it loads more sectors from the disk, the next stage of the boot. On a hard disk, the first sector is the master boot record (MBR): 446 bytes of code, then a table of up to four partitions (the separate sections of the disk), then 55 AA.
UEFI: firmware grows up
The BIOS served PCs for about 30 years, but it was showing its age. It ran in 16-bit mode with 1 MB of memory, the boot code had to fit in 446 bytes, and the MBR's partition table couldn't describe disks larger than 2 TB. Its replacement is UEFI (Unified Extensible Firmware Interface), standard on PCs since the early 2010s:
- It runs in 32- or 64-bit mode, with all the memory, and has proper drivers for disks, USB, graphics and networks.
- It understands the FAT file system (Chapter 15). Instead of running a raw sector, it loads the next stage as an ordinary file from a small FAT partition, the EFI system partition.
- It keeps a list of boot entries in a small non-volatile memory, so several operating systems can live side by side.
- Disks use the GUID partition table (GPT), which has room for 128 partitions and practically unlimited sizes.
- Secure Boot can check a digital signature on every program it loads, so malware can't sneak into the boot process.
The bootloader
The firmware's goal is to start a bootloader: a program whose only job is to load the operating system. On Linux that's usually GRUB; Windows has its own Boot Manager. A bootloader typically:
- Shows a menu if there's more than one operating system or kernel to choose from.
- Loads the operating system's kernel (Chapter 19) into memory, plus a small starter file system with the drivers needed to reach the real one.
- Passes it a command line, a few settings such as which disk holds the root file system.
- Jumps to the kernel's first instruction. The bootloader's work is done, and its memory will be reused.
From 16 bits to 64
There's one more piece of history to deal with. Because an x86-64 processor wakes up pretending to be an 8086, someone has to switch it into 64-bit mode, step by step, setting special control bits in the right order. On a modern PC the UEFI firmware does this in its first moments; on an old BIOS PC it was the bootloader's or kernel's job.
RISC-V has no such baggage. It starts in its most privileged level, machine mode, where a small firmware (often OpenSBI) runs. The firmware then drops into the less privileged supervisor mode for the operating system, which in turn runs programs in user mode. We'll see why those privilege levels matter in Chapter 19.
Meanwhile, inside a real CPU
Here's our boot sector's source code. The comments use #, the style of the assembler that built it:
.code16 # the CPU is in 16-bit real mode _start: mov ax, 0x07C0 mov ds, ax # DS:0 is 0x7C00, where the BIOS loaded us mov si, offset msg # SI = offset of the message xor bx, bx # BIOS: page 0 print: lodsb # AL = next character, SI = SI + 1 or al, al # zero marks the end jz done mov ah, 0x0E # BIOS function 0Eh: print the character in AL int 0x10 # call the BIOS video service jmp print done: hlt # sleep until an interrupt... jmp done # ...then sleep again, forever msg: .ascii "Hello from the boot sector!" .byte 13, 10, 0 .fill 510 - (. - _start), 1, 0 .byte 0x55, 0xAA # the boot signature
On the computer this book was written on, Linux can show how the boot went. systemd-analyze measures each stage, efibootmgr lists the firmware's boot entries, and /proc/cmdline holds the command line the bootloader passed to the kernel:
$ systemd-analyze
Startup finished in 8.936s (firmware) + 8.052s (loader) + 9.537s (kernel) + 12.923s (userspace) = 39.449s
$ efibootmgr (trimmed)
BootCurrent: 0001
BootOrder: 0001,0002,0003,0004
Boot0001* Ubuntu HD(1,GPT,...)/File(\EFI\ubuntu\shimx64.efi)
Boot0002* UEFI:CD/DVD Drive
$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.8.0-147-generic root=/dev/mapper/ubuntu--vg-root ro
Almost nine seconds pass before the bootloader even starts, mostly spent testing and training the memory. The loader's eight seconds include the time GRUB's menu waits for a key press. The shimx64.efi file is a small, signed first stage that lets Linux work with Secure Boot.
Try it yourself
-
The 8088 starts at segment
FFFF, offset0000. Using segment × 16 + offset, which physical address is that?Show answer
0xFFFF× 16 =0xFFFF0, plus offset 0:0xFFFF0. That's 16 bytes below the top of 1 MB (0x100000). -
Our boot sector is loaded at
0x7C00. At which addresses do the two signature bytes end up?Show answer
They are bytes 510 and 511 of the sector:0x7C00+ 510 =0x7DFEand0x7DFF. -
An MBR has 446 bytes of code, then four partition entries, then the signature. How big is each partition entry?
Show answer
512 − 446 − 2 = 64 bytes for four entries, so 16 bytes each. -
Why must the reset vector point into ROM rather than RAM?
Show answer
When the power comes on, RAM holds nothing useful (DRAM even loses its contents within moments of being switched off). The first instruction must be there before anything has had a chance to load it, so it has to live in memory that keeps its contents without power. -
Our boot sector uses
int 0x10to print. Why couldn't a UEFI bootloader running in 64-bit mode do the same?Show answer
int 0x10calls a BIOS routine written in 16-bit code, through the real-mode interrupt vector table at address 0. In 64-bit mode the processor uses a different interrupt table (the IDT) and can't run that code. UEFI instead offers its own services, called through ordinary function calls.
What we learned
- A processor starts at a fixed reset vector, which must point into ROM or flash: on x86, just below the top of memory.
- The firmware (the BIOS, today UEFI) tests and initializes the hardware, then finds and starts the next stage.
- The BIOS loads a 512-byte boot sector at
0x7C00, if it ends in55 AA. UEFI instead loads a file from the FAT EFI system partition. - A bootloader loads the kernel and its command line into memory, then jumps to it.
- x86-64 starts in 16-bit real mode and climbs through protected mode to 64-bit long mode by setting control bits in order.
The bootloader has jumped to the kernel's first instruction. Now the real show begins. In the next chapter we meet the operating system kernel: the program that owns the whole machine and shares it fairly among all the others.