Linux 2.6.39:从 16 位实模式到 32 位保护模式
Linux 2.6.39:从 16 位实模式到 32 位保护模式1. 本文要解决的问题2. 为什么 Linux 不能一直运行在实模式3. 为什么叫“保护模式”4. 保护模式中的寻址方式4.1 实模式:段值直接参与地址计算4.2 保护模式:段寄存器变为选择子5. GDT 是什么6. 16 位实模式中有 GDT 吗7. Linux 2.6.39 如何完成切换7.1 关闭中断并屏蔽 PIC7.2 打开 A207.3 创建并装载 GDT7.4 装载空 IDT7.5 设置 CR0.PE7.6 远跳转:让 CPU 真正加载 32 位代码段8. in_pm32:保护模式中的第一个稳定入口9. 本阶段结束时的系统状态总结
1. 本文要解决的问题
x86 CPU 上电后默认运行在 16 位实模式。Linux 2.6.39 的 x86 启动代码必须先在实模式中完成 BIOS 交互和启动信息收集,再切换到 32 位保护模式,进入 32 位内核启动代码。
核心路径如下:
0x90200:16 位 setup 入口
↓
收集启动参数、硬件信息
↓
准备 A20、中断状态、GDT、IDT
↓
CR0.PE = 1
↓
远跳转,加载 32 位代码段
↓
in_pm32
↓
0x100000:32 位内核入口
本文只讨论:
16 位实模式 → 32 位保护模式
不讨论后续分页、虚拟内存、64 位长模式和 start_kernel()。
2. 为什么 Linux 不能一直运行在实模式
实模式是 8086 时代的运行方式,寻址规则是:
物理地址 = 段寄存器 × 16 + 偏移
例如:
CS = 0x9000
IP = 0x0200
物理地址 = 0x9000 × 16 + 0x0200
= 0x90200
Linux 的 setup 代码通常正是从 0x90200 开始运行。
实模式适合 BIOS 和 Bootloader 做早期工作:
调用 BIOS 中断
读取设备信息
收集内存布局
加载内核镜像
但不适合运行完整 Linux 内核,原因是:
默认使用 16 位寄存器和指令语义
地址模型受早期 x86 历史限制
没有特权级保护
没有分页机制
无法建立现代操作系统所需的进程地址空间
Linux 进入保护模式的直接目的,是获得:
32 位执行环境
平坦的 32 位线性地址空间
CPU 提供的权限检查机制
后续开启分页的前提
3. 为什么叫“保护模式”
保护模式的“保护”指 CPU 开始支持并检查内存访问规则和权限规则。
保护模式中的段描述符可以定义:
段基址:段从哪个线性地址开始
段界限:段最大可访问范围
段类型:代码段、数据段、TSS 等
访问权限:可读、可写、可执行
特权级:Ring 0 到 Ring 3
Linux 主要使用两个特权级:
Ring 0:内核态
Ring 3:用户态
未来用户程序运行在 Ring 3,它不能直接执行关中断、修改页表或访问内核特权资源;CPU 会阻止这种操作。
但要区分:
保护模式 ≠ 分页
保护模式 ≠ 虚拟内存
Linux 刚切入保护模式时:
CR0.PE = 1 # 保护模式开启
CR0.PG = 0 # 分页尚未开启
此时还没有用户进程和完整内存隔离。保护模式首先提供的是 32 位执行和寻址基础。
4. 保护模式中的寻址方式
4.1 实模式:段值直接参与地址计算
物理地址 = 段值 × 16 + 偏移
此时 CS、DS、SS 中保存的是实际段值。
4.2 保护模式:段寄存器变为选择子
进入保护模式后,CS、DS、SS 不再直接代表地址段值,而是 选择子。
选择子指向 GDT 中的一个表项:
逻辑地址 = 段选择子 : 偏移
↓
根据选择子查询 GDT
↓
获取段基址、段界限、类型、权限
↓
线性地址 = 段基址 + 偏移
如果后续启用分页,完整寻址链会变成:
逻辑地址
↓ 分段机制
线性地址
↓ 页表
物理地址
但本阶段尚未开启分页,因此:
物理地址 = 线性地址
5. GDT 是什么
GDT 是 Global Descriptor Table,即全局描述符表。
它是一张位于内存中的表。每一个表项都描述一个段:
代码段在哪里
数据段在哪里
栈允许访问多大范围
该段可读、可写还是可执行
允许哪个特权级访问
Linux 2.6.39 启动阶段建立的最小 GDT 主要包括:
__BOOT_CS:32 位代码段
__BOOT_DS:32 位数据段
__BOOT_TSS:临时 TSS 描述符
其中最重要的是:
__BOOT_CS:
base = 0
limit = 4 GB
可执行、可读
__BOOT_DS:
base = 0
limit = 4 GB
可读、可写
这就是 Linux 的平坦内存模型:
代码地址 = 0 + EIP
数据地址 = 0 + 偏移
栈地址 = 0 + ESP
例如:
EIP = 0x00100000
CS 对应描述符的 base = 0
线性地址 = 0x00100000
6. 16 位实模式中有 GDT 吗
从实模式的寻址规则来说,没有。
实模式中 CPU 不查询 GDT,CS/DS/SS 仍按:
段值 × 16 + 偏移
计算地址。
但 x86 CPU 本身有 GDTR 寄存器,Linux 可以在实模式中提前:
在内存中创建 GDT
↓
执行 lgdt
↓
把 GDT 的地址和长度装入 GDTR
此时 CPU 仍在实模式,GDT 尚未真正参与寻址。
只有在:
CR0.PE = 1
之后,CPU 才开始把 CS/DS/SS 当成选择子,并通过 GDT 解释它们。
所以准确说法是:
Linux 在实模式中准备并装载 GDT;进入保护模式后,CPU 才正式使用 GDT。
7. Linux 2.6.39 如何完成切换
核心调用链位于:
arch/x86/boot/main.c
main()
↓
arch/x86/boot/pm.c
go_to_protected_mode()
↓
arch/x86/boot/pmjump.S
protected_mode_jump()
main() 完成启动参数和硬件信息准备后,最终调用:
go_to_protected_mode();
7.1 关闭中断并屏蔽 PIC
切换时,实模式中断处理环境即将失效,而保护模式的正式中断系统尚未建立。
因此 Linux 先:
关闭可屏蔽中断
禁止 NMI
屏蔽 8259 PIC
目的:
防止模式切换过程中突然发生中断,导致 CPU 跳转到错误的处理入口。
7.2 打开 A20
历史 PC 为兼容 8086,可能使超过 1 MB 的地址发生回绕。
0x100000
↓ A20 未开启时可能回绕
0x000000
而 Linux 的 32 位入口默认地址是:
code32_start = 0x100000
定义在:
arch/x86/boot/header.S
因此 Linux 必须先确认 A20 已开启,否则不能可靠跳入 1 MB 以上的内存。
7.3 创建并装载 GDT
Linux 在 setup_gdt() 中建立临时 boot GDT,并使用:
lgdt
装入 GDTR。
此时仍处于实模式;GDT 只是为下一步保护模式做准备。
7.4 装载空 IDT
Linux 同时加载空 IDT:
IDT.base = 0
IDT.limit = 0
它的目的不是处理中断,而是确保切换阶段不会误用旧的实模式中断向量表。
7.5 设置 CR0.PE
真正切换的核心代码位于:
arch/x86/boot/pmjump.S
逻辑如下:
movl %cr0, %edx
orb $X86_CR0_PE, %dl
movl %edx, %cr0
其中:
CR0.PE = 0:16 位实模式
CR0.PE = 1:保护模式
写入 CR0.PE 后,CPU 已切换到保护模式规则,但还必须立即执行远跳转。
7.6 远跳转:让 CPU 真正加载 32 位代码段
Linux 紧接着执行:
ljmpl $in_pm32, $__BOOT_CS
远跳转有两个关键作用:
1. 将 CS 设置为 __BOOT_CS 选择子
→ CPU 从 GDT 读取 32 位代码段描述符
2. 刷新指令预取队列
→ 后续指令按 32 位保护模式规则执行
因此,模式切换的关键组合是:
GDT 已装载
↓
CR0.PE = 1
↓
far jump 加载新的 CS
↓
进入 32 位保护模式代码
8. in_pm32:保护模式中的第一个稳定入口
远跳转到达 in_pm32 后,Linux 继续加载平坦数据段:
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
这些段寄存器都指向 __BOOT_DS。
此后:
CS:指向平坦 32 位代码段
DS/ES/FS/GS/SS:指向平坦 32 位数据段
最后,Linux 跳转至 boot protocol 指定的 32 位入口:
jmpl *%eax
默认目标为:
0x00100000
至此,16 位 setup 阶段结束,32 位内核启动代码开始执行。
9. 本阶段结束时的系统状态
CPU 模式:32 位保护模式
CR0.PE:1
CR0.PG:0
分页:未开启
虚拟内存:未开启
寻址:平坦 32 位线性地址
GDT:已启用
中断:仍严格受控
下一步:建立页表并开启分页
总结
Linux 2.6.39 从 16 位实模式进入 32 位保护模式,是为了摆脱早期 x86 的 16 位和低地址限制,进入能够运行 32 位内核启动代码的 CPU 环境。
其核心闭环是:
实模式 setup
↓
关闭中断、打开 A20
↓
建立并装载 GDT
↓
设置 CR0.PE
↓
远跳转加载 32 位 CS
↓
加载平坦数据段
↓
跳转到 0x100000
↓
进入 32 位保护模式内核代码
最关键的理解是:
GDT 是保护模式下的段、权限和访问范围规则表。Linux 在实模式中提前装载它,在设置 CR0.PE 并进行远跳转后,CPU 才正式按 GDT 的规则运行。