系统启动的硬件基础: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 在设计时就规划好了各个区域的用途。我画了一张图,帮你理清这个布局:

x86 实模式 1MB 内存布局 中断向量表 (0x00000 - 0x003FF) BIOS 数据区 (0x00400 - 0x004FF) 用户可用内存 (0x00500 - 0x7BFF) 引导扇区加载到 0x7C00 引导扇区 (0x7C00 - 0x7DFF) 扩展 BIOS 数据区 (0x7E00 - 0x9FFFF) 视频内存 (0xA0000 - 0xBFFFF) 系统 ROM / BIOS (0xC0000 - 0xFFFFF) 0x00000 0x00400 0x7C00 0xA0000 0xFFFFF

这张图你得记牢了。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 的引导过程,第一步就是在实模式下运行,然后切换到保护模式。这个切换过程,我拆解成几个关键步骤:

  1. 准备 GDT(全局描述符表):定义代码段、数据段描述符
  2. 关闭中断:切换过程中不能响应中断
  3. 加载 GDTR:用 lgdt 指令加载 GDT 基址
  4. 设置 CR0 寄存器:将 PE 位置 1,开启保护模式
  5. 远跳转刷新流水线:用 jmp 指令清空预取队列
  6. 设置段寄存器:重新加载 CS、DS、ES 等
  7. 设置页表:开启分页机制(如果需要)

⚠️ 切换保护模式时最容易踩的坑

我记得有一次,我忘了在切换后立即做远跳转。结果 CPU 的预取队列里还存着实模式的指令,执行到一半就崩溃了。这个 bug 让我折腾了一整夜——所以请记住:切换保护模式后,必须立即执行一个远跳转,清空流水线。

内存布局的变化:保护模式下的新世界

进入保护模式后,内存布局完全变了。实模式下的 1MB 空间还在,但只是 4GB 空间的一小部分。MINIX 3 会重新规划内存:

MINIX 3 保护模式内存布局(简化) 内核空间 (Ring 0) 进程管理、内存管理、文件系统 中断处理、设备驱动 系统服务 (Ring 1) PM、FS、RS、DS 等服务进程 用户空间 (Ring 3) Shell、编译器、用户程序 0x00000000 0x00100000 0x01000000 0xFFFFFFFF

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 的引导代码,一步步看它怎么从实模式切换到保护模式,然后加载内核。到时候我会带着你一行一行读代码——嗯,那才是真正过瘾的地方。