从零开始的操作系统-初识保护模式
引言: 不要忘了自己的初心
为什么有保护模式
实模式不安全, 真实的物理地址随便访问, 用户进程想让想指向哪片内存就去哪. 访问内存区域也非常麻烦, 超过64kb就要切换段基址, 一次只能运行一个程序…
从16位到32位的硬件演进
保护模式其实是随着新硬件的发展而提出的新模式,主要体现在从16位处理器到32位处理器的跃迁:
- 处理器位数提升: 从8086的16位发展到80386的32位
- 寻址能力扩展: 从1MB扩展到4GB的内存寻址空间
- 寄存器扩展: 通用寄存器从16位扩展到32位 (如AX→EAX)
寻址模式的根本变化
保护模式下的寻址发生了很大的区别:
- 摆脱段基址左移: 不再需要实模式下的"段基址×16+偏移"这种移位方法
- 平坦内存模型: 可以实现真正的线性地址空间,内存访问更加直观
- 灵活的基址寄存器: 可以用任意通用寄存器作为基址寄存器
指令兼容性问题
由于模式的切换,对应的机器码和指令也有一些兼容问题:
- 16位与32位指令: 需要考虑16位和32位的命令翻译
- 常见指令影响: mul、div、push等指令在不同模式下的行为可能不同
- 模式反转: 在实模式下使用eax(32位寄存器)会触发保护模式运行
全局描述符表
保护模式最重要的就是全局描述符表(GDT), 因为段的信息增加了很多, 需要提前把段定义好才能使用。就像家庭成员需要上户口一样,在户口簿上登记过才算合法。
GDT的核心作用
重点在于如何定义好GDT和进入保护模式,这样才能平坦地访问内存。
为了解决实模式存在的问题:
- 用户程序可以破坏存储代码的内存区域 -> 添加内存段
- 用户程序和系统程序的同一级别 -> 添加特权级
- 限制访问内存的范围 -> 增加约束条件
段描述符结构
这些描述内存段的属性被定义到了一个叫段描述符的结构里,占8个字节,可以参考这个链接里的讲解GDT
一个段描述符只用来定义(描述)一个内存段。代码段要占用一个段描述符、数据段和栈段等,多个内存段也要各自占用一个段描述符,这些描述符放在哪里呢?答案是放在全局描述符表。
GDT的重要特性
- 存储位置: GDT存在内存里,有一个专门的寄存器GDTR指向它,存储它的内存地址和界限,这是个48位寄存器
- 访问方式: 对于GDTR的访问需要lgdt指令,lgdt的指令格式是:lgdt 48位内存数据
- 容量限制: GDT中可容纳8192个段或门
- 第0个描述符: GDT的第0个描述符是不可用的,这是为了防止忘记初始化选择子的问题 —— 当选择子为0时,会触发异常,这是一种安全机制
段选择子
段寄存器在保护模式下,变成了段选择子,详细结构见p167。段选择子不再直接存储段基址,而是作为索引指向GDT中的具体段描述符。
特权级保护机制
同时进入保护模式后,考虑了一些特权级的问题,让程序更加安全:
- 4个特权级: Ring0(内核级) 到 Ring3(用户级)
- 特权级检查: CPU会自动检查代码段、数据段的访问权限
- 安全隔离: 低特权级程序无法直接访问高特权级的内存段
- 系统调用: 提供了安全的特权级切换机制
打开A20地址线
打开A20地址线其实就是,打开第21条地址线,不在回绕,让访存地址从 1MB 到 4GB(20位到32位)
使用固定的汇编代码即可
打开PE (Protection Enable)
PE位是CR0控制寄存器的第0位,也叫保护使能位。这是真正进入保护模式的开关。
CR0寄存器简介
CR0是32位的控制寄存器,包含了系统控制标志:
- PE位(第0位): Protection Enable,保护模式使能位
- 当PE=0: CPU工作在实模式
- 当PE=1: CPU工作在保护模式
进入保护模式的完整步骤
要成功进入保护模式,必须按以下顺序执行:
- 准备GDT: 在内存中构建全局描述符表
- 加载GDTR: 使用
lgdt指令加载GDT的地址和界限 - 打开A20地址线: 启用21位地址线,突破1MB限制
- 设置PE位: 将CR0寄存器的PE位设置为1
- 跳转刷新: 使用远跳转指令刷新指令流水线
关键汇编代码示例
1 | ; 1. 加载GDT |
注意事项
- 顺序很重要: 必须先准备好GDT再设置PE位
- 远跳转必需: 用于刷新CPU的指令预取队列
- 段寄存器重设: 进入保护模式后必须重新加载所有段寄存器
- 不可逆转: 一旦进入保护模式,返回实模式需要复杂的步骤
通过以上步骤,CPU就从16位实模式成功切换到32位保护模式,可以享受4GB地址空间和内存保护机制带来的好处。
代码实践
boot.inc
参考一些网上的资料,但是他们的 boot.inc 有部分错误,我的是完全正确的, 注意这个显存段起始位置0xb8000 的高8位也就是 16~23位是0x0b 不是 00
1 | ;---------------loader and kernel--------------- |
mbr
下面是熟悉的mbr 代码主要功能就是加载loader从硬盘到内存,注意我们的loader 变大,所以要加载4个扇区
1 | ;主引导程序 |
loader
最后是loader 部分,其实前面已经有去掉gdt的版本,其实就是按着步骤来进入保护模式,只是需要一些数据定义,一些地址线,cpu寄存器开关,打开这些模式,转换译码方法。
没什么是难的,只要慢慢来。
1 | %include "boot.inc" |
编译与写入磁盘
1 | nasm -I include/ -o mbr.bin mbr.s |
实验结果展示
可以看到我们在mbr和loader里打印的字符都出现了

