4. 从实模式到保护模式:A20地址线开启、GDT表结构、CR0控制寄存器切换

x86 处理器在上电复位后默认进入实模式(Real Mode),该模式下 CPU 仅能访问 1MB 地址空间(0x00000 – 0xFFFFF),且所有段寄存器左移 4 位 + 偏移量的寻址方式直接暴露给软件。现代操作系统内核必须切换到保护模式(Protected Mode)才能使用 4GB 平坦地址空间、硬件分页、特权级隔离等核心能力。本章围绕实模式→保护模式切换的三个关键工程步骤展开:A20 地址线开启、GDT 表结构构建、CR0 控制寄存器切换。

工程要点:保护模式切换是内核解压缩与地址重定位的前置条件。只有进入保护模式,解压缩代码才能访问 1MB 以上的物理内存,并将压缩内核重定位到高地址区域。

4.1 A20 地址线开启:突破 1MB 地址回绕

在 8086 实模式下,段:偏移最大地址为 0xFFFF:0xFFFF = 0x10FFEF(约 1.08MB),但 8086 只有 20 根地址线(A0–A19),因此超过 0xFFFFF 的地址会自动回绕到 0x00000。80286 及后续 CPU 拥有 24/32 根地址线,为了兼容旧软件,默认在启动时仍启用地址回绕(即 A20 地址线被强制拉低)。

A20 Gate 开启步骤:

  1. 键盘控制器(8042)方法:向 0x64 端口发送 0xD1 命令,然后向 0x60 端口写入 0xDF(开启 A20)或 0xDD(关闭 A20)。
  2. 快速 A20 方法(Fast A20):读取 I/O 端口 0x92,设置第 1 位(bit 1)为 1,再写回 0x92。
  3. BIOS 中断方法:调用 INT 15h,AX=0x2401 开启 A20,AX=0x2400 关闭 A20。
工程建议:现代引导加载程序(如 GRUB)通常在进入保护模式前已开启 A20。若自行编写引导代码,推荐优先使用快速 A20 方法(端口 0x92),代码量小且兼容大多数 x86 平台。
A20 地址线开启流程 实模式地址回绕 0x100000 → 0x00000 检测 A20 状态 INT 15h / 端口 0x92 选择开启方法 8042 / Fast A20 / BIOS 写入控制端口 0x92: 置位 bit1 验证 A20 开启 读 0x100000 确认 A20 开启后,CPU 可访问 0x100000–0x10FFEF 的扩展内存区域,为保护模式平坦寻址铺平道路

4.2 GDT 表结构:保护模式的段描述符

保护模式不再使用实模式的段寄存器左移方式,而是通过全局描述符表(GDT)中的段描述符来定义段的基地址、限长、访问权限和属性。GDT 是保护模式切换的核心数据结构,CPU 通过 GDTR 寄存器(全局描述符表寄存器)定位 GDT 在内存中的位置。

4.2.1 段描述符格式

每个段描述符占用 8 字节,其结构如下:

位域 位范围 说明
段限长(Limit) 0–15, 48–51 段的长度,单位由 G 位决定(字节或 4KB)
基地址(Base) 16–39, 56–63 段的 32 位起始物理地址
类型(Type) 40–43 代码段/数据段、可读/可写、可执行等属性
S 位 44 0=系统段,1=代码/数据段
DPL 45–46 描述符特权级(0–3),0 为最高
P 位 47 段存在位,1=在内存中
D/B 位 54 默认操作数大小:0=16 位,1=32 位
G 位 55 粒度:0=字节,1=4KB

4.2.2 典型 GDT 布局(内核启动阶段)

在实模式→保护模式切换中,通常至少需要三个描述符:

  • 空描述符(索引 0):必须存在,CPU 硬件要求 GDT 第一个描述符为 0。
  • 代码段描述符(索引 1):基地址 0x00000000,限长 0xFFFFF,G=1(4KB 粒度 → 4GB),D=1(32 位),DPL=0,可执行/可读。
  • 数据段描述符(索引 2):基地址 0x00000000,限长 0xFFFFF,G=1,D=1,DPL=0,可读写。
