Appendix C A minimal long-mode bootstrap

Chapter 17 lists the five steps that take a processor from protected mode to 64-bit mode and says they are about a dozen instructions. This appendix is those instructions. It is a single boot sector, with no kernel behind it, that enables A20, enters protected mode as chapter 9 did, builds the smallest possible set of 64-bit page tables, flips the three bits that activate long mode, far-jumps into 64-bit code, and prints Hello from long mode on the serial port and on the VGA screen. It is in code/appendix-c-longmode/os with the usual make, make qemu, make gdb, make test and make clean, and it is tested by the book’s continuous integration like every chapter. Keep both manuals at hand: Intel SDM Volume 3A, section 12.8.5 “Initializing IA-32e Mode”, and AMD APM Volume 2 (AMD 2026), section 14.6 “Enabling and Activating Long Mode”, with its worked example in section 14.8 “Long-Mode Initialization Example”. Everything below was verified against both.

C.1 The code

longmode.asm

;******************************************************************************
; longmode.asm -- appendix C: a minimal long-mode bootstrap
;
; The BIOS loads this sector at 0000:7C00 in real mode.  In one sector it
;   1. sets up segments and a stack, and enables A20 (as in chapter 9),
;   2. loads a GDT with a 32-bit and a 64-bit code descriptor and enters
;      protected mode (chapter 9),
;   3. builds page tables that identity-map the first 2 MiB with one
;      2 MiB page (PML4 -> PDPT -> PD),
;   4. sets CR4.PAE, loads CR3, sets EFER.LME, sets CR0.PG,
;   5. far-jumps to the 64-bit code segment, prints a line on COM1 and on
;      the VGA text screen, and halts.
; Intel SDM Vol. 3A, section 12.8.5 "Initializing IA-32e Mode";
; AMD APM Vol. 2, section 14.6 "Enabling and Activating Long Mode".
;******************************************************************************
bits 16

PML4    equ 0x1000              ; the three page tables live at 0x1000..0x3FFF,
PDPT    equ 0x2000              ; free conventional memory below the boot
PD      equ 0x3000              ; sector (code/README.md, "Memory map")
COM1    equ 0x3f8
VGA     equ 0xb8000
EFER    equ 0xc0000080          ; IA32_EFER MSR (Intel 2.2.1; AMD 3.1.7)

;------------------------------------------------------------------------------
; 1. Real mode: segments, stack, A20 (chapter 9, steps 1 and 3).
;------------------------------------------------------------------------------
start:
    cli
    cld
    xor     ax, ax
    mov     ds, ax
    mov     es, ax
    mov     ss, ax
    mov     sp, 0x7c00
    in      al, 0x92            ; fast A20 gate (port 92h, bit 1)
    or      al, 0x02
    and     al, 0xfe
    out     0x92, al

;------------------------------------------------------------------------------
; 2. Protected mode (Intel 12.9.1): LGDT, CR0.PE, far jump.
;------------------------------------------------------------------------------
    lgdt    [gdt_descriptor]
    mov     eax, cr0
    or      eax, 1              ; CR0.PE
    mov     cr0, eax
    jmp     GDT_CODE32:protected_mode

bits 32
protected_mode:
    mov     ax, GDT_DATA
    mov     ds, ax
    mov     es, ax
    mov     ss, ax
    mov     esp, 0x7c00

;------------------------------------------------------------------------------
; 3. Page tables.  Long mode needs 4-level tables (Intel 5.5; AMD 5.3):
;    CR3 -> PML4 -> PDPT -> PD -> (PT ->) page.  With the PS bit (bit 7) set
;    in a page-directory entry, the entry maps a 2 MiB page directly
;    (Intel Table 5-18; AMD 5.3.4), so three tables with one entry each
;    identity-map physical 0..2 MiB.  Entry bits: 0 = present, 1 = writable.
;------------------------------------------------------------------------------
    mov     edi, PML4
    xor     eax, eax
    mov     ecx, 3 * 4096 / 4   ; clear the three tables
    rep     stosd
    mov     dword [PML4], PDPT | 0x03   ; PML4E 0 -> PDPT, present, writable
    mov     dword [PDPT], PD   | 0x03   ; PDPTE 0 -> PD
    mov     dword [PD],   0    | 0x83   ; PDE 0: 2 MiB page at 0, PS|RW|P

