实模式(Real Mode)
本文主要介绍实模式(Real Mode)的基本概念、主要特性,以及其在x86架构启动流程中的作用。通过理解实模式,可以帮助大家更好地理解现代操作系统为何要经历“实模式→保护模式→长模式”等阶段,以及16位、20位寻址等历史遗留问题的成因。
实模式(Real Mode)
什么是实模式?
实模式是 Intel 8086 处理器的原生工作模式,也是所有 x86 CPU 上电后的默认状态。它被称为“实模式”(Real Mode)是因为程序可以直接访问“真实”的物理内存地址,没有任何保护机制。
实模式的特点:
| 特性 | 说明 | 限制 |
|---|---|---|
| 位宽 | 16位寄存器和指令 | 单次只能处理16位数据 |
| 寻址能力 | 20位地址总线 | 最多访问1MB内存(2^20) |
| 内存保护 | 无 ❌ | 程序可以覆盖任意内存 |
| 多任务 | 不支持 ❌ | 只能运行一个程序 |
| 特权级 | 无 ❌ | 所有代码Ring 0权限 |
| 虚拟内存 | 不支持 ❌ | 只能使用物理地址 |
段地址机制
实模式使用“段:偏移”的方式来寻址:
1 | +------------------------------------------+ |
为什么要乘以16(左移4位)?
这是因为段寄存器只有16位,但地址总线有20位:
1 | 段寄存器(16位) :0001 0000 0000 0000 |
四大段寄存器
实模式下有四个主要的段寄存器:
| 寄存器 | 全称 | 默认用途 | 可以改变吗 |
|---|---|---|---|
| CS | Code Segment | 代码段,存储程序指令 | 只能通过JMP/CALL改变 |
| DS | Data Segment | 数据段,存储普通数据 | 可以自由修改 ✅ |
| SS | Stack Segment | 栈段,存储栈数据 | 可以修改但需小心 ⚠️ |
| ES | Extra Segment | 附加段,额外数据段 | 可以自由修改 ✅ |
寄存器组合规则:
默认的段:偏移组合:
1 | +--------------------------------------+ |
寄存器说明:
- IP (Instruction Pointer):指令指针寄存器,与
CS配合,指向下一条要执行的指令的地址。CPU 会自动更新该寄存器。- SP (Stack Pointer):栈指针寄存器,与
SS配合,指向当前栈顶。push和pop操作会自动更新SP。- BP (Base Pointer):基址指针寄存器,与
SS配合,通常用于在栈帧中定位函数参数和局部变量,作为访问栈的基准地址。- SI (Source Index):源变址寄存器,通常与
DS配合,在字符串操作中指向源数据地址。- DI (Destination Index):目的变址寄存器,通常与
ES配合,在字符串操作中指向目标数据地址。- BX (Base Register):基址寄存器,可以与
DS配合,是通用寄存器中唯一可以作为内存寻址基址的。
内存布局(实模式下的1MB空间)
| 地址范围 | 大小 | 用途 |
|---|---|---|
| 0x00000-0x003FF | 1KB | 中断向量表 (IVT) |
| 0x00400-0x004FF | 256B | BIOS数据区 |
| 0x00500-0x07BFF | ~30KB | 可用内存 |
| 0x07C00-0x07DFF | 512B | 引导扇区 (MBR) ⭐ |
| 0x07E00-0x7FFFF | ~480KB | 可用内存 |
| 0x80000-0x9FFFF | 128KB | 扩展BIOS数据区 |
| 0xA0000-0xBFFFF | 128KB | 显存 (VGA) |
| 0xC0000-0xC7FFF | 32KB | 显卡BIOS |
| 0xC8000-0xEFFFF | 160KB | 其他ROM/映射区 |
| 0xF0000-0xFFFFF | 64KB | 系统BIOS ROM |
关键地址说明:
- 0x7C00 (MBR加载地址)
- BIOS将引导扇区加载到这里
- 引导程序从这里开始执行
- 可用空间:512字节
- 0xB8000 (VGA文本显存)
- 25行 × 80列 = 2000个字符
- 每个字符占2字节(字符+属性)
- 实际访问:0xB800:0x0000
- 0x00000 (中断向量表)
- 256个中断向量 × 4字节 = 1024字节
- 格式: 偏移16位
地址计算实践
让我们通过几个实际例子来理解地址计算:
例1:访问MBR
1 | ; 方法1: 段地址=0, 偏移=0x7C00 |
例2:访问VGA显存写"A"
1 | mov ax, 0xB800 ; VGA文本模式段地址 |
例3:同一物理地址的不同表示
物理地址 0x12345 可以表示为:
| 段地址 | 偏移 | 计算 |
|---|---|---|
| 0x1234 | 0x0005 | 0x12340 + 0x0005 |
| 0x1230 | 0x0045 | 0x12300 + 0x0045 |
| 0x1000 | 0x2345 | 0x10000 + 0x2345 |
| 0x0000 | 无法表示 | 偏移超过16位 |
这种"一个物理地址,多种表示"的特性叫做地址重叠。
实模式的限制
1. 1MB内存限制
20位地址线只能访问 2^20 = 1,048,576 字节 = 1MB
最大地址: 段地址: 0xFFFF 偏移: 0xFFFF 物理地址 = 0xFFFF × 16 + 0xFFFF = 0x10FFEF ≈ 1.06MB
但是!实际只有20根地址线,所以: 超过 0xFFFFF 的地址会"回卷"到 0x00000 这就是著名的"A20地址线"问题
2. 无内存保护
任何程序都可以:
- 修改其他程序的代码和数据
- 覆盖BIOS数据区
- 改写中断向量表
- 直接访问硬件端口
后果:一个程序的错误会导致整个系统崩溃
3. 单任务环境
由于没有内存保护和特权级:
- 不能同时运行多个程序
- 不能实现真正的多任务
- 不能隔离用户程序和内核
从实模式到保护模式
为了克服这些限制,Intel 在 80286 引入了保护模式 (Protected Mode):
| 对比项 | 实模式 | 保护模式 |
|---|---|---|
| 位宽 | 16位 | 32位(64位扩展) |
| 寻址 | 1MB | 4GB (32位) / 无限 (64位) |
| 内存保护 | ❌ | ✅ 段级+页级 |
| 多任务 | ❌ | ✅ 硬件支持 |
| 特权级 | 无 | ✅ Ring 0-3 |
| 虚拟内存 | ❌ | ✅ 分页机制 |
切换到保护模式是不可逆的(在不重启的情况下)!
这就是为什么引导程序需要非常小心地准备好一切,再进行切换。
💡 深入理解:特权级 (Privilege Levels / Protection Rings)
保护模式最重要的特性之一就是特权级机制,它是操作系统安全性的基础。
什么是特权级?
x86 CPU定义了4个特权级别,通常称为 Ring 0-3:
1 | +-----------------------------------------------------------------+ <--- 最低特权 |
实际使用:
大多数操作系统只使用 Ring 0 和 Ring 3:
| Ring | 名称 | 用途 | 权限 |
|---|---|---|---|
| Ring 0 | 内核模式 (Kernel Mode) | 操作系统内核、关键驱动 | 完全访问硬件、修改所有内存、执行特权指令 |
| Ring 1 | 设备驱动 (很少用) | 设备驱动程序 | 有限的硬件访问 |
| Ring 2 | 设备驱动 (很少用) | 系统服务 | 更多的权限 |
| Ring 3 | 用户模式 (User Mode) | 应用程序 | 受限访问、不能执行特权指令、不能直接访问硬件 |
特权级如何体现?
CPU通过以下位置记录当前特权级:
CS寄存器的低2位 (CPL - Current Privilege Level)
这2位就是当前特权级(CPL)1
2
3
4
5
615 3 2 1 0
+-----------------+--+---+
| 选择子索引 |TI|RPL| <- CS 寄存器
+-----------------+--+---+
|
+-- 这2位就是当前特权级 (CPL)段描述符中的DPL (Descriptor Privilege Level) 每个段描述符都有DPL字段,表示访问该段需要的最低特权级
选择子中的RPL (Requested Privilege Level) 程序请求访问某个段时指定的特权级
特权级检查规则:
访问数据段: CPL ≤ DPL (数字越小特权越高) RPL ≤ DPL
调用代码段:更复杂,涉及到Call Gate(后续章节)
示例: - Ring 0 代码 (CPL=0) 可以访问任何 DPL 段 - Ring 3 代码 (CPL=3) 只能访问 DPL=3 的段 - 如果 Ring 3 代码试图访问 Ring 0 的段,CPU会触发保护异常 (#GP)
特权指令:
只有在 Ring 0 才能执行的指令(特权指令):
| 指令 | 用途 | 为什么要保护 |
|---|---|---|
cli / sti |
关闭/开启中断 | 防止用户程序禁止中断导致系统无响应 |
lgdt / lidt |
加载GDT/IDT | 防止用户程序篡改系统表 |
lldt |
加载LDT | 防止破坏内存保护 |
ltr |
加载任务寄存器 | 防止破坏任务切换机制 |
mov cr0, eax |
修改控制寄存器 | 防止用户程序切换CPU模式 |
in / out |
端口I/O | 防止用户程序直接控制硬件 |
hlt |
停止CPU | 防止用户程序挂起系统 |
如果在 Ring 3 执行这些指令,CPU会触发 #GP (General Protection Fault) 异常。
实例:用户程序试图关闭中断
1 | ; 用户程序 (Ring 3) |
系统调用:如何从 Ring 3 请求 Ring 0 服务?
用户程序不能直接执行特权操作,但可以通过系统调用请求内核帮忙:
1 | +--------------------------------+ |
Linux 系统调用示例:
1 | ; Linux x86-32 系统调用示例 |
特权级转换时的栈切换:
从 Ring 3 切换到 Ring 0 时,CPU会自动切换栈:
1 | +---------------------------+ +---------------------------------+ |
每个任务都有专门的内核栈(TSS定义)。
为什么需要特权级?
安全性:防止用户程序破坏系统
1
2
3
4
5
6
7if (用户程序能直接访问硬件) {
恶意程序可以:
- 读取其他程序的内存(窃取密码)
- 格式化硬盘
- 禁用安全机制
- 接管整个系统
}稳定性:隔离错误
graph LR A["用户程序崩溃"] --> B{"只影响自己"} --> C["操作系统继续运行"] D["没有特权级"] --> E{"一个程序错误"} --> F["整个系统崩溃"]多任务:资源管理
操作系统 (Ring 0):- 决定哪个程序运行
- 分配内存和CPU时间
- 防止程序互相干扰