常见错误:GDT 必须在进入保护模式前加载到内存中,且 GDTR 中的基地址必须是线性地址(实模式下即为物理地址)。若 GDT 位于 1MB 以上区域,必须先开启 A20 并确保该内存可访问。
GDT 表结构及段描述符布局 GDT(全局描述符表) 索引 0:空描述符 8 字节全 0(必须) 索引 1:代码段 Base=0, Limit=4GB 索引 2:数据段 Base=0, Limit=4GB 段描述符 8 字节内部结构 Base[23:16] Flags Limit[19:16] Access Byte Base[15:0] Limit[15:0] Base[31:24] Byte 5 Byte 4 Byte 2-3 Byte 0-1 Byte 6-7 GDTR 寄存器保存 GDT 基地址(32 位)和限长(16 位),使用 LGDT 指令加载

4.3 CR0 控制寄存器切换:进入保护模式

CR0 是 x86 CPU 的核心控制寄存器,其第 0 位(PE 位,Protection Enable)控制 CPU 的运行模式。当 PE=0 时 CPU 处于实模式,PE=1 时进入保护模式。

4.3.1 切换步骤

  1. 关闭中断:执行 CLI 指令,防止在切换过程中响应中断导致状态不一致。
  2. 加载 GDT:使用 LGDT 指令将 GDT 的基地址和限长加载到 GDTR 寄存器。
  3. 设置 CR0.PE 位:读取 CR0,置位第 0 位,再写回 CR0。
  4. 远跳转刷新流水线:执行一条远跳转(JMP FAR 或 CALL FAR)指令,加载 CS 段选择子并刷新 CPU 指令预取队列。
  5. 加载数据段选择子:将数据段选择子加载到 DS、ES、FS、GS、SS 寄存器。
  6. 设置栈指针:重新初始化 ESP 为保护模式下的栈地址。

4.3.2 切换代码示例(NASM 语法)

; 关闭中断
cli

; 加载 GDT
lgdt [gdt_ptr]

; 设置 CR0 的 PE 位
mov eax, cr0
or eax, 1          ; 置位 bit 0
mov cr0, eax

; 远跳转刷新 CS 选择子
jmp 0x08:protect_mode_entry

[BITS 32]
protect_mode_entry:
    ; 加载数据段选择子(0x10 = 索引 2,RPL=0)
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    ; 设置保护模式栈
    mov esp, 0x7E00

    ; 现在 CPU 已完全运行在保护模式下
    ; 可以开始内核解压缩与重定位工作
关键细节:远跳转指令 jmp 0x08:protect_mode_entry 中的 0x08 是段选择子,其二进制为 0000 0000 0000 1000,即索引=1(代码段)、TI=0(GDT)、RPL=0(内核特权级)。跳转后 CS 被加载为该描述符,CPU 开始以 32 位模式执行。
CR0 控制寄存器切换流程 实模式 CR0.PE = 0 关闭中断 CLI 指令 加载 GDT LGDT 指令 置位 CR0.PE OR CR0, 1 远跳转刷新 JMP FAR 0x08:entry 加载段选择子 DS/ES/FS/GS/SS 保护模式 CR0.PE = 1 远跳转是切换的关键:它同时加载 CS 选择子并刷新 CPU 预取队列中的实模式指令

4.4 切换后的状态验证

进入保护模式后,建议立即执行以下验证操作,确保切换成功:

  • 读取 CR0:检查 PE 位是否为 1。
  • 读取 CS 段寄存器:远跳转后 CS 的选择子应为 0x08(索引 1,RPL=0)。
  • 测试 4GB 寻址:向 0x100000(1MB 以上)写入一个测试值,再读取确认,验证 A20 已开启且保护模式寻址正常。
  • 检查 GDTR:使用 SGDT 指令读取 GDTR 内容,确认基地址和限长与加载时一致。
工程总结:A20 地址线开启、GDT 表结构构建、CR0 切换是实模式到保护模式的三步曲。这三步完成后,CPU 运行在 32 位平坦模式,内核解压缩代码可以自由访问 0–4GB 物理地址空间,为后续的地址重定位和解压缩执行奠定基础。任何一步出错(如 GDT 描述符配置错误、A20 未开启、远跳转遗漏)都会导致系统崩溃或不可预期的行为,必须通过严谨的代码和验证流程确保正确性。