什么是 ‘GDT’ (全局描述符表) 与 ‘IDT’ (中断描述符表)?解析 CPU 如何在硬件层面找到处理代码

欢迎来到本次关于CPU核心机制的深入探讨。今天,我们将聚焦于两个在x86保护模式下至关重要的概念:全局描述符表(Global Descriptor Table, GDT)和中断描述符表(Interrupt Descriptor Table, IDT)。理解它们,就是理解CPU如何在硬件层面管理内存、执行代码,以及响应各种事件和错误。这不仅仅是操作系统内核开发者的必备知识,也是任何希望深入理解计算机系统运作原理的程序员的基石。

CPU的困境与保护模式的崛起

想象一下,一个CPU就像一个永不停歇的指挥家,它需要执行指令,访问数据。在早期的PC架构中,也就是16位实模式下,这一切相对简单粗暴:所有程序共享1MB的内存空间,可以直接访问任何地址,没有保护,没有多任务,也没有虚拟内存的概念。这对于单任务、单用户系统来说尚可接受,但随着计算机技术的发展,多任务操作系统和更复杂的应用程序的出现,实模式的局限性暴露无遗。

核心问题在于:

  1. 内存管理与保护: 如何让多个程序在不相互干扰的情况下共享内存?如何防止一个恶意或错误的程序破坏操作系统或其它程序的内存?
  2. 特权级管理: 如何区分操作系统内核代码和用户应用程序代码?如何防止用户程序执行特权指令或访问特权资源?
  3. 事件响应: 当硬件设备需要CPU注意时(如键盘输入),或者当程序发生错误时(如除零),CPU如何及时、有序地暂停当前任务,转而处理这些事件?

为了解决这些问题,Intel引入了“保护模式”(Protected Mode)。保护模式带来了虚拟内存、内存保护、多任务以及特权级等高级特性。而GDT和IDT,正是实现这些特性的核心硬件机制。它们是CPU在执行指令时,用于查找代码和数据段、以及中断处理程序入口的“地图”和“电话簿”。

分段机制:GDT的基础

在深入GDT之前,我们必须先理解“分段”(Segmentation)这个概念。保护模式下的内存管理最初主要依赖分段。它的核心思想是将内存划分为逻辑上的“段”(Segments),每个段都有自己的基地址(Base Address)、大小(Limit)和访问权限。

CPU的内存访问不再是直接的物理地址,而是通过“逻辑地址”(Logical Address)进行。一个逻辑地址由两部分组成:段选择子(Segment Selector)段内偏移(Offset)

  • 段选择子: 一个16位的数值,它并不直接是段的基地址,而是GDT或LDT(局部描述符表,Local Descriptor Table)中某个描述符的索引。
  • 段内偏移: 一个32位的数值,表示在选定段内的偏移量。

CPU内部的段寄存器(CS, DS, SS, ES, FS, GS)都存储着段选择子。例如:

  • CS (Code Segment): 指向当前正在执行的代码段。
  • DS (Data Segment): 指向当前数据段。
  • SS (Stack Segment): 指向当前堆栈段。
  • ES, FS, GS: 通用数据段寄存器,用于额外的数据访问。

当CPU需要访问一个内存地址时,它会结合段寄存器中的选择子和指令中的偏移量来计算最终的线性地址(Linear Address)。这个计算过程是这样的:

  1. CPU获取段寄存器中的段选择子。
  2. 利用段选择子,在GDT或LDT中查找对应的段描述符。
  3. 从段描述符中提取段的基地址。
  4. 将段的基地址与段内偏移量相加,得到线性地址。
  5. 如果启用了分页机制,这个线性地址还会进一步被转换成物理地址。

这个查找和计算过程完全由硬件自动完成,对程序员是透明的。关键在于,GDT就是存储这些段描述符的地方。

全局描述符表(GDT):内存的蓝图

全局描述符表(GDT)是保护模式下CPU用来查找所有代码段和数据段描述符的核心数据结构。它是一个数组,数组中的每个元素都是一个8字节(64位)的“段描述符”(Segment Descriptor)。

GDT的定位:GDTR寄存器

