B04 — From Real Mode to Protected Mode: GDT and CR0 Setup
Key Insight: Protected mode doesn’t “protect” memory. It allows the CPU to manage more complex memory access rules.
Limitations of Real Mode
After reset, the CPU defaults to Real Mode, the operating mode from the 8086 era. Real Mode has two severe limitations:
实模式的问题:
1. 地址空间只有 1MB(20-bit 地址)
- 段寄存器 × 16 + 偏移量 = 物理地址
- 0xFFFF:0xFFFF = 0x10FFEF(实际是 1MB + 64KB - 16B)
- 无法访问超过 1MB 的内存
2. 没有内存保护
- 任何代码可以访问任何内存地址
- 没有特权级(Privilege Level)
- 恶意代码可以覆盖 BIOS 所在的 ROM
保护模式(Protected Mode)解决了这两个问题:
- 32-bit 地址空间(4GB)
- 特权级(Ring 0~Ring 3)
- 分页(Paging)支持虚拟内存The Core of Protected Mode: GDT
GDT (Global Descriptor Table) is the core data structure of Protected Mode. It defines the attributes of segments.
GDT 入口(Segment Descriptor,8 bytes):
63~56 | 55 | 54 | 53 | 52 | 47~44 | 43~40 | 39~36 | 35~32
Base[31:24] | G |DB |L |AVL| Limit[19:16] | P | DPL | S | Type | Base[23:16]
31~16 15~0
Base[15:0] Limit[15:0]
字段解释:
Base(24-bit 或 32-bit):段基址
Limit(20-bit):段大小(粒度 G=1 时单位是 4KB)
P(Present):段是否在内存中(必须=1)
DPL(Descriptor Privilege Level):特权级(0~3)
S(System):系统段(0)还是普通段(1)
Type(4-bit):段的类型(可读/写/执行,扩展方向)
G(Granularity):0=字节,1=4KB
DB(Default Size):0=16-bit,1=32-bit(段寄存器大小)Minimum GDT Configuration (3 segments):
Linux 内核早期使用的临时 GDT:
// 伪描述符结构
struct gdt_page {
struct desc_struct gdt[16];
} __attribute__((aligned(4096)));
// GDT 初始化
gdtr.base = (unsigned long)gdt; // GDT 基址
gdtr.limit = sizeof(gdt) - 1; // GDT 大小
// 三个必须的定义段:
0: NULL descriptor(必须)
1: KERNEL CODE (Ring 0, 4GB flat, 0xCF9A)
2: KERNEL DATA (Ring 0, 4GB flat, 0xCF92)
// 0xCF9A 的含义(Code Segment, DPL=0):
// P=1, DPL=00, S=1, Type=0xA(可执行/可读/RTU)
// G=1, DB=1, L=0, AVL=0
// Limit=0xFFFFF(4GB)
// 0xCF92 的含义(Data Segment, DPL=0):
// P=1, DPL=00, S=1, Type=0x2(可读写/RTU)
// G=1, DB=1, L=0, AVL=0
// Limit=0xFFFFF(4GB)Segment Selector
Code uses Segment Selectors to reference segments in the GDT:
段选择子(16-bit)的结构:
15─────────3 2 1─────────0
Index TI RPL
Index:GDT 中的索引(×8 = 偏移量)
TI:0=GDT, 1=LDT(Local Descriptor Table)
RPL:Requested Privilege Level(请求特权级)
例子:
Selector = 0x10(二进制 0001 0000)
Index = 0b00010 = 2
TI = 0(GDT)
RPL = 0(Ring 0)
→ 引用 GDT[2],即 KERNEL DATA 段
Selector = 0x08(二进制 0000 1000)
Index = 0b00001 = 1
→ 引用 GDT[1],即 KERNEL CODE 段CR0: Switching Control Registers
To switch from Real Mode to Protected Mode, you need to set CR0 (Control Register 0):
CR0 寄存器(32-bit):
Bit 31(PG):Paging Enable
Bit 30(PDE):Page Directory Extensions(已废弃)
Bit 29(CD):Cache Disable
Bit 28(NW):Not Write-through
Bit 18(AM):Alignment Mask
Bit 05(NA):Numeric Assistance
Bit 04(WP):Write Protect(User 不能写 Kernel 页面)
Bit 03(SN):Supervisor Mode Ignore Numeric Exception
Bit 02(ET):Math Coprocessor Extension Type(1=387, 0=287)
Bit 01(TS):Task Switched(协处理器上下文切换)
Bit 00(PE):Protection Enable ← 关键位!
设置 Protection Enable(切换到保护模式):
; 假设 GDT 已经建好
lgdt [gdtr] ; 加载 GDT(GDTR 寄存器)
mov eax, cr0
or eax, 0x00000001 ; 设置 PE 位
mov cr0, eax ; 切换到保护模式!
; 此时 CS 还是实模式的段
; 需要用 far jmp 来刷新流水线并加载新的 CS
jmp 0x08:flush_cs ; Selector=0x08(KERNEL CODE),跳到新 CS
flush_cs:
; 现在 CS = 0x08(GDT[1] = KERNEL CODE,Ring 0,4GB flat)
; 接下来可以跳到 32-bit 保护模式代码Enabling Paging
Protected Mode also supports Paging, which is the foundation of modern operating systems' virtual memory:
分页的开启顺序:
1. 先建立页表(页目录/页表/页全局目录)
2. 设置 CR3 寄存器(指向页目录)
3. 设置 CR0.PG 位(开启分页)
页表结构(x86-64):
PML4(Page Map Level 4)→ PDPT → PD → PT
每一级都是 512 个条目(每个条目 8 bytes)
CR3 指向 PML4 的基址(PML4 Base Address)
x86-64 Linux 使用:
47-bit 虚拟地址
页面大小 4KB
四级页表(PML4 → PDPT → PD → PT)
开启分页:
; 假设页表已经建立,页表基址在 init_top_pgt
mov eax, 0x83 ; PG=1, RW=1, US=1(用户可访问)
mov [init_top_pgt], eax
mov eax, init_top_pgt
and eax, ~0xFFF ; 对齐到页边界
mov cr3, eax ; CR3 = 页目录基址
mov eax, cr0
or eax, 0x80000000 ; 设置 PG 位
mov cr0, eax ; 开启分页!
jmp 1f
1:
; 现在虚拟地址空间工作了
; 线性地址会通过页表翻译成物理地址Complete Mode Switch Code
从实模式到保护模式 + 分页的完整代码:
section .text
global _start
_start:
; 1. 关闭中断
cli
; 2. 加载临时 GDT
lgdt [gdt_pointer]
; 3. 设置 CR0.PE = 1(开启保护模式)
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
; 4. Far Jmp 刷新 CS 并跳转到 32-bit 代码
jmp 0x08:protected_mode
protected_mode:
; 5. 设置数据段寄存器
mov ax, 0x10 ; Selector = KERNEL DATA
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; 6. 设置栈
mov esp, stack_top ; 栈顶在 boot_stack
; 7. 设置 CR3,开启分页
mov eax, page_table_base
mov cr3, eax
mov eax, cr0
or eax, 0x80000000 ; 设置 PG 位
mov cr0, eax
; 8. 跳入更高层的内核代码
jmp start_kernel
; GDT 定义
align 4
gdt:
dq 0x0000000000000000 ; GDT[0] = NULL
dq 0x00CF9A000000FFFF ; GDT[1] = KERNEL CODE
dq 0x00CF92000000FFFF ; GDT[2] = KERNEL DATA
gdt_pointer:
dw gdt_pointer - gdt - 1 ; Limit
dd gdt ; Base
page_table_base: equ 0x1000 ; 早期页表放在 0x1000
stack_top: equ 0x8000 ; 早期栈顶Summary
- Real Mode Limits: 1MB address space; no memory protection
- GDT: Global Descriptor Table, 8-byte segment descriptors defining base/limit/type/privilege level
- Segment Selector: 16-bit = Index (x8) + TI (0=GDT) + RPL (Requested Privilege Level)
- CR0.PE: Enables Protected Mode; switches from Real Mode to Protected Mode
- Far Jmp: After switching to Protected Mode, a far jump is required to flush CS; otherwise the pipeline still uses old state
- CR0.PG: Enables Paging; virtual addresses translated to physical via page tables
Next (B05): How does the MMU translate virtual addresses to physical addresses? Single-level page tables, multi-level page tables, and TLB caching.
Comments