;------------------------------------------------------------------------------
; 4. The switch, in the order of Intel 12.8.5 / AMD 14.6.1:
;    CR4.PAE, CR3, EFER.LME (through WRMSR), then CR0.PG.
;    After CR0.PG the processor is in IA-32e/long mode, EFER.LMA = 1,
;    but CS still selects a 32-bit segment: compatibility mode.
;------------------------------------------------------------------------------
    mov     eax, cr4
    or      eax, 1 << 5         ; CR4.PAE (Intel 2.5; AMD 3.1.3)
    mov     cr4, eax
    mov     eax, PML4
    mov     cr3, eax            ; physical address of the PML4 (AMD 5.3.2)
    mov     ecx, EFER
    rdmsr                       ; EDX:EAX = MSR[ECX]
    or      eax, 1 << 8         ; EFER.LME (bit 8)
    wrmsr
    mov     eax, cr0
    or      eax, 1 << 31        ; CR0.PG; EFER.LMA becomes 1
    mov     cr0, eax

;------------------------------------------------------------------------------
; 5. 64-bit mode: a far jump to a code segment whose descriptor has L = 1
;    (Intel 3.4.5 and 12.8.5.3; AMD 4.8.1 and Table 14-4).
;------------------------------------------------------------------------------
    jmp     GDT_CODE64:long_mode

bits 64
long_mode:
    mov     ax, GDT_DATA        ; DS/ES/SS bases are ignored in 64-bit mode,
    mov     ds, ax              ; but the selectors must be valid or null
    mov     es, ax
    mov     ss, ax
    mov     rsp, 0x7c00

    ; COM1: no interrupts, DLAB=1, divisor 1 (115200 bauds), 8N1 (chapter 10)
    mov     dx, COM1 + 1
    xor     al, al
    out     dx, al
    mov     dx, COM1 + 3
    mov     al, 0x80
    out     dx, al
    mov     dx, COM1
    mov     al, 1
    out     dx, al
    mov     dx, COM1 + 1
    xor     al, al
    out     dx, al
    mov     dx, COM1 + 3
    mov     al, 0x03
    out     dx, al

    mov     rsi, message        ; a 64-bit address, to prove the point
    mov     rdi, VGA            ; top-left corner of the text screen
.next_char:
    lodsb
    test    al, al
    jz      .halt
    mov     ah, 0x0f            ; white on black
    stosw                       ; VGA: character, attribute
    mov     bl, al
    mov     dx, COM1 + 5
.wait_thr:
    in      al, dx              ; line status register, bit 5: THR empty
    test    al, 0x20
    jz      .wait_thr
    mov     dx, COM1
    mov     al, bl
    out     dx, al
    jmp     .next_char
.halt:
    cli
    hlt
    jmp     .halt

;------------------------------------------------------------------------------
; Data
;------------------------------------------------------------------------------
message: db "Hello from long mode", 13, 10, 0