CPU如何知道GDT在哪里?答案是GDTR(Global Descriptor Table Register)寄存器。GDTR是一个48位的寄存器:

  • 低16位存储GDT的大小限制(Limit),即GDT的字节数减一。
  • 高32位存储GDT的基地址(Base Address),即GDT在物理内存中的起始地址。

操作系统在进入保护模式之前,必须初始化GDT,并使用LGDT指令将GDT的基地址和大小加载到GDTR寄存器中。一旦GDTR设置完毕,CPU在后续的内存访问中,只要涉及段选择子,就会自动查询GDT。

段描述符的结构

一个8字节的段描述符包含了定义一个内存段所需的所有信息。理解其结构是理解GDT的关键。

字段(位) 描述
Bit 0-15 Limit 0-15 (段限长低16位)
Bit 16-31 Base 0-15 (段基址低16位)
Bit 32-39 Base 16-23 (段基址中8位)
Bit 40-43 Type (段类型)
Bit 44 S (描述符类型,0=系统段,1=代码/数据段)
Bit 45-46 DPL (Descriptor Privilege Level,描述符特权级)
Bit 47 P (Present,存在位,1=存在)
Bit 48-51 Limit 16-19 (段限长高4位)
Bit 52 AVL (Available,可用位,供OS使用)
Bit 53 L (Long-mode code 64-bit segment,64位代码段标记,只在长模式下有效)
Bit 54 D/B (Default operation size/Big,操作数大小/B位)
Bit 55 G (Granularity,粒度位,0=字节,1=4KB)
Bit 56-63 Base 24-31 (段基址高8位)

让我们逐一解析这些字段:

  • Base Address (基地址): 一个32位的字段,分散在描述符的不同位置,它指明了段在4GB线性地址空间中的起始地址。
  • Limit (段限长): 一个20位的字段,也分散在描述符中,它定义了段的大小。如果G位为0(字节粒度),则段的最大大小为2^20字节,即1MB。如果G位为1(4KB粒度),则段的最大大小为2^20 * 4KB = 4GB。
  • P (Present 位): 如果P位为1,表示该段描述符有效且对应的段存在于物理内存中。如果为0,则表示该段不在内存中,CPU尝试访问时会触发“段不存在”异常(Interrupt 11)。这在虚拟内存管理中很有用。
  • DPL (Descriptor Privilege Level): 2位,表示该段的特权级(0-3,0是最高特权级,3是最低)。CPU会进行特权级检查,以防止低特权级的代码访问高特权级的段。
  • S (System 位): 1位。
    • S=1:表示这是一个代码段或数据段描述符。
    • S=0:表示这是一个系统段描述符,例如TSS(Task State Segment)或LDT(Local Descriptor Table)的描述符。
  • Type (类型): 4位,与S位结合使用,定义了段的更具体类型和访问权限。
    • 对于代码段 (S=1):
      • 0b1000 (8): 执行上行,不可读
      • 0b1001 (9): 执行上行,可读
      • 0b1100 (C): 执行下行,不可读
      • 0b1101 (D): 执行下行,可读
      • 其中,最高位表示可执行(1),次高位表示一致性(0=非一致性,1=一致性),最低两位表示读/写权限。
    • 对于数据段 (S=1):
      • 0b0000 (0): 不可写,扩展下行
      • 0b0001 (1): 可写,扩展下行
      • 0b0010 (2): 不可写,扩展上行
      • 0b0011 (3): 可写,扩展上行
      • 其中,最高位表示可写(1),次高位表示扩展方向(0=上行,1=下行)。
    • 对于系统段 (S=0):
      • 0b0001 (1): 16位TSS(可用)
      • 0b0010 (2): LDT
      • 0b0011 (3): 16位TSS(忙)
      • 0b1001 (9): 32位TSS(可用)
      • 0b1011 (B): 32位TSS(忙)
      • 还有一些门描述符(如调用门、中断门、陷阱门)也在系统段类型中定义。
  • D/B (Default Operation Size / Big 位):
    • 对于代码段:D=1表示32位操作数和地址,D=0表示16位操作数和地址。
    • 对于数据段:B=1表示堆栈段的默认操作大小是32位(ESP),B=0表示16位(SP)。
  • G (Granularity 位): 粒度。
    • G=0:Limit以字节为单位。
    • G=1:Limit以4KB页面为单位。这意味着实际的段限长是描述符中Limit值的4096倍。
  • AVL (Available 位): 操作系统可用位,硬件不使用此位,可供OS自由使用。
  • L (Long-mode code 64-bit segment): 在64位长模式下,如果L=1,表示这是一个64位代码段。此时D/B位必须为0。

