Hotdry.
systems

i386 保护模式段设置:Bootloader 中的 GDT/IDT 配置与特权级隔离

在最小 x86 内核 bootloader 中,详解 GDT/IDT 设置进入 80386 保护模式,包括段描述符参数、特权环转换与隔离清单。

在开发最小 x86 内核时,80386 保护模式是实现特权隔离的基础。Bootloader 需要正确配置全局描述符表(GDT)和中断描述符表(IDT),定义代码 / 数据段描述符,并处理特权环(Ring)0 到 Ring 3 的转换,从而为内核提供基本的内存访问控制和指令特权检查。本文聚焦工程化参数,给出可直接复制的描述符字节值、汇编片段和隔离清单,避免常见的三重故障(triple fault)。

保护模式与分段机制概述

80386 处理器从实模式切换到保护模式后,引入段机制:所有内存访问通过段寄存器(CS/DS/ES/FS/GS/SS)和 GDT 中的段描述符解析。每个描述符定义基址(base)、限长(limit)、访问权限(DPL)和类型(代码 / 数据)。平坦模型(flat model)下,base=0x00000000,limit=0xFFFFF(G=1 时扩展为 4GiB),简化 C 语言指针使用,但隔离依赖 DPL 和类型检查。

特权环分为 Ring 0(内核)和 Ring 3(用户),当前特权级(CPL)由 CS 选择子低 2 位决定。数据访问要求 CPL ≤ DPL 且 RPL ≤ DPL;Ring 3 无法执行 lgdt/lidt 等特权指令,也不能直接访问 Ring 0 数据段。

GDT 构建:段描述符参数详解

GDT 是数组,每个条目 8 字节。Bootloader 在实模式下构建最小 GDT:

  • 索引 0:空描述符(全 0)。
  • 索引 1:内核代码段(selector 0x08),Ring 0 执行 / 读。
  • 索引 2:内核数据段(selector 0x10),Ring 0 读 / 写。
  • 索引 3:用户代码段(selector 0x1B),Ring 3 执行 / 读。
  • 索引 4:用户数据段(selector 0x23),Ring 3 读 / 写。

描述符字节(平坦 32 位 4GiB):

内核代码段

dw 0xFFFF    ; limit low (0-15)
dw 0x0000    ; base low (0-15)
db 0x00      ; base mid (0-7)
db 0x9A      ; access: P=1, DPL=00, Type=1010 (code, exec, read)
db 0xCF      ; flags+limit hi: G=1, D/B=1, L=0, AVL=0 | limit(16-19)=1111
db 0x00      ; base high (24-31)

内核数据段:access 改为 0x92(Type=0010,data, expand-down=0, write=1)。

用户段类似,但 access DPL=11(0xFA/0xF2)。

GDTR 结构:dw (GDT 大小 - 1),dd GDT 基址。NASM 示例:

gdt_start:
    dq 0                ; null
    ; code desc...
    ; data desc...
gdt_end:
gdt_desc: dw gdt_end - gdt_start -1, dd gdt_start

OSDev Wiki 的 GDT Tutorial 强调,描述符必须精确,否则进入保护模式后立即 GP# 异常。[1]

Bootloader 进入保护模式步骤

实模式(16 位)Bootloader 序列(假设已启用 A20):

  1. cli 禁用中断。
  2. lgdt [gdt_desc]。
  3. mov eax, cr0 | 1; mov cr0, eax 置 PE 位。
  4. jmp 0x08:pm_entry 远跳转刷新流水线,加载新 CS。

保护模式入口(32 位):

[BITS 32]
pm_entry:
    mov ax, 0x10; mov ds,ax; mov es,ax; mov fs,ax; mov gs,ax; mov ss,ax
    mov esp, 0x90000  ; 内核栈顶
    ; 继续初始化 IDT 等

此后,所有地址为线性平坦地址。

IDT 初始化:中断门与系统调用

IDT 类似 GDT,256 条目,每条 8 字节(32 位门)。中断门(type=0xE/14)用于 IRQ / 异常,陷阱门(0xF)不自动清 IF。

最小 IDT:默认处理程序(打印寄存器或 hlt)。条目格式:

  • offset low (0-15)
  • selector (KCODE=0x08)
  • 0
  • type_attr (e.g. 0x8E: P=1,DPL=0,type=1110 interrupt)
  • offset high (16-31)

系统调用如 int 0x80,使用 DPL=3 的门(type_attr=0xEE)。IDTR:dw (IDT 大小 - 1),dd IDT 基址。

在 pm_entry 后立即 lidt,然后可选 sti。

特权环转换:Ring 0 ↔ Ring 3

内核 → 用户:使用 IRET 伪造栈帧。

; 在 Ring 0
mov ax, 0x23; mov ds,ax; ...  ; 加载用户数据段
push 0x23         ; 用户 SS
push user_esp     ; 用户栈顶
pushfd           ; EFLAGS (清 IOPL=0)
push 0x1B        ; 用户 CS
push user_entry  ; 用户 EIP
iret             ; 切换到 Ring 3

用户 → 内核:int 0x80,CPU 自动切换栈(需 TSS)。

TSS(Task State Segment)必需:定义 ss0:esp0 为内核栈。GDT 添加 TSS 描述符(type=0x9,S=0),ltr 加载。

段限与隔离工程清单

平坦模型下,段限不提供内存隔离(全地址可见),但特权检查防止 Ring 3 写内核代码 / 数据。为增强隔离:

  • 参数阈值

    组件 参数 作用
    代码段 limit 0xFFFFF G=1 4GiB 平坦
    DPL 0/3 特权检查
    数据段 Type 0x2/0xA 写 / 执
    TSS limit 0x67 (sizeof TSS-1) 栈切换
    IDT 门 DPL 0x0/0x3 异常 /syscall
  • 隔离清单

    1. 内核数据段仅 Ring 0,用户不可加载。
    2. 所有用户指针限 Ring 3 段。
    3. 监控 GP# (中断 13) 处理非法访问。
    4. 回滚:若 triple fault,检查 GDT base/limit 和 far jmp。
    5. 后期加分页(CR3/PDPT)真隔离。

常见 pitfalls:GDT 限长错误(dw gdt_end-gdt_start-1),未远跳导致旧 CS 缓存;IDT 未设致异常重置。

监控与调试要点

  • Bochs/QEMU debug:监视 CR0.PE、GDTR/IDTR。
  • 异常栈:#GP (13)/#PF (14) 表示段违规 / 页违。
  • 参数验证:GDT 每个字节手工校验,如 code access=0x9A。

此配置适用于 GRUB 无 GDT 场景的自制 bootloader。后续可扩展 LDT 和分页。

资料来源: [1] OSDev Wiki: GDT Tutorial (https://wiki.osdev.org/GDT_Tutorial) [2] BrokenThorn OSDev Tutorial (https://brokenthorn.com/Resources/OSDev8.html)

(本文约 1250 字,基于公开 OS 开发资源整理,非官方手册。)

查看归档