Boot Loading and CPU Isolation

Why This Matters

Before a single line of kernel code runs, a carefully orchestrated sequence of low-level hardware events must take place. Understanding the boot process demystifies what happens in the first milliseconds after power-on, and the CPU isolation mechanisms introduced here—privilege rings and segmentation—underpin every security guarantee your OS makes. If an attacker can break isolation, they own the machine. If you understand how isolation is enforced in hardware, you can reason about where it can and cannot be relied upon.


The Boot Sequence

Firmware: BIOS and UEFI

The first software to run on an x86 machine is firmware stored in ROM—either the legacy BIOS (Basic Input/Output System) or the modern UEFI (Unified Extensible Firmware Interface). The firmware initializes hardware and then looks for something to boot.

BIOS follows a simple convention: it scans bootable devices (hard disk, USB, CD-ROM) in order, reads the very first sector (512 bytes, called the Master Boot Record) into physical memory at address 0x7C00, and checks that the last two bytes of that sector are the boot signature 0x55 0xAA. If the signature is present, the BIOS transfers control to that address. If not, the device is skipped. This signature is purely a marker—it does not verify any checksum or authenticate the code.

The Bootloader

The 512-byte program loaded at 0x7C00 is the bootloader. In xv6 it is compiled from two source files:

The bootloader's job is narrow but critical:

  1. Switch from real mode to protected mode. The CPU starts in 16-bit real mode on every power-on. The bootloader sets the CR0_PE (Protection Enable) bit in control register CR0 and executes a long jump (ljmp) to flush the instruction pipeline, transitioning the CPU to 32-bit protected mode.
  2. Load the kernel ELF from disk. bootmain() reads the kernel starting at disk sector 1 and copies it to physical address 0x100000 (1 MiB).
  3. Jump to the kernel entry point. The ELF header tells bootmain exactly where to jump.

xv6 Disk Layout

Sector offset Content
0 (0x000000) Bootloader (bootblock, 512 B)
1 (0x000200) Kernel ELF image
File system

The Makefile assembles this image with dd:

xv6.img: bootblock kernel fs.img
    dd if=/dev/zero of=xv6.img count=10000
    dd if=bootblock of=xv6.img conv=notrunc
    dd if=kernel of=xv6.img seek=1 conv=notrunc

Physical Memory Layout at Boot

Address Content
0x00007C00 Bootloader code (loaded by BIOS)
0x00100000 Kernel ELF (loaded by bootmain)
0x00100020 Kernel entry point (e.g., entry.S)

CPU Isolation: Privilege Rings

Modern x86 processors define four privilege levels called rings, numbered 0–3. Only rings 0 and 3 are used in practice:

Ring Name Who runs here
0 Kernel mode OS kernel — most privileged
3 User mode User applications — least privileged

The current privilege level (CPL) is stored in the lowest two bits of the %cs (Code Segment) register. When CPL = 0, the CPU is in ring 0; when CPL = 3, it is in ring 3.

Ring 0 protects:

User code in ring 3 that attempts any of these operations triggers a general protection fault, which the kernel handles.


x86 Segmentation

Real Mode (16-bit)

In real mode, memory addresses are computed as:

physical address = [segment register] × 16 + offset

For example, if %cs = 0x0010 and %ip = 0x00aa:

0x0010 × 16 = 0x0100
0x0100 + 0x00aa = 0x01aa   ← physical address

There is no paging and no hardware protection between programs.

Protected Mode / Long Mode (32/64-bit)

In protected mode and 64-bit long mode, segment registers (%cs, %ds, %ss, %es, %fs, %gs) hold selectors, not base addresses. A selector is an index into the Global Descriptor Table (GDT), plus two bits that encode the Requested Privilege Level (RPL).

The format of a selector stored in %cs:

Bits 15:3 — GDT index
Bit  2    — Table indicator (0 = GDT, 1 = LDT)
Bits 1:0  — CPL (Current Privilege Level)

So if %cs = 0x2b = 0b0010_1011:

Segment Descriptors

Each GDT entry (segment descriptor) encodes:

xv6 Flat Segmentation

xv6 uses flat segmentation: all segments have base = 0 and limit = unlimited. This means segmentation adds no address translation—virtual addresses equal physical addresses (before paging). The GDT's real job is to carry DPL for enforcement.

main() calls seginit() to initialize the GDT:

gdt[SEG_KCODE] = SEG((STA_X|STA_R), 0, 0, APP_SEG, !DPL_USER, 1);  // ring 0, executable
gdt[SEG_KDATA] = SEG(STA_W,         0, 0, APP_SEG, !DPL_USER, 0);  // ring 0, writable
gdt[SEG_UCODE] = SEG((STA_X|STA_R), 0, 0, APP_SEG, DPL_USER,  1);  // ring 3, executable
gdt[SEG_UDATA] = SEG(STA_W,         0, 0, APP_SEG, DPL_USER,  0);  // ring 3, writable

The selector constants in mmu.h bake the DPL into the lowest bits:

#define USER_CS  ((SEG_UCODE<<3)|DPL_USER)   // user code selector
#define KERNEL_CS (SEG_KCODE<<3)              // kernel code selector

Switching Between Rings: System Calls

User code cannot change its own CPL—the hardware forbids it. The only sanctioned way to enter the kernel from user space is via a controlled transfer:

Direction Instruction Effect on CPL
User → Kernel syscall (or int n) CPL 3 → 0; %csKERNEL_CS
Kernel → User sysret (or iretq) CPL 0 → 3; %csUSER_CS

You can observe this in GDB. Set a breakpoint in a user-space binary's main(), then single-step through a syscall instruction: the CPL bits in %cs flip from 11 to 00 as execution enters the kernel, then flip back to 11 on sysret.


Key Takeaways

Practice

  1. What is the purpose of the two-byte signature 0x55 0xAA at the end of the boot sector?
  2. To which physical memory address does the BIOS load the 512-byte boot sector?
  3. In x86 real mode, if the %cs register holds 0x0010 and %ip holds 0x00AA, what is the physical memory address the CPU will access?
  4. In xv6, after the bootloader finishes executing, where has it placed the kernel ELF image in physical memory?
  5. What Current Privilege Level (CPL) does the CPU operate at when running in Ring 0, and what code typically runs there?
  6. In xv6's bootasm.S, the bootloader sets the CR0_PE bit before executing a long jump (ljmp). What does setting CR0_PE accomplish?
  7. During execution in xv6, you observe that %cs = 0x2B. (a) What is the Current Privilege Level (CPL)? Show your work. (b) Which GDT entry index is being referenced?
  8. Explain precisely what the CPU does to the %cs register when a user-space program executes the syscall instruction, and what the reverse instruction (sysret) does when the kernel returns. Why can't user code perform these transitions itself without using these instructions?