GDT的结构示例(C语言)

为了更直观地理解,我们可以用C语言结构体来表示一个段描述符。由于位字段的分布不连续,通常会使用联合体或位操作来构建它。

#pragma pack(push, 1) // 确保结构体成员紧密排列,没有填充字节

typedef struct {
    uint16_t limit_low;       // 段限长 0-15 位
    uint16_t base_low;        // 基地址 0-15 位
    uint8_t  base_middle;     // 基地址 16-23 位
    uint8_t  access;          // 访问权限和类型
                              // Bit 0-3: Type
                              // Bit 4: S (Descriptor type)
                              // Bit 5-6: DPL (Descriptor Privilege Level)
                              // Bit 7: P (Present)
    uint8_t  flags_limit_high; // 标志位和段限长 16-19 位
                               // Bit 0-3: Limit 16-19
                               // Bit 4: AVL (Available for OS)
                               // Bit 5: L (Long-mode code 64-bit)
                               // Bit 6: D/B (Default operation size / Big)
                               // Bit 7: G (Granularity)
    uint8_t  base_high;       // 基地址 24-31 位
} __attribute__((packed)) GDTEntry; // GCC特有,确保没有填充

// GDTR 寄存器结构
typedef struct {
    uint16_t limit;           // GDT 的大小 - 1
    uint32_t base;            // GDT 的基地址
} __attribute__((packed)) GDTR;

#pragma pack(pop)

// 辅助函数:创建GDT描述符
void create_gdt_entry(GDTEntry* entry, uint32_t base, uint32_t limit, uint8_t access, uint8_t flags) {
    entry->limit_low = (limit & 0xFFFF);
    entry->base_low = (base & 0xFFFF);
    entry->base_middle = (base >> 16) & 0xFF;
    entry->access = access;
    entry->flags_limit_high = ((limit >> 16) & 0x0F) | (flags & 0xF0);
    entry->base_high = (base >> 24) & 0xFF;
}

// 示例:定义GDT
#define GDT_NULL_SEGMENT    0   // 空描述符
#define GDT_CODE_SEGMENT    1   // 内核代码段
#define GDT_DATA_SEGMENT    2   // 内核数据段
#define GDT_TSS_SEGMENT     3   // TSS段

GDTEntry gdt[4]; // 假设GDT有4个条目

// GDT初始化
void init_gdt() {
    // 0x00: 空描述符,必须存在
    create_gdt_entry(&gdt[0], 0, 0, 0, 0);

    // 0x08: 内核代码段 (32位,4GB,可执行,可读,特权级0)
    // Base: 0x0, Limit: 0xFFFFF (4GB)
    // Access: P=1, DPL=0, S=1, Type=0b1010 (Code, Read/Execute, Non-conforming) -> 0x9A
    // Flags: G=1, D/B=1 -> 0xC0
    create_gdt_entry(&gdt[GDT_CODE_SEGMENT], 0, 0xFFFFF, 0x9A, 0xC0);

    // 0x10: 内核数据段 (32位,4GB,可写,特权级0)
    // Base: 0x0, Limit: 0xFFFFF (4GB)
    // Access: P=1, DPL=0, S=1, Type=0b0010 (Data, Read/Write, Expand-up) -> 0x92
    // Flags: G=1, D/B=1 -> 0xC0
    create_gdt_entry(&gdt[GDT_DATA_SEGMENT], 0, 0xFFFFF, 0x92, 0xC0);

    // 假设TSS段在这里创建

    GDTR gdtr;
    gdtr.limit = sizeof(GDTEntry) * 4 - 1; // GDT大小 - 1
    gdtr.base = (uint32_t)&gdt;            // GDT的物理地址

    // 加载GDTR (汇编指令)
    // asm volatile("lgdt %0" : : "m"(gdtr));
    // 刷新段寄存器,确保使用新的GDT
    // asm volatile("mov $0x10, %axnt"
    //              "mov %ax, %dsnt"
    //              "mov %ax, %esnt"
    //              "mov %ax, %fsnt"
    //              "mov %ax, %gsnt"
    //              "mov %ax, %ssnt"
    //              "ljmp $0x08, $continue_pm"); // 长跳转刷新CS
}

