当前位置:首页>Linux>Linux 2.6.39:从 16 位实模式到 32 位保护模式

Linux 2.6.39:从 16 位实模式到 32 位保护模式

  • 2026-09-13 09:44:36
Linux 2.6.39:从 16 位实模式到 32 位保护模式

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 的规则运行。

最新文章

随机文章