B04 —— 从实模式到保护模式:GDT 和 CR0 的设置
金句:保护模式不是”保护”内存,而是让 CPU 能够管理更复杂的内存访问规则。
实模式的局限
CPU 复位后默认是 实模式(Real Mode),这是 8086 时代的工作模式。实模式有两个严重限制:
实模式的问题:
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)支持虚拟内存保护模式的核心:GDT
GDT(Global Descriptor Table) 是保护模式的核心数据结构,它定义了"段"的属性。
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(段寄存器大小)GDT 的最小配置(3 个段):
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)段选择子(Selector)
代码用 段选择子(Selector) 来引用 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:控制寄存器的切换
从实模式切换到保护模式,需要设置 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 保护模式代码分页(Paging)的开启
保护模式还支持 分页(Paging),这是现代操作系统的虚拟内存基础:
分页的开启顺序:
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:
; 现在虚拟地址空间工作了
; 线性地址会通过页表翻译成物理地址完整的模式切换代码
从实模式到保护模式 + 分页的完整代码:
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 ; 早期栈顶总结
- 实模式限制:1MB 地址空间,无内存保护
- GDT:Global Descriptor Table,8-byte 段描述符,定义基址/限长/类型/特权级
- 段选择子:16-bit = Index(×8)+ TI(0=GDT)+ RPL(请求特权级)
- CR0.PE:开启保护模式(Protection Enable),从实模式切换到保护模式
- Far Jmp:切换保护模式后必须 far jmp 刷新 CS,否则流水线还是旧的
- CR0.PG:开启分页(Paging),虚拟地址通过页表翻译成物理地址
下篇预告(B05):MMU(内存管理单元)是怎么把虚拟地址翻译成物理地址的?一级页表、多级页表、以及 TLB(Translation Lookaside Buffer)缓存。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论