GDT如何工作:CPU的地址翻译过程

当CPU需要访问一个逻辑地址(例如 CS:EIPDS:EAX)时,它会执行以下步骤:

  1. 获取段选择子: 从段寄存器(如CS)中获取16位的段选择子。
  2. 解析选择子: 段选择子包含三个部分:
    • 索引(Index, Bit 3-15): 指示GDT或LDT中描述符的索引。
    • TI (Table Indicator, Bit 2): TI=0表示在GDT中查找,TI=1表示在LDT中查找。
    • RPL (Requester’s Privilege Level, Bit 0-1): 请求者的特权级。
  3. 查找描述符: CPU根据TI位和索引,在GDTR指向的GDT或LDTR指向的LDT中找到对应的8字节段描述符。
  4. 描述符缓存: 为了提高效率,CPU会将描述符的基地址、限长和权限等信息加载到隐藏的、不可编程的段寄存器部分(描述符缓存器)中。这意味着一旦一个段选择子被加载到段寄存器中,后续对该段的访问无需再次查询GDT/LDT。
  5. 权限检查: CPU会检查当前特权级(CPL,Current Privilege Level,存储在CS寄存器的Bit 0-1中)、请求特权级(RPL)和描述符特权级(DPL)。
    • 对于数据段访问:CPL <= DPLRPL <= DPL 才能访问。
    • 对于代码段(非一致性):CPL == DPL 才能跳转或调用。
    • 对于代码段(一致性):CPL >= DPL 才能跳转或调用,且CPL不会改变。
    • 如果权限检查失败,会触发“通用保护错误”异常(Interrupt 13)。
  6. 限长检查: 检查段内偏移是否在段限长之内。如果偏移超过限长,会触发“通用保护错误”异常(Interrupt 13)。
  7. 计算线性地址: 将描述符中的基地址与段内偏移量相加,得到线性地址。
  8. (如果启用)分页转换: 如果启用了分页,线性地址会进一步被转换为物理地址。

GDT的存在,使得操作系统能够精细地控制每个代码和数据段的内存位置、大小和访问权限,为内存保护和多任务提供了坚实的基础。

中断与异常:CPU的紧急响应机制

在复杂的系统中,各种事件层出不穷:

  • 硬件中断: 外部设备(如键盘、鼠标、硬盘、网卡)需要CPU处理数据或状态变化时发出的信号。例如,键盘按下时会产生一个中断。
  • 软件中断: 通过INT n指令主动触发的中断,常用于实现系统调用(System Call),让用户程序请求操作系统服务。
  • 异常: CPU在执行指令过程中遇到的错误或不正常情况。
    • 故障(Fault): 错误发生前被检测到,CPU可以纠正并重新执行指令。例如,缺页故障(Page Fault)、通用保护错误。
    • 陷阱(Trap): 错误发生后被检测到,CPU在执行完当前指令后转到处理程序,不会重新执行。例如,调试断点。
    • 终止(Abort): 严重的、无法恢复的错误,通常会导致程序或系统崩溃。例如,硬件错误。

CPU需要一种机制来:

  1. 识别事件的类型(即中断向量号)。
  2. 找到对应的处理程序(Interrupt Service Routine, ISR)。
  3. 在处理程序执行前后保存和恢复CPU的状态。
  4. 在必要时进行特权级切换。

在实模式下,CPU使用一个固定的中断向量表(IVT),它位于内存的最低1KB,存储着256个中断处理程序的段地址和偏移地址。但在保护模式下,这种简单的机制不足以提供内存保护和特权级切换,因此引入了中断描述符表(IDT)。

中断描述符表(IDT):事件的调度中心

中断描述符表(IDT)是保护模式下CPU用来查找所有中断和异常处理程序入口点的核心数据结构。它同样是一个数组,数组中的每个元素都是一个8字节(64位)的“门描述符”(Gate Descriptor)。

IDT的定位:IDTR寄存器

