系统启动的硬件基础:x86 实模式、保护模式、内存布局与中断向量表
各位同学,咱们今天聊点硬核的。要理解 MINIX 3 怎么启动,你得先搞清楚 x86 处理器在开机那一刻是什么状态。说白了,CPU 刚上电时就像个刚睡醒的人——脑子还不太清醒,只能干点简单活儿。这个状态,就是实模式。
实模式:CPU 的“婴儿期”
实模式是 x86 处理器最原始的工作模式。我当年刚接触底层开发时,总觉得这模式太古老了,没什么用。后来调一个 BIOS 中断时才发现——嗯,没有实模式,你连键盘都读不了。
实模式有几个关键特征:
- 20 位地址总线:最大寻址 1MB 空间(0x00000 - 0xFFFFF)
- 16 位通用寄存器:AX、BX、CX、DX 这些,每个 16 位
- 段式内存管理:物理地址 = 段基址 × 16 + 偏移量
- 无保护机制:任何程序都能访问任何内存地址
你想想看,一个程序可以随便改写系统数据,甚至覆盖中断向量表——这在今天看来简直是灾难。但在 8086 那个年代,这就是全部了。
实模式地址计算示例
段寄存器 CS = 0x7C0,偏移 IP = 0x0000
物理地址 = 0x7C0 × 16 + 0x0000 = 0x7C00
这就是 BIOS 加载引导扇区的目标地址——0x7C00。
内存布局:1MB 空间的“分蛋糕”
实模式下的 1MB 内存,可不是随便用的。Intel 在设计时就规划好了各个区域的用途。我画了一张图,帮你理清这个布局:
这张图你得记牢了。MINIX 3 的引导代码就放在 0x7C00 这个位置。为什么是这里?因为 BIOS 规定死了——它从启动设备读 512 字节,就放到 0x7C00。
中断向量表:系统服务的“电话本”
中断向量表(IVT)位于内存最开头,从 0x00000 到 0x003FF,一共 1024 字节。每个中断向量占 4 字节(段地址 2 字节 + 偏移量 2 字节),所以总共支持 256 个中断。
| 中断号 | 地址范围 | 用途 | 个人备注 |
|---|---|---|---|
| 0x00 | 0x00000 - 0x00003 | 除零错误 | 写驱动时经常触发 |
| 0x01 | 0x00004 - 0x00007 | 单步调试 | 调试器依赖这个 |
| 0x10 | 0x00040 - 0x00043 | 视频服务 | 显示字符全靠它 |
| 0x13 | 0x0004C - 0x0004F | 磁盘服务 | 读硬盘扇区用这个 |
| 0x19 | 0x00064 - 0x00067 | 加载引导程序 | BIOS 用它启动系统 |
| 0x21 | 0x00084 - 0x00087 | 键盘服务 | 我调这个调了三天 |
避坑指南
我曾经在写引导程序时,不小心把数据写到了 0x00000 附近。结果呢?中断向量表被覆盖了,一调用 int 0x10 就死机。排查了整整一个下午才发现——嗯,从那以后我写引导代码时,第一件事就是保护中断向量表。
保护模式:从“婴儿”到“成年人”
实模式虽然简单,但缺点太明显了:只能访问 1MB 内存,没有权限保护,多任务更是想都别想。80286 开始引入保护模式,到了 80386 就成熟了。
保护模式的核心变化:
- 32 位地址总线:寻址空间扩大到 4GB
- 分段与分页:段描述符 + 页表,两层地址转换
- 4 个特权级:Ring 0(内核)到 Ring 3(用户程序)
- 中断描述符表(IDT):取代了实模式的中断向量表
你想想看,实模式下的程序可以随便改系统数据,这在保护模式下根本不可能——Ring 3 的程序想访问 Ring 0 的数据?门都没有,CPU 直接给你抛个异常。
从实模式到保护模式的切换流程
MINIX 3 的引导过程,第一步就是在实模式下运行,然后切换到保护模式。这个切换过程,我拆解成几个关键步骤:
- 准备 GDT(全局描述符表):定义代码段、数据段描述符
- 关闭中断:切换过程中不能响应中断
- 加载 GDTR:用 lgdt 指令加载 GDT 基址
- 设置 CR0 寄存器:将 PE 位置 1,开启保护模式
- 远跳转刷新流水线:用 jmp 指令清空预取队列
- 设置段寄存器:重新加载 CS、DS、ES 等
- 设置页表:开启分页机制(如果需要)
⚠️ 切换保护模式时最容易踩的坑
我记得有一次,我忘了在切换后立即做远跳转。结果 CPU 的预取队列里还存着实模式的指令,执行到一半就崩溃了。这个 bug 让我折腾了一整夜——所以请记住:切换保护模式后,必须立即执行一个远跳转,清空流水线。
内存布局的变化:保护模式下的新世界
进入保护模式后,内存布局完全变了。实模式下的 1MB 空间还在,但只是 4GB 空间的一小部分。MINIX 3 会重新规划内存:
MINIX 3 的微内核设计,让内核空间非常精简。大部分操作系统功能(文件系统、进程管理)都跑在 Ring 1 的服务层。用户程序在 Ring 3,想干点什么事都得通过系统调用——说白了,就是求着内核帮忙。
中断描述符表(IDT):保护模式下的“新电话本”
保护模式不再用实模式的中断向量表了,改用中断描述符表(IDT)。IDT 可以放在内存任何位置,通过 IDTR 寄存器指向它。
IDT 的每个表项叫门描述符,有 8 字节:
| 位域 | 长度 | 说明 |
|---|---|---|
| Offset 0-15 | 16 位 | 中断处理函数偏移量低 16 位 |
| Selector | 16 位 | 代码段选择子 |
| Flags | 16 位 | 类型、DPL、存在位等 |
| Offset 16-31 | 16 位 | 中断处理函数偏移量高 16 位 |
个人经验
我习惯在 IDT 里把前 32 个中断(0x00-0x1F)都注册上默认处理函数。这些是 CPU 异常,比如除零、页错误、保护错误。万一程序出问题了,至少能打印个错误信息,而不是直接死机。调试的时候,这个习惯救了我好几次。
总结:从实模式到保护模式,MINIX 3 的必经之路
好了,咱们把今天的内容串一下:
- CPU 上电后处于实模式,只能访问 1MB 内存,用中断向量表
- BIOS 把引导扇区加载到 0x7C00,然后跳过去执行
- 引导代码先做准备工作,然后切换到保护模式
- 保护模式下用 GDT 和 IDT,支持 4GB 寻址和特权级保护
- MINIX 3 的微内核结构,把内核和服务层分开,各占不同的内存区域
下一章,咱们就跟着 MINIX 3 的引导代码,一步步看它怎么从实模式切换到保护模式,然后加载内核。到时候我会带着你一行一行读代码——嗯,那才是真正过瘾的地方。