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.

Step through the stages for the original PC and for a modern one. The modern timings come from the computer this book was written on.

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:

These are the actual 512 bytes. Click any byte to see what it is. Then press Power on and step through the program. Finally, break the signature and try again.

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:

An emulated PC screen: SeaBIOS version line, then 'Booting from Floppy...' and 'Hello from the boot sector!'
Our boot sector, running in QEMU. The lines above it are printed by the BIOS.
The same emulated PC reporting 'Booting from Floppy... Boot failed: not a bootable disk'
The same sector with its last two bytes changed to 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:

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:

  1. Shows a menu if there's more than one operating system or kernel to choose from.
  2. 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.
  3. Passes it a command line, a few settings such as which disk holds the root file system.
  4. 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.

Climb the ladder one step at a time and watch the control bits. Every 64-bit x86 computer goes through these steps every time it starts.

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

  1. The 8088 starts at segment FFFF, offset 0000. Using segment × 16 + offset, which physical address is that?
    Show answer0xFFFF × 16 = 0xFFFF0, plus offset 0: 0xFFFF0. That's 16 bytes below the top of 1 MB (0x100000).
  2. Our boot sector is loaded at 0x7C00. At which addresses do the two signature bytes end up?
    Show answerThey are bytes 510 and 511 of the sector: 0x7C00 + 510 = 0x7DFE and 0x7DFF.
  3. An MBR has 446 bytes of code, then four partition entries, then the signature. How big is each partition entry?
    Show answer512 − 446 − 2 = 64 bytes for four entries, so 16 bytes each.
  4. Why must the reset vector point into ROM rather than RAM?
    Show answerWhen 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.
  5. Our boot sector uses int 0x10 to print. Why couldn't a UEFI bootloader running in 64-bit mode do the same?
    Show answerint 0x10 calls 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

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.