与GDT类似,CPU通过IDTR(Interrupt Descriptor Table Register)寄存器来定位IDT。IDTR也是一个48位的寄存器:

  • 低16位存储IDT的大小限制(Limit),即IDT的字节数减一。
  • 高32位存储IDT的基地址(Base Address),即IDT在物理内存中的起始地址。

操作系统在进入保护模式之前,必须初始化IDT,并使用LIDT指令将IDT的基地址和大小加载到IDTR寄存器中。

门描述符的结构

IDT中的每个条目都是一个门描述符,有三种主要类型:中断门(Interrupt Gate)、陷阱门(Trap Gate)和任务门(Task Gate)。它们都共享一个类似的8字节结构。

以中断门和陷阱门为例,它们的结构几乎相同,只有Type字段有所区别。

字段(位) 描述
Bit 0-15 Offset 0-15 (处理程序入口偏移量低16位)
Bit 16-31 Selector (代码段选择子)
Bit 32-39 Reserved (保留位,必须为0)
Bit 40-43 Type (门类型)
Bit 44 S (描述符类型,对于门描述符,S=0)
Bit 45-46 DPL (Descriptor Privilege Level,描述符特权级)
Bit 47 P (Present,存在位,1=存在)
Bit 48-63 Offset 16-31 (处理程序入口偏移量高16位)

解析这些字段:

  • Offset (偏移量): 一个32位的字段,分散在描述符的不同位置,它指明了中断/异常处理程序在目标代码段内的入口点偏移。
  • Selector (代码段选择子): 一个16位的GDT/LDT选择子,指向中断/异常处理程序所在的代码段描述符。CPU会使用这个选择子来加载处理程序代码段的基地址和权限。
  • P (Present 位): 如果P位为1,表示该门描述符有效且对应的处理程序存在。如果为0,则表示该门不存在,CPU尝试访问时会触发“通用保护错误”异常。
  • DPL (Descriptor Privilege Level): 2位,表示通过此门访问处理程序所需的最低特权级。当通过INT n指令触发软件中断时,CPL <= DPL 才能成功。硬件中断和异常通常忽略DPL。
  • S (Descriptor Type): 对于门描述符,S位必须为0,表示这是一个系统描述符。
  • Type (类型): 4位,定义了门的具体类型。
    • 0b0100 (4): 16位任务门
    • 0b0101 (5): 16位中断门
    • 0b0110 (6): 16位陷阱门
    • 0b1100 (C): 32位任务门
    • 0b1110 (E): 32位中断门
    • 0b1111 (F): 32位陷阱门
  • IST (Interrupt Stack Table): 在64位长模式下,门描述符的某些保留位被重新定义为IST字段,用于指定在中断发生时切换到哪个堆栈。这对于防止堆栈溢出或在处理NMI(非可屏蔽中断)时提供隔离非常有用。在32位模式下,这些位是保留的,必须为0。

中断门 vs. 陷阱门

这两种门的主要区别在于CPU处理IF(Interrupt Flag)位的方式:

  • 中断门(Interrupt Gate): 当通过中断门进入处理程序时,CPU会自动清除EFLAGS寄存器中的IF位(即cli操作),禁用进一步的硬件中断。这可以防止在处理一个中断时被另一个中断打断,确保中断处理的原子性。处理程序完成后,IRET指令会恢复EFLAGS,重新启用中断。
  • 陷阱门(Trap Gate): 当通过陷阱门进入处理程序时,CPU不会清除IF位。这意味着在处理程序执行期间,硬件中断仍然是启用的。陷阱门常用于实现系统调用,因为系统调用通常不需要禁用所有中断,并且可能需要依赖其他中断来完成操作。

任务门则是一种特殊的门,它指向一个TSS(Task State Segment)描述符,用于任务切换。当通过任务门触发中断时,CPU会执行硬件任务切换。

IDT的结构示例(C语言)

#pragma pack(push, 1)

typedef struct {
    uint16_t offset_low;      // 处理程序入口偏移量 0-15 位
    uint16_t selector;        // 代码段选择子
    uint8_t  zero;            // 必须为0
    uint8_t  type_attr;       // 类型和属性
                              // Bit 0-3: Type
                              // Bit 4: S (Descriptor type, must be 0 for gates)
                              // Bit 5-6: DPL (Descriptor Privilege Level)
                              // Bit 7: P (Present)
    uint16_t offset_high;     // 处理程序入口偏移量 16-31 位
} __attribute__((packed)) IDTEntry;