; GDT.  In 64-bit code segments only L, D, P, DPL and the type matter; base
; and limit are ignored (Intel 3.4.5, 6.2.1 "Code-Segment Descriptor in
; 64-bit Mode"; AMD 4.8.1).  Byte 6 holds the flags: G=1,D/B=1 is 0xC for
; the 32-bit segments; L=1,D=0 is 0x2 for the 64-bit one.
align 8
gdt:
    dq 0                                        ; 0x00: null
    dw 0xffff, 0x0000, 0x9a00, 0x00cf           ; 0x08: 32-bit code, flat
    dw 0xffff, 0x0000, 0x9200, 0x00cf           ; 0x10: data, flat
    dw 0x0000, 0x0000, 0x9a00, 0x0020           ; 0x18: 64-bit code, L=1
gdt_end:
GDT_CODE32 equ 0x08
GDT_DATA   equ 0x10
GDT_CODE64 equ 0x18

gdt_descriptor:
    dw gdt_end - gdt - 1
    dd gdt

times 510 - ($ - $$) db 0
dw 0xaa55

The assembled sector is 512 bytes with about 150 to spare. Three things are new compared with the bootloader of chapter 9, and they are the subject of the next three sections: a fourth descriptor in the GDT, three page tables, and the eleven instructions between mov eax, cr4 and the far jump.

C.2 The 64-bit code descriptor

The descriptor at selector 0x18 is 0000 0000 9a00 0020. Chapter 9 taught you to read the eight bytes: limit 0, base 0, access byte 0x9A (present, DPL 0, code, readable), and flags nibble 0x2. In the 32-bit descriptors the flags nibble is 0xC: G = 1 (4 KiB granularity) and D/B = 1 (32-bit default operand size). Here it is 0x2: G = 0, D = 0, and bit 1 of the nibble, bit 53 of the descriptor, set. That bit was reserved in the 32-bit architecture and Intel SDM Volume 3A, section 3.4.5 “Segment Descriptors” now calls it L (64-bit code segment); AMD APM Volume 2, section 4.8.1 “Code-Segment Descriptors” shows the long-mode layout in figure 4-20 and describes the “Long (L) Attribute Bit” there. Both manuals state the rule in one table (Intel 12.8.5.3; AMD Table 14-4 “Processor Operating Modes”): with long mode active, CS.L = 1 and CS.D = 0 is 64-bit mode; CS.L = 0 is compatibility mode, in which CS.D means what it meant in chapter 9; CS.L = 1 and CS.D = 1 is reserved.

The limit and base of this descriptor are zero and it does not matter: in 64-bit mode the processor ignores them for CS, DS, ES and SS (Intel 3.4.5 and 6.2.1 “Code-Segment Descriptor in 64-bit Mode”; AMD 4.5.3 “Segment Registers in 64-Bit Mode”). This is the “segmentation is mostly switched off” of chapter 17; the two bases that survive are FS and GS, which 64-bit kernels use for per-processor data, and which are set through MSRs rather than descriptors. The data descriptor at 0x10 is the same as in chapter 9 and is reused after the jump: the selectors loaded into DS, ES and SS must still be valid descriptors (or null), only their base and limit are ignored.

C.3 Three page tables

Long mode cannot run without paging, and the paging must be the 4-level kind: Intel SDM Volume 3A, section 5.5 “4-Level Paging and 5-Level Paging”, AMD APM Volume 2, section 5.3 “Long-Mode Page Translation”. A 48-bit linear address is split into four 9-bit indices and a 12-bit offset: bits 47 to 39 select an entry of the PML4 (page-map level 4) pointed to by CR3, bits 38 to 30 an entry of the page-directory-pointer table, bits 29 to 21 an entry of the page directory, bits 20 to 12 an entry of the page table, and the remaining 12 bits are the offset in a 4 KiB page. Every table is one 4 KiB page of 512 entries of 8 bytes. The entry formats are in Intel Tables 5-15 (PML4E), 5-17 (PDPTE), 5-19 (PDE) and 5-20 (PTE), and in AMD figures 5-20 to 5-23; they are the 32-bit entries of chapter 12 widened: bit 0 present, bit 1 writable, bit 2 user, bit 5 accessed, bit 6 dirty, bit 7 page size, bits 12 and up the physical address, and bit 63 the new execute-disable bit (Intel 5.6; AMD 5.6.3), usable once EFER.NXE is set.

Bit 7 is what makes the example short. Set in a page-directory entry, it says “this entry does not point to a page table; it maps a 2 MiB page directly”, and bits 20 to 0 of the linear address become the offset (Intel Table 5-18 “Format of a Page-Directory Entry that Maps a 2-MByte Page” and figure 5-9; AMD 5.3.4 “2-Mbyte Page Translation”, figure 5-24, with the entry in figure 5-29). So three entries suffice to identity-map the first 2 MiB, which is all the memory the sector touches: entry 0 of the PML4 at 0x1000 points to the PDPT at 0x2000, whose entry 0 points to the PD at 0x3000, whose entry 0 is 0x83: physical page 0, PS, writable, present. The tables are first cleared with rep stosd, because an entry with garbage in its reserved bits raises a page fault with the RSVD bit set in the error code the first time it is used. We will see in gdb that the processor writes to these entries too.

C.4 The switch

Both manuals give the same sequence, in the same words (Intel 12.8.5, five numbered steps; AMD 14.6.1 “Activating Long Mode”, three numbered steps): with paging off, set CR4.PAE, load CR3, set EFER.LME, then set CR0.PG. The order of the first three does not matter (AMD says “in any order”); PG must be last. Our code arrives from chapter 9’s protected mode with paging off, so step 1 of the Intel list, “disable paging”, is already done.

CR4.PAE is bit 5 of CR4 (Intel 2.5 “Control Registers”; AMD 3.1.3 “CR4 Register”). It selects the 8-byte entry format; without it the processor would read our tables as the 4-byte entries of chapter 12.

CR3 receives the physical address of the PML4 (Intel Table 5-12; AMD 5.3.2 “CR3”). The mov cr3, eax is executed in 32-bit mode, so only 32 bits are written: this is why both manuals say the tables must lie below 4 GiB until long mode is active, and why a real kernel builds its first tables in low memory and relocates later.

EFER is a model-specific register, read with rdmsr and written with wrmsr with the register number in ECX and the value in EDX:EAX (Intel 2.8.10 “Reading and Writing Model-Specific Registers” and the instruction pages of Volume 2B; AMD 3.2 “Model-Specific Registers (MSRs)” and the RDMSR and WRMSR pages in APM Volume 3, chapter 4). Its number is C000_0080h on both vendors because AMD defined it; its layout is in Intel figure 2-4 and AMD figure 3-9: bit 0 SCE enables syscall, bit 8 LME enables long mode, bit 10 LMA is set by the processor when long mode becomes active, bit 11 NXE enables the execute-disable bit. We read the register, set bit 8, and write it back, which is what AMD’s note on LMA asks for (“software must read the EFER register … change any other bits as required and then write”). A processor without long mode has no LME bit to set; writing it raises #GP, and a careful bootloader checks cpuid first (leaf 8000_0001h, EDX bit 29, Intel 5.1.4 “Enumeration of Paging Features by CPUID”; AMD APM Volume 3, section E.4.2).

CR0.PG is bit 31, as in chapter 12. The moment it is set, the processor is in long mode and sets EFER.LMA itself; the consistency checks of Intel 12.8.5 and AMD 14.6.2 run at this instruction, and would raise #GP if PAE were clear or if CS already had L = 1. The instruction after mov cr0 must be a branch located in an identity-mapped page (both manuals, same sentence): ours is the far jump at 0x7c8e, in the page that PDE 0 maps. We are now in compatibility mode: 64-bit page tables, 32-bit code, the mode in which a 64-bit Linux runs a 32-bit program.

The far jump loads CS with 0x18 and the processor, reading L = 1 in the new descriptor, decodes the following bytes as 64-bit code. Nothing else changes at the jump: the registers keep their values (RSP is 0x7c00 because ESP was), the GDTR and IDTR still point to the 32-bit tables (Intel 12.8.5.1; AMD 14.6.3 “Updating System Descriptor Table References”), and interrupts are still disabled by the cli of the first instruction, which they must be, because the IDT is still the real-mode interrupt vector table and the first interrupt would be interpreted as a 16-byte 64-bit gate with, in Intel’s words, “unpredictable results” (12.8.5.2; AMD says the same in 14.6.3).

C.5 The 64-bit code

The code after long_mode: is assembled with bits 64. Note what it looks like: mov rsi, message loads a 64-bit immediate, lodsb reads through RSI, stosw writes through RDI, and in and out are unchanged. The serial port is initialized exactly as in chapter 10 (the line control register, the divisor latch, 8 bits and no parity) and each character is written to the VGA buffer at 0xB8000 and then to COM1, after waiting for bit 5 of the line status register, because the port is slower than the processor. Then hlt in a loop, with interrupts off, which under QEMU costs no CPU.

C.6 Building and running

The Makefile follows chapter 9 with three differences, all forced by the target being 64-bit:

Makefile (excerpt)

QEMU_BIN=qemu-system-x86_64

$(BUILD_DIR)/longmode.o: longmode.asm
    mkdir -p $(BUILD_DIR)
    nasm -f elf64 -F dwarf -g $< -o $@

$(ELF): $(BUILD_DIR)/longmode.o longmode.lds
    ld -m elf_x86_64 --no-warn-rwx-segments -T longmode.lds $< -o $@

test: bootdisk
    QEMU_BIN=$(QEMU_BIN) ../../../tools/serial-test.sh $(DISK_IMG) "Hello from long mode"

The object file is a 64-bit ELF (-f elf64, linked with -m elf_x86_64) although the sector contains 16-bit and 32-bit code too: nasm does not care, bits switches the encoding, and a 64-bit ELF is what gdb expects once it talks to a 64-bit target. The target is the second difference: the guest must be run by qemu-system-x86_64, not by the qemu-system-i386 of every other chapter. The i386 emulator implements a processor without long mode: its cpuid leaf 8000_0001h has bit 29 clear, wrmsr silently ignores the LME bit, and setting CR0.PG then enables plain PAE paging, in which CR3 points to a 4-entry table and our three tables are read as three levels instead of four. Under that reading the only mapped page is the first 4 KiB, the far jump at 0x7c8e is outside it, and the sector dies at the instruction after mov cr0, eax:

$ qemu-system-i386 -machine q35 -drive format=raw,file=build/disk.img,if=ide \
      -display none -serial none -no-reboot -d int
check_exception old: 0xffffffff new 0xe
     0: v=0e e=0000 i=0 cpl=0 IP=0008:00007c8e pc=00007c8e SP=0010:00007c00 CR2=00007c8e
check_exception old: 0xe new 0xd
     1: v=08 e=0000 i=0 cpl=0 IP=0008:00007c8e pc=00007c8e SP=0010:00007c00 env->regs[R_EAX]=80000011
check_exception old: 0x8 new 0xd

A page fault (vector 0e) on the instruction fetch at 0x7c8e, a double fault (08) because there is no handler, then the triple fault that -no-reboot turns into an exit. This is the QEMU trace format of chapter 11; EAX = 80000011 is the CR0 value we had just written. The third difference is the last line of the Makefile: tools/serial-test.sh starts qemu-system-i386 by default and now takes the binary from the QEMU_BIN variable, which is the only change the appendix makes outside its own directory.

make test builds the sector, boots it headless with the serial port captured, and waits for the text:

$ make clean && make test
rm -rf build
mkdir -p build
nasm -f elf64 -F dwarf -g longmode.asm -o build/longmode.o
ld -m elf_x86_64 --no-warn-rwx-segments -T longmode.lds build/longmode.o -o build/longmode.elf
objcopy -O binary build/longmode.elf build/longmode.bin
dd if=/dev/zero of=build/disk.img bs=512 count=8192 status=none
dd conv=notrunc if=build/longmode.bin of=build/disk.img bs=512 count=1 seek=0 status=none
QEMU_BIN=qemu-system-x86_64 ../../../tools/serial-test.sh build/disk.img "Hello from long mode"
serial-test: ok, found "Hello from long mode"
--- serial output ---
Hello from long mode
qemu-system-x86_64: terminating on signal 15 from pid 19 (/bin/sh)

The last line is the test script killing QEMU, which would otherwise sit in hlt forever. make qemu runs the same machine with a window, where the message is also visible in the top-left corner of the VGA screen, and with the gdb stub on port 26000.

C.7 The switch under gdb

Start make qemu in one terminal and make gdb in another. The .gdbinit is chapter 9’s with two changes: the symbol file is the 64-bit ELF, and the breakpoint is on long_mode, the first 64-bit instruction. The first thing to notice is what gdb says before the processor has executed anything:

$ make gdb
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x000000000000fff0 in ?? ()
(gdb) show architecture
The target architecture is set to "auto" (currently "i386:x86-64").
(gdb) info registers rip cs eflags
rip            0xfff0              0xfff0
cs             0xf000              61440
eflags         0x2                 [ IOPL=0 ]

The processor is at the reset vector in real mode, as in chapter 7, but QEMU’s x86-64 stub describes a 64-bit target, so gdb shows rip and 64-bit values from the start. There is no set architecture to type: with this QEMU binary gdb is always in i386:x86-64, which is right after the jump and wrong before it, exactly as i386 was right after chapter 9’s jump and wrong in real mode. The breakpoint at long_mode was set by .gdbinit before the sector was even loaded; as chapter 9 explained, QEMU’s breakpoints compare addresses, so it holds:

(gdb) b *0x7c00
Breakpoint 2 at 0x7c00: file longmode.asm, line 29.
(gdb) c
Continuing.

Breakpoint 2, start () at longmode.asm:29
29      cli
(gdb) x/3i $pc
=> 0x7c00 <start>:  cli
   0x7c01 <start+1>:    cld
   0x7c02 <start+2>:    xor    eax,eax
(gdb) info registers rip cs
rip            0x7c00              0x7c00 <start>
cs             0x0                 0
(gdb) c
Continuing.

Breakpoint 1, long_mode () at longmode.asm:100
100     mov     ax, GDT_DATA        ; DS/ES/SS bases are ignored in 64-bit mode,
(gdb) info registers rip rsp rsi rdi cs ds
rip            0x7c95              0x7c95 <long_mode>
rsp            0x7c00              0x7c00 <start>
rsi            0x0                 0
rdi            0x4000              16384
cs             0x18                24
ds             0x10                16
(gdb) x/6i $pc
=> 0x7c95 <long_mode>:  mov    ax,0x10
   0x7c99 <long_mode+4>:    mov    ds,eax
   0x7c9b <long_mode+6>:    mov    es,eax
   0x7c9d <long_mode+8>:    mov    ss,eax
   0x7c9f <long_mode+10>:   mov    esp,0x7c00
   0x7ca4 <long_mode+15>:   mov    dx,0x3f9

We are on the first instruction after the far jump: CS is 0x18, the 64-bit code selector, RSP is the 0x7c00 that ESP held, and RDI is 0x4000, where the rep stosd that cleared the three tables stopped. The disassembly at 0x7c9f reads mov esp,0x7c00 although the source says mov rsp, 0x7c00: nasm chose the shorter encoding with a 32-bit immediate, which in 64-bit mode zero-extends into the full register, and gdb shows what the bytes say. Now the control registers, which recent gdb and QEMU expose as $cr0, $cr3, $cr4 and $efer:

(gdb) p/x $cr0
$1 = 0x80000011
(gdb) p/x $cr4
$2 = 0x20
(gdb) p/x $cr3
$3 = 0x1000
(gdb) p/x $efer
$4 = 0x500

CR0 has PG (bit 31), ET (bit 4, always set) and PE (bit 0); CR4 has PAE (bit 5); CR3 points to the PML4; EFER is 0x500: bit 8, LME, which we wrote, and bit 10, LMA, which the processor set when paging went on. monitor info registers shows the same from QEMU’s side, with the hidden parts of the segment registers:

(gdb) monitor info registers
RAX=0000000080000011 RBX=0000000000000000 RCX=00000000c0000080 RDX=0000000000000000
RSI=0000000000000000 RDI=0000000000004000 RBP=0000000000000000 RSP=0000000000007c00
R8 =0000000000000000 R9 =0000000000000000 R10=0000000000000000 R11=0000000000000000
R12=0000000000000000 R13=0000000000000000 R14=0000000000000000 R15=0000000000000000
RIP=0000000000007c95 RFL=00000086 [--S--P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0010 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0018 0000000000000000 00000000 00209a00 DPL=0 CS64 [-R-]
SS =0010 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0010 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0000 0000000000000000 0000ffff 00009300 DPL=0 DS   [-WA]
GS =0000 0000000000000000 0000ffff 00009300 DPL=0 DS   [-WA]
LDT=0000 0000000000000000 0000ffff 00008200 DPL=0 LDT
TR =0000 0000000000000000 0000ffff 00008b00 DPL=0 TSS64-busy
GDT=     0000000000007d10 0000001f
IDT=     0000000000000000 000003ff
CR0=80000011 CR2=0000000000000000 CR3=0000000000001000 CR4=00000020
..... floating-point registers omitted .....
EFER=0000000000000500

Read the CS line: selector 0018, base 0, limit 0, attributes 00209a00, and QEMU’s own decoding, CS64. ECX still holds c0000080, the MSR number from the rdmsr, and RAX the value written to CR0. The IDT line is the real-mode vector table (base 0, limit 3ff), which is why interrupts must stay off. Finally the page tables, which gdb reads through the identity mapping:

(gdb) x/2gx 0x1000
0x1000: 0x0000000000002023  0x0000000000000000
(gdb) x/2gx 0x2000
0x2000: 0x0000000000003023  0x0000000000000000
(gdb) x/2gx 0x3000
0x3000: 0x00000000000000a3  0x0000000000000000

We wrote 0x2003, 0x3003 and 0x83; memory holds 0x2023, 0x3023 and 0xa3. Bit 5 is set in all three: the accessed bit, which the processor sets in every entry it walks through the first time it translates an address (Intel 5.8 “Accessed and Dirty Flags”; AMD 5.4.2 “Notes on Accessed and Dirty Bits”). The first translation was the instruction fetch of the far jump itself, so by the time the breakpoint hit, the hardware had already used our tables and left its mark.

C.8 What a C kernel needs next

The sector stops where chapter 9’s kernel begins. To continue Part III in 64-bit mode, the bootloader does what this sector does and then jumps to the ELF entry point, and the kernel changes in the following ways, each a small chapter of its own:

That list is the whole difference between the kernel of this book and a 64-bit one. None of its items is harder than what you did in Part III; they are the same mechanisms with wider fields and fewer special cases, which is what AMD set out to design.

Exercise C.1. Replace the 2 MiB page by a fourth table of 4 KiB pages: a page table at 0x4000 whose 512 entries map 0x0000 to 0x1FFFFF, with the PDE pointing to it (PS clear). Verify in gdb that the accessed bit appears in the page-table entry of the page that holds the sector, and in no other.

Exercise C.2. Clear the PS bit without adding the page table, and boot with -d int -no-reboot. Which exception is raised, at which instruction, and what does the error code say (Intel figure 5-12; AMD 8.4.2 “Page-Fault Error Code”)? Then set a reserved bit in the PDE instead and compare the error code.

Exercise C.3. Add a code path that runs before the switch: read cpuid leaf 8000_0001h and, if EDX bit 29 is clear, print no long mode through the BIOS teletype service of chapter 7 and halt. Test it with qemu-system-i386, which should now print the message instead of triple-faulting, and with qemu-system-x86_64 -cpu qemu32.