从零写 OS 内核-第四篇:进入保护模式(段式内存管理详解)

"保护模式 ≠ 分页!
它的第一道防线,是被现代 OS ‘悄悄绕过'的段机制。"

在前面几篇中,我们借助 GRUB 轻松启动了内核,输出了 "Hello World",还理解了 Multiboot 协议。
但你是否想过:为什么 GRUB 要帮我们进入保护模式?它到底在保护什么?

今天,我们就来亲手完成这一历史性转变——从实模式(Real Mode)切换到保护模式(Protected Mode),并深入理解其核心:段式内存管理(Segmentation),包括 GDT、LDT 与段选择子的工作原理。

🔥 即便现代操作系统几乎"废弃"了段机制,理解它,仍是掌握 x86 架构的必经之路


一、什么是实模式(Real Mode)?

实模式,是 x86 CPU 上电后的默认运行状态,源自 1978 年的 Intel 8086 处理器。

✅ 特点:

  • 20 位地址线 → 最大寻址 1MB 内存(0x00000 ~ 0xFFFFF)
  • 段:偏移寻址:物理地址 = 段寄存器 × 16 + 偏移
    例如:CS:IP = 0x1000:0x0020 → 物理地址 = 0x10000 + 0x20 = 0x10020
  • 无内存保护:任何程序可读写任意内存(包括 BIOS 和显存)
  • 无特权级:所有代码运行在"上帝模式"

💡 实模式就像一辆没有方向盘、没有刹车的老式拖拉机——简单,但极其危险。

❓ 为什么现代 CPU 还要从实模式启动?

为了兼容!
PC 兼容机必须能运行 1980 年代的老 DOS 程序,所以 CPU 上电后"假装自己是 8086"。


二、什么是保护模式(Protected Mode)?

保护模式Intel 80286(1982)引入,80386 起成为现代操作系统的基石。

✅ 核心优势:

特性 说明
32 位寻址 可访问 4GB 内存(后续通过 PAE/64 位扩展)
内存保护 防止程序越界访问(段界限、权限检查)
特权级(Ring 0~3) 内核(Ring 0) vs 用户程序(Ring 3)
虚拟内存基础 为分页(Paging)提供支持

🌟 保护模式下的内存访问,不再是"直接物理地址",而是通过 段描述符表 进行翻译和校验。


三、关键机制:段描述符与段寄存器

在保护模式中,段寄存器(CS、DS、SS 等)不再直接存地址,而是存一个 段选择子(Segment Selector)

🔍 段选择子结构(16 位):

| 15        3 | 2 | 1 | 0 |
|   Index     | TI| RPL|
  • Index:在 GDT/LDT 表中的索引(×8 得到字节偏移)
  • TI(Table Indicator):0 = GDT,1 = LDT
  • RPL(Requested Privilege Level):请求特权级(0~3)

📦 段描述符(64 位)

由 Index 指向的描述符,包含:

  • 段基地址(32 位)
  • 段界限(Limit)
  • 类型(代码/数据/只读/可执行等)
  • DPL(Descriptor Privilege Level)
  • G 位(粒度:0=字节,1=4KB)

✅ CPU 通过 GDT(全局描述符表)LDT(局部描述符表) 查找描述符。


四、GDT vs LDT:谁来管理"段"?

对比项 GDT(Global Descriptor Table) LDT(Local Descriptor Table)
作用范围 全局,所有任务共享 局部,每个任务可有自己的 LDT
数量 系统只有一个 可有多个(每个进程一个)
使用频率 极高(内核、用户代码/数据段) 极低(现代 OS 几乎不用)
切换方式 LGDT 指令加载 通过 GDT 中的一个特殊描述符指向 LDT

📌 现代操作系统(如 Linux)几乎完全弃用 LDT,转而依赖分页机制实现内存隔离。
但在进入保护模式初期,GDT 是必须的


五、实战:如何从实模式进入保护模式?

即使你用 GRUB 启动(它已切换),手动实现一次,才能真正理解
以下是裸机环境下切换的完整步骤:

步骤 1:关闭中断

cli    ; 禁用可屏蔽中断

步骤 2:加载 GDT

先定义 GDT(通常在汇编中):

gdt_start:
    dq 0x0                    ; 空描述符(必须)

gdt_code:
    dw 0xFFFF                 ; 段界限 0-15
    dw 0x0                    ; 基地址 0-15
    db 0x0                    ; 基地址 16-23
    db 10011010b              ; 类型:代码段,可读,DPL=0
    db 11001111b              ; 粒度:4KB,32位
    db 0x0                    ; 基地址 24-31

gdt_data:
    dw 0xFFFF
    dw 0x0
    db 0x0
    db 10010010b              ; 数据段,可写,DPL=0
    db 11001111b
    db 0x0

gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1   ; GDT 大小
    dd gdt_start                 ; GDT 起始地址

加载 GDT:

lgdt [gdt_descriptor]

步骤 3:设置 CR0 的 PE 位(Protection Enable)

mov eax, cr0
or eax, 0x1      ; 设置 bit 0 (PE)
mov cr0, eax

步骤 4:远跳转,刷新 CS 段寄存器

jmp 0x08:protected_mode_start   ; 0x08 = GDT 中代码段选择子(Index=1, TI=0, RPL=0)

🔥 这一步至关重要!
因为 jmp远跳转(far jump),会同时加载 CSEIP,从而让 CPU 用新的段描述符解释后续指令。

步骤 5:设置其他段寄存器

mov ax, 0x10     ; 数据段选择子(Index=2)
mov ds, ax
mov es, ax
mov ss, ax
; 注意:不能直接 mov cs, ax!

步骤 6:进入 32 位代码段(NASM 需用 [bits 32]

[bits 32]
protected_mode_start:
    ; 现在你已在保护模式!
    call kernel_main

六、为什么 GRUB 要帮我们做这些?

因为:

  • 实模式下无法访问 1MB 以上内存
  • 无法实现内存保护和多任务
  • 现代操作系统必须运行在保护模式(或长模式)下

GRUB 在加载 Multiboot 内核前,已经完成了:

  1. 加载 GDT
  2. 设置 CR0.PE = 1
  3. 远跳转到 32 位代码段
  4. 设置好段寄存器

所以你的内核代码天然运行在保护模式——但这不等于你可以忽略它!


七、下一步:准备开启分页!

进入保护模式只是第一步。
接下来,你需要:

  • 启用分页(Paging) → 实现虚拟内存
  • 设置中断描述符表(IDT) → 处理异常和硬件中断
  • 初始化堆和内存管理器 → 为 kmalloc 打基础

而这一切,都建立在你已掌握保护模式的前提之上。


写在最后

从实模式到保护模式,
不仅是 CPU 工作方式的改变,
更是操作系统从"玩具"走向"系统"的分水岭

今天你写的这几行汇编,
正是 Linux、Windows、macOS 内核启动时也曾走过的路。

🌟 理解历史,才能创造未来。


📬 动手试试吧!
即使你不写裸机启动代码,也建议在 QEMU 中模拟一次手动切换过程。
欢迎在评论区分享你的 GDT 定义或遇到的坑!

👇 下一篇你想看:分页机制详解,还是 中断与异常处理(IDT)


#操作系统 #内核开发 #x86 #实模式 #保护模式 #GDT #LDT #段机制 #从零开始


📢 彩蛋:关注后回复关键词 "gdt",获取:

  • 完整 GDT 汇编模板(含注释)
  • 实模式 → 保护模式切换的 QEMU 调试脚本
  • Intel 手册相关章节摘录(中文)