从零写 OS 内核-第四篇:进入保护模式(段式内存管理详解)¶
“保护模式 ≠ 分页!
它的第一道防线,是被现代 OS ‘悄悄绕过’的段机制。”
在前面几篇中,我们借助 GRUB 轻松启动了内核,输出了 “Hello World”,还理解了 Multiboot 协议。
但你是否想过:为什么 GRUB 要帮我们进入保护模式?它到底在保护什么?
今天,我们就来亲手完成这一历史性转变——从实模式(Real Mode)切换到保护模式(Protected Mode),并深入理解其核心:段式内存管理(Segmentation),包括 GDT、LDT 与段选择子的工作原理。
🔥 即便现代操作系统几乎“废弃”了段机制,理解它,仍是掌握 x86 架构的必经之路。
1 一、什么是实模式(Real Mode)?¶
实模式,是 x86 CPU 上电后的默认运行状态,源自 1978 年的 Intel 8086 处理器。
1.1 ✅ 特点:¶
- 20 位地址线 → 最大寻址 1MB 内存(0x00000 ~ 0xFFFFF)
- 段:偏移寻址:物理地址 = 段寄存器 × 16 + 偏移
例如:CS:IP = 0x1000:0x0020→ 物理地址 =0x10000 + 0x20 = 0x10020 - 无内存保护:任何程序可读写任意内存(包括 BIOS 和显存)
- 无特权级:所有代码运行在“上帝模式”
💡 实模式就像一辆没有方向盘、没有刹车的老式拖拉机——简单,但极其危险。
1.2 ❓ 为什么现代 CPU 还要从实模式启动?¶
为了兼容!
PC 兼容机必须能运行 1980 年代的老 DOS 程序,所以 CPU 上电后“假装自己是 8086”。
2 二、什么是保护模式(Protected Mode)?¶
保护模式由 Intel 80286(1982)引入,80386 起成为现代操作系统的基石。
2.1 ✅ 核心优势:¶
| 特性 | 说明 |
|---|---|
| 32 位寻址 | 可访问 4GB 内存(后续通过 PAE/64 位扩展) |
| 内存保护 | 防止程序越界访问(段界限、权限检查) |
| 特权级(Ring 0~3) | 内核(Ring 0) vs 用户程序(Ring 3) |
| 虚拟内存基础 | 为分页(Paging)提供支持 |
🌟 保护模式下的内存访问,不再是“直接物理地址”,而是通过 段描述符表 进行翻译和校验。
3 三、关键机制:段描述符与段寄存器¶
在保护模式中,段寄存器(CS、DS、SS 等)不再直接存地址,而是存一个 段选择子(Segment Selector)。
3.1 🔍 段选择子结构(16 位):¶
| 15 3 | 2 | 1 | 0 |
| Index | TI| RPL|
3.2 📦 段描述符(64 位)¶
由 Index 指向的描述符,包含: - 段基地址(32 位) - 段界限(Limit) - 类型(代码/数据/只读/可执行等) - DPL(Descriptor Privilege Level) - G 位(粒度:0=字节,1=4KB)
✅ CPU 通过 GDT(全局描述符表) 或 LDT(局部描述符表) 查找描述符。
4 四、GDT vs LDT:谁来管理“段”?¶
| 对比项 | GDT(Global Descriptor Table) | LDT(Local Descriptor Table) |
|---|---|---|
| 作用范围 | 全局,所有任务共享 | 局部,每个任务可有自己的 LDT |
| 数量 | 系统只有一个 | 可有多个(每个进程一个) |
| 使用频率 | 极高(内核、用户代码/数据段) | 极低(现代 OS 几乎不用) |
| 切换方式 | LGDT 指令加载 |
通过 GDT 中的一个特殊描述符指向 LDT |
📌 现代操作系统(如 Linux)几乎完全弃用 LDT,转而依赖分页机制实现内存隔离。
但在进入保护模式初期,GDT 是必须的!
5 五、实战:如何从实模式进入保护模式?¶
即使你用 GRUB 启动(它已切换),手动实现一次,才能真正理解。
以下是裸机环境下切换的完整步骤:
5.1 步骤 1:关闭中断¶
cli ; 禁用可屏蔽中断
5.2 步骤 2:加载 GDT¶
先定义 GDT(通常在汇编中):
gdt_start:
dq 0x0 ; 空描述符(必须)
gdt_code:
dw 0xFFFF ; 段界限 0-15
dw 0x0 ; 基地址 0-15
db 0x0 ; 基地址 16-23
db 10011010b ; 类型:代码段,可读,DPL=0
db 11001111b ; 粒度:4KB,32位
db 0x0 ; 基地址 24-31
gdt_data:
dw 0xFFFF
dw 0x0
db 0x0
db 10010010b ; 数据段,可写,DPL=0
db 11001111b
db 0x0
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; GDT 大小
dd gdt_start ; GDT 起始地址
加载 GDT:
lgdt [gdt_descriptor]
5.3 步骤 3:设置 CR0 的 PE 位(Protection Enable)¶
mov eax, cr0
or eax, 0x1 ; 设置 bit 0 (PE)
mov cr0, eax
5.4 步骤 4:远跳转,刷新 CS 段寄存器¶
jmp 0x08:protected_mode_start ; 0x08 = GDT 中代码段选择子(Index=1, TI=0, RPL=0)
🔥 这一步至关重要!
因为jmp是远跳转(far jump),会同时加载CS和EIP,从而让 CPU 用新的段描述符解释后续指令。
5.5 步骤 5:设置其他段寄存器¶
mov ax, 0x10 ; 数据段选择子(Index=2)
mov ds, ax
mov es, ax
mov ss, ax
; 注意:不能直接 mov cs, ax!
5.6 步骤 6:进入 32 位代码段(NASM 需用 [bits 32])¶
[bits 32]
protected_mode_start:
; 现在你已在保护模式!
call kernel_main
6 六、为什么 GRUB 要帮我们做这些?¶
因为: - 实模式下无法访问 1MB 以上内存 - 无法实现内存保护和多任务 - 现代操作系统必须运行在保护模式(或长模式)下
GRUB 在加载 Multiboot 内核前,已经完成了: 1. 加载 GDT 2. 设置 CR0.PE = 1 3. 远跳转到 32 位代码段 4. 设置好段寄存器
所以你的内核代码天然运行在保护模式——但这不等于你可以忽略它!
7 七、下一步:准备开启分页!¶
进入保护模式只是第一步。
接下来,你需要:
- 启用分页(Paging) → 实现虚拟内存
- 设置中断描述符表(IDT) → 处理异常和硬件中断
- 初始化堆和内存管理器 → 为 kmalloc 打基础
而这一切,都建立在你已掌握保护模式的前提之上。
8 写在最后¶
从实模式到保护模式,
不仅是 CPU 工作方式的改变,
更是操作系统从“玩具”走向“系统”的分水岭。
今天你写的这几行汇编,
正是 Linux、Windows、macOS 内核启动时也曾走过的路。
🌟 理解历史,才能创造未来。
📬 动手试试吧!
即使你不写裸机启动代码,也建议在 QEMU 中模拟一次手动切换过程。
欢迎在评论区分享你的 GDT 定义或遇到的坑!
👇 下一篇你想看:分页机制详解,还是 中断与异常处理(IDT)?
#操作系统 #内核开发 #x86 #实模式 #保护模式 #GDT #LDT #段机制 #从零开始
📢 彩蛋:关注后回复关键词 “gdt”,获取: - 完整 GDT 汇编模板(含注释) - 实模式 → 保护模式切换的 QEMU 调试脚本 - Intel 手册相关章节摘录(中文)