IDT 和 GDT 都不是寄存器,它们是放在内存里的"表"(数组);但 CPU 里确实有一对寄存器 GDTR 和 IDTR,专门用来记住这两张表在哪里。 可以理解为"书在书架上,书签在 CPU 手里"。
一、两层结构:表(内存)+ 指针(寄存器)
CPU 内部 内存 (RAM)
┌─────────────┐
│ GDTR (48位) │──┐
│ 限长 | 基址 │ │ 指向 ┌──────────────────────┐
└─────────────┘ └────────────────► │ GDT 全局描述符表 │
┌─────────────┐ │ [0] 空描述符 │
│ IDTR (48位) │──┐ │ [1] 内核代码段描述符 │
│ 限长 | 基址 │ │ 指向 │ [2] 内核数据段描述符 │
└─────────────┘ └────────────────► │ [4] 0号任务TSS ... │
└──────────────────────┘
┌──────────────────────┐
│ IDT 中断描述符表 │
│ [0] 除零处理程序入口 │
│ [0x20] 时钟中断入口 │
│ [0x80] 系统调用入口 │
└──────────────────────┘
GDTR/IDTR 是真正的寄存器,但只有 48 位(16 位限长 + 32 位基址),装不下整张表------所以采用间接寻址 :寄存器只存"表在哪、多大",表本体放在内存里,由操作系统随时重建或搬家。这也解释了为什么有 lgdt/lidt 这种"加载表"指令------实际加载的是表的位置。
二、GDT:段描述符表(保护模式的"户口系统")
它解决什么问题
回忆之前讲过的:实模式下段寄存器是裸数字(0x9000 直接 ×16);保护模式下段寄存器(此时叫选择子)只是个下标,真正的段信息(基址、限长、权限)存在 GDT 表项里。
一个描述符 = 8 字节,描述一个段的全部信息:
┌──────────┬──────────────────────────────────────┐
│ 基址(32位)│ 这段内存从物理/线性哪里开始 │
│ 限长(20位)│ 这段内存有多长(配合粒度位,最大 4GB) │
│ TYPE │ 代码段/数据段?可读/可写/可执行? │
│ DPL(2位) │ 特权级 0~3(内核段 DPL=0,用户不可碰) │
│ P │ 存在位(不在内存则触发缺页) │
│ G │ 粒度(限长单位是字节还是 4KB 页) │
└──────────┴──────────────────────────────────────┘
选择子怎么查表 (head.s 里那句 movl $0x10,%eax):
选择子 0x10 = 二进制 0000 0000 0001 0_000
索引=2 ─┘ │└─ RPL(请求特权级)=0
└─ TI=0 表示查 GDT(=1 查 LDT)
→ GDT 第 2 项 = 内核数据段
对照我们读过的代码,一切对上了:
! setup.s 建的临时 GDT(3 项):
gdt: .word 0,0,0,0 ! [0] 空描述符(规定必须空,用于捕获未初始化的选择子)
.word 0x07FF,... ! [1] 代码段:基址0,8MB,可执行 → 选择子 0x08
.word 0x07FF,... ! [2] 数据段:基址0,8MB,可读写 → 选择子 0x10
jmpi 0,8 ! 跳转"选择子8" = 查GDT第1项 → 代码段基址0
mov ax,#INITSEG... lmsw ! 切换成功,进入保护模式
"全局"和"局部"
- GDT(Global):全系统共享一张,放内核段、各任务的 TSS 和 LDT 描述符;
- LDT(Local) :每个任务一张。Linux 0.11 给每个进程的代码段/数据段各建一个 LDT 描述符,实现进程间内存隔离。
证据就在之前那次验证输出的寄存器转储里:TR=0x0020, LDT=0x0028------这俩选择子正是 GDT 第 4、5 项(0.11 约定:任务 n 的 TSS 放 GDT4+2n、LDT 放 GDT5+2n)。head.s 文件末尾 .fill 252,8,0 预留的空间,就是给未来每个任务的 TSS/LDT 用的。
三、IDT:中断描述符表(保护模式的"应急电话簿")
它解决什么问题
CPU 收到中断/异常号 N 时要去执行对应处理程序------N 对应哪个函数? 实模式的答案叫 IVT(中断向量表):固定放在内存 0x000~0x3FF,每项 4 字节(段:偏移)。保护模式换成了 IDT,本质相同但每项升级为 8 字节的"门",多了权限检查:
IDT 表项(门描述符,8字节):
┌─────────────────────────────────────────────┐
│ 目标EIP低16位 │ 目标代码段选择子 │ P│DPL│TYPE │ 目标EIP高16位 │
└─────────────────────────────────────────────┘
TYPE=0xE 中断门(进门时关中断) / 0xF 陷阱门(保持中断) / 任务门(罕见)
发生 int N(或硬件中断/异常)时,CPU 自动查 IDTR → 取第 N 项 → 检查权限 → 跳到对应处理程序。
两个关键例子(对应 0.11 源码):
// init/main.c → trap_init():
set_trap_gate(14, &page_fault); // 缺页异常:IDT[14],DPL=0,只有内核能"主动调"
// sched_init():
set_intr_gate(0x20, &timer_interrupt); // 时钟:8259A被重映射到0x20,呼应setup.s的重编程
// sched_init():
set_system_gate(0x80, &system_call); // 系统调用:IDT[0x80],且是 DPL=3 的"系统门"
int 0x80 那个门的 DPL 特意设为 3:用户程序(Ring 3)被硬件禁止调 DPL=0 的门,但系统门允许------这就是用户态进入内核态的唯一合法通道,特权级保护体系在这里闭环。
回到代码:head.s 的 setup_idt
setup_idt:
lea ignore_int,%edx ; edx = 默认处理程序地址
movl $0x00080000,%eax
movw %dx,%ax ; eax = [EIP低16位 | 选择子0x0008] ------ 正好拼出门的低4字节
movw $0x8E00,%dx ; 0x8E00 = P=1, DPL=0, TYPE=0xE(32位中断门) ------ 门的高4字节
mov $256,%ecx ; 256 项(中断号8位,最多256个向量)
rp_sidt: ; 循环256次:全部指向 ignore_int
... ; ("谁要装真处理程序,自己来改" ------ 之后trap_init逐个替换)
lidt idt_descr ; 加载 IDTR:限长 256*8-1,基址 = idt(head.s数据区)
而 setup.s 里那句 lidt idt_48 更极端------限长直接填 0 :"暂时一张表都没有",此时任何中断都会让 CPU 崩溃重启动。之所以敢这么干,是因为中断早已被 cli + 8259A 全屏蔽双保险挡住。这也从反面说明:保护模式下没有合法 IDT,系统一秒都活不了。
四、那 4 个"系统地址寄存器"
x86 共有 4 个系统地址寄存器,GDT/IDT 相关的占前两个:
| 寄存器 | 长度 | 内容 | 加载指令 | 存储指令 |
|---|---|---|---|---|
| GDTR | 48位 | GDT 的限长+基址 | lgdt(Ring0特权) |
sgdt(任意特权级可读) |
| IDTR | 48位 | IDT 的限长+基址 | lidt(Ring0特权) |
sidt |
| LDTR | 16位选择子 | 当前任务 LDT 在 GDT 中的选择子 | lldt |
sldt |
| TR | 16位选择子 | 当前任务 TSS 的选择子 | ltr |
str |
注意两个细节:
- GDTR/IDTR 本身只有 6 字节容量(限长2 + 基址4),这就是"表必须放内存、寄存器只放指针"的硬件原因;
lgdt/lidt是特权指令(用户态执行直接 #GP),但sgdt/sidt竟然任何程序都能执行------一个有趣的历史遗留:用户程序可以探测到内核表放在哪,这也是某些漏洞利用的起点。
五、Linux 0.11 中这两张表的完整生命史
setup.s lidt idt_48 IDTR ← 限长0(空表,靠中断屏蔽保命)
lgdt gdt_48 GDTR ← 临时GDT(在setup自己的内存里,稍后会被覆盖)
只有 内核代码段0x08 / 数据段0x10 两项,够跳进head.s即可
│
head.s call setup_idt IDT ← 256项全指向 ignore_int(打印"Unknown interrupt")
call setup_gdt GDTR ← 正式GDT(建在内核数据区,限长扩到16MB)
│
main() trap_init() IDT ← 填入真货:除零、缺页(14)、时钟(0x20)、键盘(0x21)...
sched_init() IDT[0x80] ← system_call(DPL=3系统门)
GDT ← 逐任务追加:GDT[4]=0号任务TSS、GDT[5]=0号LDT
│
进程fork时 GDT ← 为新进程分配 TSS 和 LDT 描述符(get_free_page找空位)
六、总结
| GDT | IDT | |
|---|---|---|
| 本体 | 内存中的表 | 内存中的表 |
| 对应寄存器 | GDTR(48位指针) | IDTR(48位指针) |
| 表项内容 | 段描述符:基址/限长/权限 | 门描述符:处理程序入口/权限 |
| 被谁索引 | 段寄存器(选择子) | 中断号 int N / 异常号 |
| 决定什么 | "这段内存是什么、谁能怎么用" | "发生事件N该执行哪段代码、谁能触发" |
| 0.11 中谁维护 | head.s 建骨架,sched.c 为每任务加 TSS/LDT | head.s 全指 ignore_int,trap_init/sched_init 填真货 |
一句话收尾:GDT 是"地址的翻译规则表",IDT 是"事件的响应规则表";它们躺在内存里,GDTR/IDTR 是 CPU 手里指向它们的两张书签------书签是寄存器,书不是。