IDT和GDT是什么

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 手里指向它们的两张书签------书签是寄存器,书不是。

相关推荐
Android系统攻城狮1 小时前
Linux Gstreamer深度解析之gst_audio_decoder_set_estimate_rate调用流程与实战(四十五)
linux·gstreamer音视频·音视频进阶
FED_AF1 小时前
Linux运维“邪修”功法之通配符
linux·运维
箓维1 小时前
线程的概念和理解
linux
云杂项2 小时前
tmux指南:安装、配置与高效使用(Ubuntu上)
linux·服务器·ubuntu
j7~2 小时前
【Linux网络编程】四十七.《Linux IO 模型详解:阻塞 IO、非阻塞 IO 与 IO 多路转接(select)》
linux·运维·网络·select·非阻塞io·阻塞io·i/o多路连接
GeW3 小时前
智能工厂背后:Linux在跑,数据库在扛,高端制造在加速
linux
qq_394562003 小时前
【在Linux上升级nginx】
linux·服务器·nginx
Tairitsu_H3 小时前
[Linux系统] 基础IO核心 | 系统接口 | 文件描述符 | 重定向
linux·服务器·文件操作
嵌入式-老费3 小时前
esp32开发与应用(用Python+PySide6解决上位机开发)
linux·单片机·嵌入式硬件