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