异常和中断

异常的入栈出栈流程

抱歉之前的内容仍存在混淆。按照栈"后进先出"的原则,在退出中断时,若存在错误码,确实应该先将错误码弹出。以下是完整且正确的进入中断和退出中断时的入栈出栈顺序表格:

进入中断(异常发生时)的入栈顺序(CPU 硬件行为)

| 步骤 | 操作内容 | 操作主体 | 操作前 ESP | 操作后 ESP | 栈中存储情况(从栈顶到栈底) | | — | — | — | — | — | — | | 1 | 压入 EFLAGS | CPU 硬件 | 0x20 | 0x1C | 地址 0x1C: EFLAGS | | 2 | 压入 CS | CPU 硬件 | 0x1C | 0x18 | 地址 0x18: CS
地址 0x1C: EFLAGS | | 3 | 压入 EIP | CPU 硬件 | 0x18 | 0x14 | 地址 0x14: EIP
地址 0x18: CS
地址 0x1C: EFLAGS | | 4 | 压入错误码(若异常有) | CPU 硬件 | 0x14 | 0x10 | 地址 0x10: 错误码
地址 0x14: EIP
地址 0x18: CS
地址 0x1C: EFLAGS |

退出中断(异常处理完成后)的出栈顺序

| 步骤 | 操作内容 | 操作主体 | 操作前 ESP | 操作后 ESP | 栈中剩余存储情况(从栈顶到栈底) | | — | — | — | — | — | — | | 1 | 弹出错误码(若有) | 程序代码(手动处理) | 0x10 | 0x14 | 地址 0x14: EIP
地址 0x18: CS
地址 0x1C: EFLAGS | | 2 | 弹出 EIP | CPU 硬件(执行 iret 指令) | 0x14 | 0x18 | 地址 0x18: CS
地址 0x1C: EFLAGS | | 3 | 弹出 CS | CPU 硬件(执行 iret 指令) | 0x18 | 0x1C | 地址 0x1C: EFLAGS | | 4 | 弹出 EFLAGS | CPU 硬件(执行 iret 指令) | 0x1C | 0x20 | 无 |

解释

  • 进入中断:当异常发生时,CPU 硬件会自动按照 EFLAGSCSEIP、错误码(如果该异常产生错误码)的顺序将这些值压入栈中,以保存被中断程序的执行现场。
  • 退出中断
    • 由于错误码是最后压入栈的,所以需要在程序代码中手动先将其弹出(例如使用 add esp, 4 指令跳过错误码,或者使用 pop 指令将其弹出)。
    • 之后执行 iret 指令,iret 指令会按照 EIPCSEFLAGS 的顺序从栈中弹出相应的值,从而恢复被中断程序的执行现场,使程序能够从中断处继续执行。

所有异常列表

以下是 x86 架构中常见异常的详细信息表格,涵盖异常编号、名称、是否有错误码、进入中断和退出中断时的入栈出栈顺序等内容:

异常编号 异常名称 是否有错误码 进入中断入栈顺序(CPU 硬件行为) 退出中断出栈顺序(程序代码手动处理 + CPU 硬件 iret 指令) 简要说明
0 除法错误(Divide Error) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行除法指令(DIVIDIV)时,除数为 0 或商无法表示时触发。
1 单步中断(Debug Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
用于调试,当 EFLAGS 寄存器中的单步标志(TF)置 1 时,每条指令执行后触发。
2 非屏蔽中断(NMI Interrupt) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
由硬件触发的非屏蔽中断,通常用于处理紧急情况,如硬件故障。
3 断点中断(Breakpoint Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行 INT 3 指令时触发,用于调试。
4 溢出中断(Overflow Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行 INTO 指令且 EFLAGS 寄存器中的溢出标志(OF)置 1 时触发。
5 边界检查异常(BOUND Range Exceeded Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行 BOUND 指令时,操作数超出指定范围触发。
6 无效操作码异常(Invalid Opcode Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
CPU 遇到无法识别的操作码时触发。
7 设备不可用异常(Device Not Available Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行浮点运算指令,但协处理器不可用时触发。
8 双重故障异常(Double Fault Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
尝试处理一个异常时,又发生另一个异常,且无法正确处理时触发。
9 协处理器段超越异常(Coprocessor Segment Overrun) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
早期 x86 架构中,浮点运算跨越段边界时触发,现代架构已较少使用。
10 无效 TSS 异常(Invalid TSS Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
任务切换时,TSS(任务状态段)无效或不可访问时触发。
11 段不存在异常(Segment Not Present Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
访问的段描述符的 P(存在位)为 0 时触发。
12 栈段异常(Stack-Segment Fault Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
访问栈段时发生错误,如栈溢出或栈段不存在时触发。
13 一般保护异常(General Protection Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
违反系统保护规则时触发,如访问权限不足等。
14 页错误异常(Page Fault Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
访问的页面不在物理内存中,触发页错误,需要进行页面置换等操作。
16 浮点错误异常(x87 FPU Floating-Point Error) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行 x87 浮点运算指令时发生错误。
17 对齐检查异常(Alignment Check Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
4. 压入错误码
1. 手动弹出错误码
2. iret 弹出 EIP
3. iret 弹出 CS
4. iret 弹出 EFLAGS
访问未对齐的内存地址时触发,需要启用对齐检查标志。
18 机器检查异常(Machine Check Exception) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
硬件检测到内部错误,如总线错误、缓存错误等。
19 - 31 保留异常 - - - 为未来的 CPU 扩展保留。
32 - 255 可屏蔽中断(Interrupts) 通常无(取决于中断处理程序设计) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
由外部设备通过中断控制器发送的中断请求。

这个表格总结了 x86 架构中常见异常的关键信息,包括异常编号、名称、是否有错误码以及进入和退出中断时的栈操作顺序,同时对每个异常的触发条件做了简要说明。

IRQ列表

在 x86 架构中,IRQ(Interrupt Request,中断请求)是外部设备向 CPU 发送的信号,用于请求 CPU 处理特定事件。以下是常见的 IRQ 列表及相关信息整理:

IRQ 编号 名称 是否有错误码 压栈信息 出栈恢复信息 触发原因及用途
0 系统定时器(System Timer) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
由系统定时器芯片(如 PIT)周期性触发,用于实现系统的时钟滴答、进程调度等功能。
1 键盘控制器(Keyboard Controller) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
当用户按下或释放键盘上的按键时,键盘控制器会向 CPU 发送此中断,以便操作系统处理键盘输入。
2 级联中断(Cascade) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
用于连接主中断控制器(PIC)和从中断控制器,使得系统可以处理更多的中断请求。
3 串口 2(COM2) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
当串口 2 有数据到达或发生错误时,会触发此中断,以便操作系统进行相应的处理。
4 串口 1(COM1) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
功能与串口 2 类似,用于处理串口 1 的数据传输和错误情况。
5 并行口 2/声卡(LPT2/Sound Card) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
可用于并行口 2 的数据传输,也可能被声卡等设备使用,具体取决于系统配置。
6 软盘控制器(Floppy Disk Controller) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
当软盘驱动器进行读写操作或发生错误时,会触发此中断。
7 并行口 1(LPT1) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
用于处理并行口 1 的数据传输和状态变化。
8 实时时钟(Real - Time Clock,RTC) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
实时时钟芯片会按一定周期触发此中断,可用于系统时间的更新和定时任务。
9 重定向为通用用途(Redirected to General Purpose) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
在某些系统中,此中断可能被重定向用于其他设备或功能。
10 通用用途(General Purpose) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
可由系统管理员或设备驱动程序配置给特定的设备使用。
11 通用用途(General Purpose) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
用途与 IRQ 10 类似,可灵活分配给不同设备。
12 鼠标控制器(PS/2 Mouse Controller) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
当鼠标移动或按键被按下/释放时,鼠标控制器会触发此中断,以便操作系统处理鼠标输入。
13 数学协处理器(Math Coprocessor) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
当数学协处理器(如早期的 x87 FPU)发生错误或完成运算时,会触发此中断。
14 主 IDE 控制器(Primary IDE Controller) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
用于处理主 IDE 接口(通常连接硬盘、光驱等设备)的数据传输和状态变化。
15 从 IDE 控制器(Secondary IDE Controller) 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
功能与主 IDE 控制器类似,用于处理从 IDE 接口的设备。

需要注意的是,现代计算机系统中,随着硬件的发展和中断控制器的改进(如使用 APIC 替代传统的 PIC),IRQ 的分配和管理方式可能会有所不同。同时,一些设备可能会使用更高级的中断机制(如 MSI,Message - Signaled Interrupts)来提高中断处理的效率。

iret指令

特权场景

抱歉,此前表格存在逻辑错误,现重新梳理修正(基于32位系统,按iret弹栈逻辑:先弹EFLAGS,再弹CS,最后弹EIP等,结合入栈顺序还原正确栈操作):

操作阶段 操作步骤 操作内容 操作前 ESP 操作后 ESP 栈中存储情况(从栈顶到栈底)
入栈 初始状态 - 20 20
入栈 1 pushl $0x23 20 16 地址 16: SS
入栈 2 pushl %%eax 16 12 地址 12: ESP
地址 16: SS
入栈 3 pushfl 12 8 地址 8: EFLAGS
地址 12: ESP
地址 16: SS
入栈 4 orl $0x200, (%%esp) 8 8 地址 8: EFLAGS
地址 12: ESP
地址 16: SS
入栈 5 pushl $0x1B 8 4 地址 4: CS
地址 8: EFLAGS
地址 12: ESP
地址 16: SS
入栈 6 pushl %%ebx 4 0 地址 0: EIP
地址 4: CS
地址 8: EFLAGS
地址 12: ESP
地址 16: SS
弹栈 1 弹出 EIP 0 4 地址 4: CS
地址 8: EFLAGS
地址 12: ESP
地址 16: SS(弹出值:EIP
弹栈 2 弹出 CS 4 8 地址 8: EFLAGS
地址 12: ESP
地址 16: SS(弹出值:CS
弹栈 3 弹出 EFLAGS 8 12 地址 12: ESP
地址 16: SS(弹出值:EFLAGS
弹栈 4 弹出 ESP 12 16 地址 16: SS(弹出值:ESP
弹栈 5 弹出 SS 16 20 无(弹出值:SS

修正说明:

  1. iret 弹栈逻辑:按 EIPCSEFLAGS 顺序弹出(特权级切换场景),表格按此还原。
  2. 每步弹栈操作,优先弹出栈顶数据(即最后入栈的 EIP 最先被弹出)。

无特权切换场景

iret 正常弹栈逻辑:先弹 EIP,再弹 CS,最后弹 EFLAGS)重新梳理,假设初始 ESP = 20,32 位系统下每步栈操作 ESP 增减 4:

操作阶段 操作步骤 操作内容 操作前 ESP 操作后 ESP 栈中存储情况(从栈顶到栈底)
入栈 初始状态 - 20 20
入栈 1 pushfl(压 EFLAGS 20 16 地址 16: EFLAGS
入栈 2 pushl $0x1B(压 CS 16 12 地址 12: CS
地址 16: EFLAGS
入栈 3 pushl %%ebx(压 EIP 12 8 地址 8: EIP
地址 12: CS
地址 16: EFLAGS
弹栈 1 弹出 EIP 8 12 地址 12: CS
地址 16: EFLAGS(弹出值:EIP
弹栈 2 弹出 CS 12 16 地址 16: EFLAGS(弹出值:CS
弹栈 3 弹出 EFLAGS 16 20 无(弹出值:EFLAGS

关键逻辑说明:

  • 入栈顺序:按 EFLAGSCSEIP 压栈,栈顶地址逐步递减(20→16→12→8)。
  • 弹栈顺序iretEIP(栈顶最先压入)→ CSEFLAGS 弹出,栈顶地址逐步回增(8→12→16→20),最终恢复初始 ESP 值。

iret, iretd, iretq区别

iretiretdiretq 都是用于从中断或异常处理程序返回的指令,不过它们在不同的架构和操作数大小方面存在差异,下面为你详细介绍:

1. 操作数大小与架构

  • iret
    • 适用架构:它是一个通用的中断返回指令,在 16 位和 32 位 x86 架构中都可以使用。
    • 操作数大小:在 16 位模式下,iret 会弹出 6 个字节的数据,分别是 2 字节的 IP(指令指针)、2 字节的 CS(代码段寄存器)和 2 字节的 FLAGS(标志寄存器);在 32 位模式下,它会弹出 12 个字节的数据,即 4 字节的 EIP、4 字节的 CS 和 4 字节的 EFLAGS
  • iretd
    • 适用架构:专门用于 32 位 x86 架构。
    • 操作数大小iretd 指令明确弹出 12 个字节的数据,具体为 4 字节的 EIP、4 字节的 CS 和 4 字节的 EFLAGS。在 32 位系统中,iretdiret 的功能是一样的,只是 iretd 更清晰地表明是 32 位的中断返回操作。
  • iretq
    • 适用架构:用于 64 位 x86 - 64 架构。
    • 操作数大小iretq 会弹出 16 个字节的数据,包含 8 字节的 RIP(64 位指令指针)、8 字节的 CS 和 8 字节的 RFLAGS(64 位标志寄存器)。这是为了适应 64 位系统更大的地址空间和寄存器宽度。

2. 指令功能与使用场景

  • 功能
    • 这三条指令的核心功能都是从当前的中断或异常处理程序中返回,恢复被中断程序的执行环境。它们会按照特定的顺序从栈中弹出相应的寄存器值,然后将控制权交还给被中断的程序。
  • 使用场景
    • iret:当编写的代码需要同时兼容 16 位和 32 位系统时,使用 iret 是一个合适的选择。例如,在一些早期的操作系统开发或者跨平台的中断处理代码中会用到。
    • iretd:在 32 位系统的开发中,如果需要明确表示是 32 位的中断返回操作,使用 iretd 可以提高代码的可读性。比如在 32 位 Linux 内核的中断处理代码中就可能会使用到。
    • iretq:在 64 位系统的开发中,必须使用 iretq 来进行中断返回操作,以确保能够正确恢复 64 位的执行环境。例如,64 位 Windows 或 Linux 内核的中断处理程序就会使用 iretq

3. 示例代码片段(伪代码)

16 位模式下的 iret

; 假设中断处理程序结束,准备返回
iret ; 弹出 IP、CS 和 FLAGS 恢复执行环境

32 位模式下的 iretd

; 32 位中断处理程序结束
iretd ; 弹出 EIP、CS 和 EFLAGS 恢复执行环境

64 位模式下的 iretq

; 64 位中断处理程序结束
iretq ; 弹出 RIP、CS 和 RFLAGS 恢复执行环境

综上所述,iretiretdiretq 的主要区别在于适用的架构和操作数大小,你需要根据具体的系统架构来选择合适的指令。

call和ret指令

以下是补充了不同CALL指令区别的详细表格:

CALL指令分类及区别

| 指令类型 | 操作数类型 | 执行流程 | 适用架构 | 使用场景 | 示例代码 | |—————-|——————|————————————————————————–|———-|————————————————————————–|——————————| | call label | 相对偏移量(近调用) | 1. 压入当前EIP/RIP
2. 跳转到label地址 | 16/32/64位 | 同一代码段内的函数调用 | call subproc
call 0x1234 | | call reg | 寄存器中的地址 | 1. 压入当前EIP/RIP
2. 跳转到寄存器指定的地址 | 32/64位 | 通过寄存器间接调用(如call eax) | call edi
call rax | | call mem | 内存中的地址 | 1. 压入当前EIP/RIP
2. 跳转到内存指定的地址 | 32/64位 | 通过内存间接调用(如call DWORD PTR [esi]) | call [0x1000]
call qword ptr [rbp+8] | | lcall | 段选择子+偏移量(远调用) | 1. 压入当前CS和EIP
2. 跳转到指定段和偏移 | 16/32位 | 跨代码段调用(实模式/保护模式) | lcall 0x08:0x1234 | | call far ptr | 段选择子+偏移量(远调用) | 功能与lcall相同,但语法更明确 | 16/32位 | 显式远调用(兼容旧语法) | call far ptr [0x1000] | | call qword ptr | 64位远指针 | 1. 压入当前CS和RIP
2. 跳转到64位地址(含段选择子) | 64位 | 长模式下的远调用(需配合RIP相对寻址) | call qword ptr [rip+0x100] |

关键区别说明

  1. 调用范围
    • 近调用(call:仅修改指令指针(IP/EIP/RIP),用于同一代码段内的跳转。
    • 远调用(lcall/call far ptr/call qword ptr:修改段寄存器(CS)和指令指针,用于跨段调用(需特权级检查)。
  2. 参数传递
    • 近调用:操作数为目标地址的相对偏移量(如call subproc)或绝对地址(如call 0x1234)。
    • 远调用:操作数为段选择子和偏移量的组合(如0x08:0x1234)。
  3. 架构差异
    • 16位:使用lcallretf,操作数为16位段选择子+16位偏移量。
    • 32位:call支持32位偏移量,lcall支持32位偏移量+16位段选择子。
    • 64位:call默认使用RIP相对寻址,call qword ptr用于远调用(需显式指定段选择子)。
  4. 现代处理器优化
    • 64位模式下,远调用较少使用,通常通过syscall指令进入内核态。
    • 编译器会优先使用近调用(call)以提高效率。

示例对比

; 近调用(同一代码段)
call subroutine  ; 压入当前EIP,跳转到subroutine

; 远调用(跨段)
lcall 0x08:0x1000  ; 压入当前CS和EIP,跳转到段0x08的0x1000地址

; 64位远调用
call qword ptr [rip+0x20]  ; 压入当前CS和RIP,跳转到[rip+0x20]指向的64位地址

如果需要进一步解释某个指令的细节,可以随时补充说明!

TSS 任务状态段

以下是结合之前两个回答内容的整合版本,采用结构化呈现:

任务状态段(TSS)深度解析

一、TSS核心概念

任务状态段(Task State Segment, TSS) 是x86架构用于存储任务上下文的核心数据结构,包含:

  • 寄存器组:通用寄存器(EAX/EBX/ECX等)、段寄存器(CS/DS/ES等)
  • 控制信息:指令指针(EIP)、栈指针(SS/ESP)、特权级(CPL/IOPL)
  • 硬件辅助字段:调试陷阱标志、链接指针、I/O许可位图

关键功能

  • 支持硬件任务切换(通过ltr/task指令)
  • 存储中断上下文(内核栈指针)
  • 实现特权级保护机制

二、TSS架构演进

| 模式 | TSS结构差异 | 典型应用场景 | |————–|————————————-|—————————| | 保护模式 | 存储完整任务上下文(寄存器+控制信息)| 硬件任务切换(较少使用) | | 长模式 | 仅存储中断栈表(IST) | 中断处理栈切换 |

三、Linux中的TSS实现

1. 数据结构

// arch/x86/include/asm/tss.h
struct tss_struct {
    unsigned long esp0;    // 特权级0栈指针
    unsigned long ss0;     // 特权级0栈段
    unsigned long ldt;     // LDT选择子
    unsigned short iomap;  // I/O许可位图基址
    // ...其他保留字段
};

2. 初始化流程

// arch/x86/kernel/tss.c
void __init tss_setup_idtentry(int cpu) {
    struct tss_struct *tss = &per_cpu(init_tss, cpu);
    
    // 设置内核栈指针
    tss->esp0 = per_cpu(kernel_stack, cpu) + THREAD_SIZE;
    tss->ss0 = __KERNEL_DS;
    
    // 加载TSS到TR寄存器
    load_TR(&tss_desc);
}

四、核心应用场景解析

场景1:中断处理栈切换

软硬件流程

| 阶段 | 硬件行为 | 软件行为 | 关键指令/寄存器 | |——–|————————————————————————–|————————————————————————–|———————-| | 进入 | 1. 加载TSS.esp0/ss0
2. 压入EFLAGS/CS/EIP到内核栈 | 1. 保存通用寄存器(pusha
2. 保存段寄存器(push ds/es/fs/gs) | IDTR, TR | | 处理 | 无 | 1. 调用do_IRQ()处理中断
2. 恢复寄存器(pop ds/es/fs/gs, popa) | call, iretd | | 返回 | 1. 弹出EIP/CS/EFLAGS
2. 恢复用户栈指针(SS/ESP) | 无 | iret |

指令示例

interrupt_entry:
    pusha               ; 保存通用寄存器
    push ds             ; 保存段寄存器
    call do_IRQ         ; 调用C处理函数
    pop ds
    popa
    iret                ; 返回用户态

场景2:系统调用(syscall)

软硬件流程

| 阶段 | 硬件行为 | 软件行为 | 关键指令/寄存器 | |——–|————————————————————————–|————————————————————————–|———————-| | 触发 | 1. 执行syscall指令
2. 加载TSS.esp0/ss0
3. 压入RIP/CS/RFLAGS到内核栈 | 无 | syscall, TR | | 处理 | 无 | 1. 保存参数(push rdi/rsi/rbx
2. 调用sys_read等系统服务例程 | system_call | | 返回 | 1. 执行sysret指令
2. 弹出RIP/CS/RFLAGS
3. 恢复用户栈指针 | 无 | sysret |

指令示例

; 用户态
mov eax, __NR_read
syscall

; 内核态
system_call:
    push rbx          ; 保存寄存器
    call sys_read
    pop rbx
    sysret

场景3:硬件上下文保存(缺页异常)

软硬件流程

| 阶段 | 硬件行为 | 软件行为 | 关键指令/寄存器 | |——–|————————————————————————–|————————————————————————–|———————-| | 触发 | 1. 检测到页错误
2. 压入EFLAGS/CS/EIP/错误码到内核栈 | 无 | CR2, TR | | 处理 | 无 | 1. 分析错误码(do_page_fault
2. 分配物理页并更新页表 | alloc_page, mmap | | 返回 | 1. 执行iret指令
2. 弹出EIP/CS/EFLAGS
3. 恢复用户栈指针 | 无 | iret |

指令示例

page_fault:
    push %error_code   ; 硬件已压入错误码
    call do_page_fault
    add $4, %esp       ; 跳过错误码
    iret

五、关键技术对比

| 维度 | 中断处理 | 系统调用 | 硬件异常处理 | |————–|————————-|————————-|————————-| | 触发方式 | 外部中断(如IRQ) | 软件指令(syscall) | 硬件错误(如页错误) | | 栈切换 | 自动加载TSS.esp0 | 自动加载TSS.esp0 | 自动加载TSS.esp0 | | 特权级 | 用户态→内核态 | 用户态→内核态 | 用户态→内核态 | | 保存内容 | EFLAGS/CS/EIP + 错误码 | RFLAGS/CS/RIP | EFLAGS/CS/EIP + 错误码 |

六、总结

| 特性 | 说明 | |————–|———————————————————————-| | 核心功能 | 存储任务上下文,支持硬件栈切换 | | Linux实现 | 每个CPU一个TSS,仅保存内核栈和LDT信息 | | 关键指令 | ltr(加载TSS)、syscall(系统调用)、iret(中断返回) | | 优化点 | 避免硬件任务切换,仅保留最小化硬件支持 |

应用建议

  • 系统开发者需重点关注TSS的初始化和中断栈配置
  • 应用开发者可通过ltr指令访问TSS,但需注意特权级限制
  • 硬件上下文保存场景需结合CR2寄存器分析异常原因

七、硬件任务切换应该怎么做?用 TSS

以下是关于硬件任务切换的详细技术解析,结合x86架构原理和TSS使用方法:

一、硬件任务切换核心机制

1. 关键数据结构

  • 任务状态段(TSS)
    每个任务对应一个TSS段,包含:
    struct tss32 {
        uint32_t prev_tss;       // 前一个任务TSS选择子(链接指针)
        uint32_t esp0;           // 特权级0栈指针
        uint32_t ss0;            // 特权级0栈段选择子
        uint32_t esp1;           // 特权级1栈指针(保护模式)
        uint32_t ss1;            // 特权级1栈段选择子
        uint32_t esp2;           // 特权级2栈指针
        uint32_t ss2;            // 特权级2栈段选择子
        uint32_t cr3;            // 页目录基址寄存器
        uint32_t eip;            // 指令指针
        uint32_t eflags;         // 标志寄存器
        uint32_t eax, ecx, edx, ebx; // 通用寄存器
        uint32_t esp;            // 用户栈指针(SS/ESP)
        uint32_t ebp;            // 基址指针
        uint32_t esi;            // 源变址寄存器
        uint32_t edi;            // 目标变址寄存器
        uint16_t es, cs, ss, ds, fs, gs; // 段寄存器
        uint16_t ldt;            // LDT选择子
        uint16_t iomap_base;     // I/O许可位图基址
    };
    
  • 任务门描述符(Task Gate Descriptor)
    用于指向目标任务的TSS,结构如下:
    struct task_gate {
        uint16_t offset_low;      // TSS选择子偏移低16位
        uint16_t selector;        // TSS段选择子
        uint8_t  dcount;          // 保留(必须为0)
        uint8_t  attributes;      // P(1), DPL(0-3), TYPE(5)
        uint16_t offset_high;     // TSS选择子偏移高16位
    };
    

二、硬件任务切换实现步骤

1. 初始化准备

; 定义全局描述符表(GDT)
gdt:
    .quad 0x0000000000000000       ; 空描述符
    .quad 0x00CF9A000000FFFF       ; 代码段(32位,特权级0)
    .quad 0x00CF92000000FFFF       ; 数据段(32位,特权级0)
    .quad tss_desc                ; TSS描述符
    .quad task_gate_desc          ; 任务门描述符

; TSS描述符格式(32位)
tss_desc:
    .word 0x0000                   ; TSS段界限低16位
    .word 0x0000                   ; TSS段基址低16位
    .byte 0x00                     ; TSS段基址中8位
    .byte 0x89                     ; 类型(B=1, TSS=9)
    .byte 0x00                     ; 段界限高4位
    .byte 0x00                     ; TSS段基址高8位

; 任务门描述符(在IDT中)
task_gate_desc:
    .word 0x0000                   ; 偏移低16位
    .word tss_selector              ; TSS选择子(索引为3)
    .byte 0x00                     ; DCount
    .byte 0x85                     ; P=1, DPL=0, TYPE=5
    .word 0x0000                   ; 偏移高16位

2. 创建任务TSS

// 任务A的TSS
struct tss32 task_a_tss = {
    .ss0 = DATA_SEG_SEL,          // 特权级0栈段
    .esp0 = 0x00007c00,           // 特权级0栈指针
    .cr3 = page_directory_a,       // 页目录基址
    .eip = 0x00007c00,            // 任务入口地址
    .cs = CODE_SEG_SEL,           // 代码段选择子
    .eflags = 0x00000202,         // 启用IF标志
    // 其他寄存器初始化...
};

// 任务B的TSS
struct tss32 task_b_tss = {
    .ss0 = DATA_SEG_SEL,
    .esp0 = 0x00007d00,
    .cr3 = page_directory_b,
    .eip = 0x00007d00,
    .cs = CODE_SEG_SEL,
    .eflags = 0x00000202,
    // 其他寄存器初始化...
};

3. 任务切换指令

; 通过任务门切换任务
jmp far [task_gate_desc]

; 直接切换任务(使用TSS选择子)
jmp 0x18:0x00000000  ; 选择子0x18指向目标TSS

三、硬件任务切换流程

1. 触发任务切换

; 用户态任务A执行切换指令
mov eax, 0x18         ; TSS选择子
ltr ax               ; 加载任务A的TSS到TR寄存器

; 任务A主动切换到任务B
jmp far [task_b_gate]

2. 硬件自动执行步骤

| 步骤 | 操作内容 | 涉及寄存器/结构 | |——|———-|——————| | 1 | 保存当前任务上下文到旧TSS | TR寄存器指向的TSS | | 2 | 加载新任务TSS到TR寄存器 | 目标TSS选择子 | | 3 | 切换页目录(CR3) | 新TSS.cr3 | | 4 | 加载段寄存器(CS/DS等) | 新TSS中的段选择子 | | 5 | 恢复通用寄存器(EAX/EBX等) | 新TSS中的寄存器值 | | 6 | 跳转至新任务入口(EIP) | 新TSS.eip |

四、特权级切换场景

1. 同一特权级切换

; 任务A(特权级0)切换到任务B(特权级0)
jmp far [task_b_tss_selector]

2. 特权级提升(用户态→内核态)

; 用户态任务执行特权指令触发异常
mov eax, 0x00000000
lgdt [gdt_ptr]          ; 特权指令,触发异常

; 异常处理程序切换到内核任务
iret                    ; 硬件自动加载TSS.esp0

五、注意事项

  1. TSS描述符设置
    • 必须设置B标志(0x80)表示32位TSS
    • 段界限需设置为0x67(103字节)
  2. 任务门限制
    • 任务门必须位于IDT中
    • 目标TSS必须位于GDT中
  3. 现代架构限制
    • 长模式下TSS仅支持中断栈表(IST)
    • 硬件任务切换需通过VMX虚拟化技术实现

六、代码示例

1. 汇编实现任务切换

section .text
extern task_a_tss
extern task_b_tss

switch_task:
    ; 保存当前任务上下文
    mov eax, [tr_reg]
    ltr ax

    ; 加载新任务TSS
    mov ax, task_b_tss_selector
    ltr ax

    ; 切换页目录
    mov eax, [task_b_tss.cr3]
    mov cr3, eax

    ; 跳转至新任务
    jmp task_b_tss.eip

2. C语言封装接口

void switch_to_task(struct tss32 *tss) {
    asm volatile (
        "ltr %%ax\n"
        "mov %%eax, %%cr3\n"
        "ljmp $0x%x, $0x%x\n"
        : 
        : "a" (tss->selector),
          "i" (tss->cs),
          "b" (tss->eip)
        : "memory"
    );
}

七、对比总结

| 特性 | 硬件任务切换 | 软件任务切换(Linux) | |————–|—————————–|—————————–| | 上下文保存 | 硬件自动完成 | 软件显式保存(如switch_to)| | 切换开销 | 约500-1000 CPU周期 | 约100-200 CPU周期 | | 灵活性 | 低(依赖硬件设计) | 高(可定制上下文) | | 应用场景 | 实时系统、虚拟化 | 通用操作系统 |

八、扩展知识

  • 任务嵌套:通过TSS.prev_tss实现任务链表
  • TSS加密:现代CPU支持TSS段的内存加密(如AMD SVM)
  • 调试支持:使用TR寄存器配合调试寄存器(DR0-DR7)

建议在现代系统开发中优先使用软件任务切换,硬件任务切换仅适用于特定场景(如实时嵌入式系统)。如需进一步优化,可结合Intel VT-x或AMD SVM技术实现虚拟化任务管理。

x86的各种描述符

以下是使用Markdown高亮技术术语的版本(关键术语加粗,指令和寄存器用反引号包裹):

描述符类型 作用 加载方式 加载指令 使用示例 行为说明
段描述符 定义内存段属性(如基地址、界限、访问权限),用于程序访问内存段。细分包括:
- 代码段描述符
- 数据段描述符
- 栈段描述符
通过16位段选择子加载到段寄存器(如CSDSES)。段选择子包含:
- GDT/LDT索引
- 请求特权级(RPL)
- 描述符表指示位(TI)
无特定指令(通过mov赋值段选择子)
asm<br>mov ax, 0x08 ; 代码段选择子<br>mov cs, ax ; 加载到CS寄存器<br>
CPU根据段选择子从GDT/LDT获取段描述符,后续内存访问基于描述符的基地址和界限计算物理地址。
任务状态段描述符 指向任务状态段(TSS),用于任务切换时保存/恢复上下文(寄存器、栈指针等)。 使用ltr指令加载TSS描述符的选择子到任务寄存器(TR ltr
asm<br>mov ax, 0x10 ; TSS描述符选择子<br>ltr ax ; 加载到TR寄存器<br>
CPU根据选择子从GDT获取TSS描述符,将TSS基地址和界限存入TR,任务切换时通过TR找到目标TSS。
调用门描述符 实现跨特权级子程序调用(如用户态→内核态),包含目标代码段选择子、偏移量和权限检查逻辑。 通过calljmp指令配合调用门选择子触发,CPU自动查找GDT/LDT中的调用门描述符。 calljmp
asm<br>call 0x18:0x1234 ; 调用门选择子0x18,偏移量0x1234<br>
CPU验证调用权限(CPL ≤ DPL)后,跳转到目标代码段执行,自动压栈返回地址。
中断门描述符 处理硬件中断(如IRQ)和软件中断(如int 3),包含中断服务程序(ISR)的入口地址和权限。 通过初始化中断描述符表(IDT)加载,中断发生时CPU根据向量号查找IDT中的中断门。 无特定指令(IDT初始化)
c<br>// C语言示例:设置IDT表项<br>set_idt_entry(0x20, &irq_handler);<br>
中断发生时,CPU自动压栈EFLAGSCSEIP,并跳转到ISR,自动清除IF标志。
陷阱门描述符 与中断门类似,但执行时不清除中断标志(IF,允许嵌套中断。 与中断门相同,通过IDT加载。 无特定指令(IDT初始化)
asm<br>int 0x80 ; 触发陷阱门(系统调用)<br>
CPU压栈上下文后跳转到陷阱处理程序,IF标志保持不变,允许响应其他中断。
任务门描述符 实现硬件任务切换,指向目标任务的TSS描述符。 通过calljmp指令配合任务门选择子触发,CPU自动查找GDT中的任务门描述符。 calljmp
asm<br>jmp 0x20:0x0000 ; 任务门选择子0x20,无实际偏移量<br>
CPU保存当前任务上下文到原TSS,加载目标任务的TSS,切换页目录(CR3)并跳转执行。

高亮规则说明

  1. 描述符类型:加粗(如段描述符
  2. 寄存器名称:反引号包裹(如CSTR
  3. 指令名称:反引号包裹(如ltrcall
  4. 关键概念:加粗(如保护模式中断向量号
  5. 特殊字段:反引号包裹(如IFRPL

如需调整高亮方式或补充其他术语,请随时告知!

进程切换

  1. 进程切换只发生在内核态
  2. 用户态程序进入内核态时,将寄存器保存在内核栈上
  3. 第一次从内核进入用户态时,需要构造相关寄存器
  4. kernel thread永远不会进入用户态