// IDTR 寄存器结构
typedef struct {
    uint16_t limit;           // IDT 的大小 - 1
    uint32_t base;            // IDT 的基地址
} __attribute__((packed)) IDTR;

#pragma pack(pop)

// 辅助函数:创建IDT门描述符
void create_idt_gate(IDTEntry* entry, uint32_t handler_addr, uint16_t selector, uint8_t type_attr) {
    entry->offset_low = (handler_addr & 0xFFFF);
    entry->selector = selector;
    entry->zero = 0; // 必须为0
    entry->type_attr = type_attr;
    entry->offset_high = (handler_addr >> 16) & 0xFFFF;
}

#define IDT_MAX_ENTRIES 256 // IDT最多256个条目
IDTEntry idt[IDT_MAX_ENTRIES];

// IDT初始化
void init_idt() {
    // 假设内核代码段选择子是 0x08 (GDT_CODE_SEGMENT << 3)
    uint16_t kernel_code_selector = 0x08;

    // 示例:设置中断向量 0 (除零异常)
    // P=1, DPL=0, S=0, Type=0b1110 (32-bit Interrupt Gate) -> 0x8E
    create_idt_gate(&idt[0], (uint32_t)divide_by_zero_handler, kernel_code_selector, 0x8E);

    // 示例:设置中断向量 14 (缺页异常)
    // P=1, DPL=0, S=0, Type=0b1110 (32-bit Interrupt Gate) -> 0x8E
    create_idt_gate(&idt[14], (uint32_t)page_fault_handler, kernel_code_selector, 0x8E);

    // 示例:设置中断向量 0x80 (系统调用,通常使用陷阱门)
    // P=1, DPL=3 (允许用户程序触发), S=0, Type=0b1111 (32-bit Trap Gate) -> 0xEE
    create_idt_gate(&idt[0x80], (uint32_t)syscall_handler, kernel_code_selector, 0xEE);

    // 设置其他中断和异常处理程序...

    IDTR idtr;
    idtr.limit = sizeof(IDTEntry) * IDT_MAX_ENTRIES - 1;
    idtr.base = (uint32_t)&idt;

    // 加载IDTR (汇编指令)
    // asm volatile("lidt %0" : : "m"(idtr));
}

// 假设的中断处理函数原型
extern void divide_by_zero_handler();
extern void page_fault_handler();
extern void syscall_handler();

中断处理程序的实现通常需要使用汇编语言,因为它们需要保存和恢复大量的CPU寄存器,并处理错误码。

IDT如何工作:CPU的中断处理流程

当一个中断或异常发生时,CPU会执行一系列硬件操作:

  1. 识别中断向量:
    • 对于硬件中断,中断控制器(PIC或APIC)将中断信号转换为一个0-255范围内的中断向量号,并发送给CPU。
    • 对于软件中断(INT n),指令中的n就是向量号。
    • 对于异常,CPU内部会生成一个预定义的向量号(例如,除零是0,缺页是14)。
  2. 查找门描述符: CPU使用中断向量号作为索引,在IDTR指向的IDT中查找对应的门描述符。
  3. 门描述符检查:
    • 检查P位:如果P位为0,触发“段不存在”异常。
    • 检查DPL:如果中断是由INT n指令触发的,CPU会检查CPL <= DPL。如果检查失败,触发“通用保护错误”。硬件中断和CPU生成的异常通常忽略DPL。
  4. 特权级切换(如果需要):
    • CPU比较当前特权级(CPL)和门描述符指向的代码段的DPL。
    • 如果CPL > DPL(即从低特权级到高特权级),CPU会进行堆栈切换。它会从当前任务的TSS(Task State Segment)中获取新特权级的堆栈指针(SS:ESP),并将旧的堆栈指针压入新堆栈。
    • 这种机制确保内核中断处理程序总是在一个干净、受保护的内核堆栈上运行,防止用户程序通过中断破坏内核堆栈。
  5. 保存CPU上下文: CPU自动将当前程序的上下文信息压入堆栈(无论是当前堆栈还是切换后的新堆栈)。压入的顺序通常是:SS (旧堆栈选择子), ESP (旧堆栈指针), EFLAGS, CS (旧代码段选择子), EIP (旧指令指针)。如果异常带有错误码(如缺页、通用保护错误),错误码也会被压入堆栈。
  6. 清除IF位(仅中断门): 如果是中断门,CPU会清除EFLAGS寄存器中的IF位,禁用进一步的硬件中断。
  7. 加载处理程序地址: CPU从门描述符中加载新的CS:EIP(即处理程序所在的代码段选择子和入口点偏移),开始执行中断处理程序。
  8. 执行处理程序: 中断处理程序执行其任务,例如处理I/O,修复错误等。
  9. 返回: 处理程序最后执行IRET(Interrupt Return)指令。IRET指令会从堆栈中弹出之前保存的CS, EIP, EFLAGS, SS, ESP,恢复CPU的上下文,并根据EFLAGS恢复IF位(重新启用中断),从而返回到被中断的程序继续执行。如果发生特权级切换,IRET也会自动恢复到原来的堆栈。

