跳转至

异常和中断

1 异常的入栈出栈流程

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

1.1 进入中断(异常发生时)的入栈顺序(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

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

步骤 操作内容 操作主体 操作前 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 无

1.3 解释

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

2 所有异常列表

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

异常编号 异常名称 是否有错误码 进入中断入栈顺序(CPU 硬件行为) 退出中断出栈顺序(程序代码手动处理 + CPU 硬件 iret 指令) 简要说明
0 除法错误(Divide Error) 否 1. 压入 EFLAGS
2. 压入 CS
3. 压入 EIP
1. iret 弹出 EIP
2. iret 弹出 CS
3. iret 弹出 EFLAGS
执行除法指令(DIV 或 IDIV)时,除数为 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 架构中常见异常的关键信息,包括异常编号、名称、是否有错误码以及进入和退出中断时的栈操作顺序,同时对每个异常的触发条件做了简要说明。

3 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指令

4 特权场景

抱歉,此前表格存在逻辑错误,现重新梳理修正(基于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 弹栈逻辑:按 EIP → CS → EFLAGS 顺序弹出(特权级切换场景),表格按此还原。
2. 每步弹栈操作,优先弹出栈顶数据(即最后入栈的 EIP 最先被弹出)。

5 无特权切换场景

(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)

关键逻辑说明: - 入栈顺序:按 EFLAGS → CS → EIP 压栈,栈顶地址逐步递减(20→16→12→8)。
- 弹栈顺序:iret 按 EIP(栈顶最先压入)→ CS → EFLAGS 弹出,栈顶地址逐步回增(8→12→16→20),最终恢复初始 ESP 值。

iret, iretd, iretq区别

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

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 位系统中,iretd 和 iret 的功能是一样的,只是 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 恢复执行环境

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

call和ret指令

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

5.1 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]

5.2 关键区别说明

  1. 调用范围
  2. 近调用(call):仅修改指令指针(IP/EIP/RIP),用于同一代码段内的跳转。
  3. 远调用(lcall/call far ptr/call qword ptr):修改段寄存器(CS)和指令指针,用于跨段调用(需特权级检查)。

  4. 参数传递

  5. 近调用:操作数为目标地址的相对偏移量(如call subproc)或绝对地址(如call 0x1234)。
  6. 远调用:操作数为段选择子和偏移量的组合(如0x08:0x1234)。

  7. 架构差异

  8. 16位:使用lcall和retf,操作数为16位段选择子+16位偏移量。
  9. 32位:call支持32位偏移量,lcall支持32位偏移量+16位段选择子。
  10. 64位:call默认使用RIP相对寻址,call qword ptr用于远调用(需显式指定段选择子)。

  11. 现代处理器优化

  12. 64位模式下,远调用较少使用,通常通过syscall指令进入内核态。
  13. 编译器会优先使用近调用(call)以提高效率。

5.3 示例对比

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

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

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

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

TSS 任务状态段

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

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

6 一、TSS核心概念

任务状态段(Task State Segment, TSS) 是x86架构用于存储任务上下文的核心数据结构,包含: - 寄存器组:通用寄存器(EAX/EBX/ECX等)、段寄存器(CS/DS/ES等) - 控制信息:指令指针(EIP)、栈指针(SS/ESP)、特权级(CPL/IOPL) - 硬件辅助字段:调试陷阱标志、链接指针、I/O许可位图

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

7 二、TSS架构演进

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

8 三、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);
}

9 四、核心应用场景解析

9.1 场景1:中断处理栈切换

9.1.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

9.1.2 指令示例

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

9.2 场景2:系统调用(syscall)

9.2.1 软硬件流程

阶段 硬件行为 软件行为 关键指令/寄存器
触发 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

9.2.2 指令示例

; 用户态
mov eax, __NR_read
syscall

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

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

9.3.1 软硬件流程

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

9.3.2 指令示例

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

10 五、关键技术对比

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

11 六、总结

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

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

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

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

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

12.1.1 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位
    };
    

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

12.2.1 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位

12.2.2 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,
    // 其他寄存器初始化...
};

12.2.3 3. 任务切换指令

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

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

12.3 三、硬件任务切换流程

12.3.1 1. 触发任务切换

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

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

12.3.2 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

12.4 四、特权级切换场景

12.4.1 1. 同一特权级切换

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

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

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

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

12.5 五、注意事项

  1. TSS描述符设置
  2. 必须设置B标志(0x80)表示32位TSS
  3. 段界限需设置为0x67(103字节)

  4. 任务门限制

  5. 任务门必须位于IDT中
  6. 目标TSS必须位于GDT中

  7. 现代架构限制

  8. 长模式下TSS仅支持中断栈表(IST)
  9. 硬件任务切换需通过VMX虚拟化技术实现

12.6 六、代码示例

12.6.1 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

12.6.2 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"
    );
}

12.7 七、对比总结

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

12.8 八、扩展知识

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

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

x86的各种描述符

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

描述符类型 作用 加载方式 加载指令 使用示例 行为说明
段描述符 定义内存段属性(如基地址、界限、访问权限),用于程序访问内存段。细分包括:
- 代码段描述符
- 数据段描述符
- 栈段描述符
通过16位段选择子加载到段寄存器(如CS、DS、ES)。段选择子包含:
- 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。
调用门描述符 实现跨特权级子程序调用(如用户态→内核态),包含目标代码段选择子、偏移量和权限检查逻辑。 通过call或jmp指令配合调用门选择子触发,CPU自动查找GDT/LDT中的调用门描述符。 call、jmp
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自动压栈EFLAGS、CS、EIP,并跳转到ISR,自动清除IF标志。
陷阱门描述符 与中断门类似,但执行时不清除中断标志(IF),允许嵌套中断。 与中断门相同,通过IDT加载。 无特定指令(IDT初始化)
asm<br>int 0x80 ; 触发陷阱门(系统调用)<br>
CPU压栈上下文后跳转到陷阱处理程序,IF标志保持不变,允许响应其他中断。
任务门描述符 实现硬件任务切换,指向目标任务的TSS描述符。 通过call或jmp指令配合任务门选择子触发,CPU自动查找GDT中的任务门描述符。 call、jmp
asm<br>jmp 0x20:0x0000 ; 任务门选择子0x20,无实际偏移量<br>
CPU保存当前任务上下文到原TSS,加载目标任务的TSS,切换页目录(CR3)并跳转执行。

12.9 高亮规则说明

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

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

进程切换

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