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 0xaa55The 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:
- Compiler flags.
-m64instead of-m32;-mcmodel=kernel, which tells gcc the kernel is linked in the top 2 GiB of the address space so that it can use 32-bit sign-extended addresses;-mno-red-zone, because the AMD64 ABI lets a function use 128 bytes below RSP without moving RSP, and an interrupt would overwrite them (see chapter 17); and-mno-sse -mno-mmxor their equivalent, so that the compiler does not emit vector instructions before the kernel has enabled the registers (CR4.OSFXSR and friends).ld -m elf_x86_64andOUTPUT_FORMAT(elf64-x86-64)in the linker script replace the i386 versions. - The calling convention. Arguments arrive in
rdi,rsi,rdx,rcx,r8,r9; thecdeclreasoning of chapter 4 about the stack still applies to the seventh argument and beyond. Every piece of assembly that calls C or is called by C (the ISR stubs of chapter 11,context_switchof chapter 13) follows the System V AMD64 ABI, andrbx,rbp,r12tor15are the callee-saved registers. - The 64-bit interrupt frame. Gate descriptors are 16
bytes (Intel 7.14.1 “64-Bit Mode IDT”; AMD 8.9.1), the processor pushes
SS and RSP unconditionally, even without a privilege change,
and aligns the stack to 16 bytes before pushing (Intel 7.14.2 “64-Bit
Mode Stack Frame”; AMD 8.9.3 “Interrupt Stack Frame”);
iretqpops it. Thestruct registersof chapter 11 becomes fifteen 64-bit general registers plus the frame, andpush/popof segment registers is gone. - No segment bases. The TSS of chapter 13 shrinks to
the 64-bit TSS (Intel 10.7 “Task Management in 64-bit Mode”; AMD 12.2.5
“64-Bit Task State Segment”): no register image, just
RSP0for the ring 3 to ring 0 switch and the seven interrupt stack table entries that let a double fault run on a known-good stack. There is one GDT for all processes, with four or five entries, and the user code segment has L = 1 and DPL = 3. - System calls.
int 0x80still works and is the smallest change, butsyscallis the point of the mode: setEFER.SCE, write the kernel entry point intoIA32_LSTAR, the code selectors intoIA32_STARand the flags to clear intoIA32_FMASK(Intel 6.8.8; AMD 6.1.1, with the same registers under AMD’s namesSTAR,LSTAR,SFMASK), and remember thatsyscalldoes not switch stacks: the entry stub must load a kernel stack pointer itself, withswapgsand a per-processor pointer inKERNEL_GS_BASE, before it pushes anything. - Page tables.
paging.cof chapter 12 grows one level, entries becomeuint64_t, and a kernel usually maps itself at0xFFFF_FFFF_8000_0000(the top 2 GiB that-mcmodel=kernelassumes) with 2 MiB pages, while user programs stay below. The frame allocator and the heap do not change.
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.