IDT和GDT协同工作,为CPU提供了在保护模式下安全、高效地管理内存和响应事件的能力。

TSS与特权级转换:GDT与IDT的桥梁

在上述讨论中,我们多次提到了特权级切换和TSS。任务状态段(Task State Segment, TSS)是一个特殊的系统段,其描述符存储在GDT中(S=0,Type=0x09或0x0B)。每个任务(或进程)通常都有一个TSS。

TSS包含了任务的所有硬件上下文信息,包括所有通用寄存器、段寄存器、EFLAGS、EIP等。更重要的是,TSS中还存储了每个特权级(0-2)的堆栈指针(SS:ESP)。

当CPU通过中断门或陷阱门从低特权级(例如用户模式,CPL=3)进入高特权级(例如内核模式,DPL=0)时,CPU会:

  1. 根据TSS中存储的DPL=0的堆栈指针(SS0:ESP0),切换到内核堆栈。
  2. 将旧的SS、ESP(用户模式堆栈)压入新的内核堆栈。
  3. 然后继续压入EFLAGS、CS、EIP等。

这种机制确保了内核代码总是在一个独立的、受保护的堆栈上运行,即使用户程序堆栈损坏,也不会影响内核。

现代化演进:长模式下的GDT与IDT

虽然GDT和IDT是x86保护模式的基石,但在x86-64(长模式,Long Mode)下,它们的作用发生了一些微妙的变化。

  • GDT在长模式下: 分段机制在长模式下被大大简化。代码段和数据段的基地址通常被设置为0,限长被设置为4GB,形成了“平坦模型”(Flat Model)。这意味着段基地址 + 偏移 = 线性地址的转换几乎变成了0 + 偏移 = 线性地址,使得线性地址直接等同于偏移量。然而,GDT仍然是必需的,它至少需要包含一个64位代码段描述符(Type字段中的L位设置为1)和一个数据段描述符。此外,TSS描述符在GDT中仍然至关重要,因为TSS仍然用于特权级切换时的堆栈管理。
  • IDT在长模式下: IDT在长模式下依然扮演着核心角色,其结构和功能与32位保护模式基本相同。门描述符的格式略有修改,以支持64位地址,并且引入了IST(Interrupt Stack Table)字段。IST允许为特定中断向量配置多个独立的堆栈,这对于处理NMI、双重故障等特殊情况非常有用,可以防止堆栈溢出导致系统崩溃。

无论是在32位还是64位模式下,GDT和IDT都是CPU查找代码和处理事件的不可或缺的硬件表格。它们是操作系统实现内存保护、多任务、系统调用和错误处理的底层机制。

总结:CPU核心的导览图

GDT和IDT是x86架构保护模式下CPU操作的两个核心数据结构。GDT通过段描述符定义了内存中代码和数据段的布局、大小和访问权限,为CPU提供了地址翻译和内存保护的蓝图。IDT则通过门描述符,为CPU提供了处理各种中断和异常的入口点,是系统响应事件和错误的核心调度机制。两者协同工作,加上TSS等辅助机制,共同构建了现代操作系统得以安全、稳定运行的底层硬件基础。理解这些机制,是深入探索操作系统内核乃至更底层硬件交互的必由之路。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注