从实模式到保护模式的内存布局
回顾我们之前讨论的1MB内存空间分布,现在我们来看看具体的内存布局图和模式切换的影响:
实模式下的1MB内存布局详解
| 地址范围 | 大小 | 用途 | 重要说明 |
|---|---|---|---|
| 0x00000-0x003FF | 1KB | 🔗 中断向量表 | 256个中断向量,每个4字节 |
| 0x00400-0x004FF | 256B | 📋 BIOS数据区 | 设备状态、配置信息 |
| 0x00500-0x07BFF | ~30KB | 💾 可用内存区域1 | Loader加载区(0x00900)在此 |
| 0x07C00-0x07DFF | 512B | 🎯 MBR区域 | BIOS加载主引导记录到此 |
| 0x07E00-0x9FBFF | ~608KB | 💾 可用内存区域2 | 可放置操作系统代码 |
| 0x9FC00-0x9FFFF | 1KB | 📋 扩展BIOS数据区 | 扩展设备信息 |
| 0xA0000-0xBFFFF | 128KB | 📺 显存区域 | 0xB8000开始为文本显存 |
| 0xC0000-0xEFFFF | 192KB | 🔧 ROM扩展区 | 显卡BIOS等 |
| 0xF0000-0xFFFFF | 64KB | 🔒 BIOS ROM | 系统固件代码 |
保护模式后的内存访问变化
关键变化:Loader进入保护模式后,使用32位寄存器选择同样的内存部分执行代码
注意我们代码中的代码段描述符起始地址是0,段界限是4gb,也就是所有的内存都能放代码,同时当前实模式下loader 所在的内存片段,
在保护模式的重新映射下不会有偏移。所以我们刷新流水线后修改段寄存器,就能根据偏移flag无痛的继续执行保护模式代码,这与我们设置 vstart 0x900也有关系。
内存保护的实现
进入保护模式后,虽然物理内存布局不变,但访问控制完全改变:
- 段限长检查: 防止越界访问
- 特权级检查: Ring0-Ring3的访问控制
- 读写权限: 代码段只读,数据段可读写
- 类型检查: 严格区分代码段和数据段
这样,同样的物理内存0x900处的Loader代码,在保护模式下通过32位寄存器和GDT描述符进行更安全、更高效的访问。





