中断和异常是表示系统、处理器或当前正在执行的程序或任务中存在需要处理器注意的情况的事件。它们通常导致将执行从当前运行的程序或任务强制转移到一个称为中断处理程序或异常处理程序的特殊软件例程或任务。处理器响应中断或异常而采取的动作称为服务或处理中断或异常。
在程序执行过程中,响应来自硬件的信号,中断随机发生。系统硬件使用中断来处理处理器外部的事件,例如服务外围设备的请求。软件还可以通过执行INT n指令来生成中断。
当处理器在执行指令时检测到错误情况(例如被零除)时,会发生异常。处理器检测各种错误情况,包括保护违规、页面错误和内部机器故障。Pentium 4, Intel Xeon, P6 family, and Pentium processors 的机器检查体系结构还允许在检测到内部硬件错误和总线错误时生成机器检查异常。
当接收到中断或检测到异常时,当处理器执行中断或异常处理程序时,当前运行的过程或任务将被挂起。当处理程序的执行完成时,处理器将继续执行中断的过程或任务。中断的过程或任务的恢复不会失去程序的连续性,除非无法从异常中恢复或中断导致当前运行的程序终止。
本章描述了在保护模式下运行时处理器的中断和异常处理机制。本章末尾给出了异常和导致异常产生的条件的描述。
为了帮助处理异常和中断,每个架构定义的异常和每个需要处理器进行特殊处理的中断条件都被分配了一个唯一的标识号,称为矢量号。处理器使用分配给异常或中断的矢量号作为中断描述符表(IDT)的索引。该表为异常或中断处理程序提供入口点(见第6.10节“中断描述符表(IDT)”)。
矢量号的允许范围是0到255。范围为0到31的矢量号由英特尔64和IA-32体系结构保留,用于体系结构定义的异常和中断。
并非此范围内的所有矢量号都具有当前定义的函数。此范围内的未分配矢量号是保留的。不要使用保留的矢量号。
范围在32到255之间的矢量号被指定为用户定义的中断,英特尔64和IA-32体系结构不保留。这些中断通常分配给外部I/O设备,以使这些设备能够通过一种外部硬件中断机制向处理器发送中断(见第6.3节“中断源”)。
表6-1显示了架构定义的异常和NMI中断的矢量号分配。此表给出了异常类型(参见第6.5节“异常分类”),并指示是否在堆栈中为异常保存了错误代码。还给出了每个预定义异常和NMI中断的来源。
处理器从两个源接收中断:
- 外部(硬件生成)中断。
- 软件生成的中断。
外部中断通过处理器上的引脚或通过本地 APIC 接收。 Pentium 4、Intel Xeon、P6 系列和 Pentium 处理器上的主要中断引脚是 LINT[1:0] 引脚,它们连接到本地 APIC(参见第 10 章,“高级可编程中断控制器 (APIC)”)。 当本地 APIC 使能时,可以通过 APIC 的本地向量表 (LVT) 对 LINT[1:0] 引脚进行编程,以与处理器的任何异常或中断向量相关联。
当本地 APIC 被全局/硬件禁用时,这些引脚分别配置为 INTR 和 NMI 引脚。 置位 INTR 引脚会向处理器发出外部中断已发生的信号。 处理器从系统总线读取外部中断控制器提供的中断向量编号,例如 8259A(参见第 6.2 节“异常和中断向量”)。 置位 NMI 引脚会发出一个不可屏蔽中断 (NMI) 信号,该中断分配给中断向量 2。
- Intel386处理器之后的处理器不会产生这个异常。
- Intel486处理器引入了这个异常。
- 此异常在奔腾处理器中引入,并在 P6 系列处理器中得到增强。
- 此异常是在 Pentium III 处理器中引入的。
- 此异常仅在支持“EPT-violation #VE”VM 执行控制的 1-setting 的处理器上发生。
处理器的本地 APIC 通常连接到基于系统的 I/O APIC。在这里,在 I/O APIC 的引脚处接收到的外部中断可以通过系统总线(Pentium 4、Intel Core Duo、Intel Core 2、Intel Atom 和 Intel Xeon 处理器)或 APIC 串行总线(P6系列和奔腾处理器)。 I/O APIC 确定中断的向量编号并将该编号发送到本地 APIC。当一个系统包含多个处理器时,处理器也可以通过系统总线(Pentium 4、Intel Core Duo、Intel Core 2、Intel Atom 和 Intel Xeon 处理器)或 APIC 串行总线(P6 系列和奔腾处理器)。
LINT[1:0] 管脚在 Intel486 处理器和不包含片上本地 APIC 的早期 Pentium 处理器上不可用。这些处理器具有专用的 NMI 和 INTR 引脚。
对于这些处理器,外部中断通常由基于系统的中断控制器 (8259A) 生成,中断通过 INTR 引脚发出信号。
请注意,处理器上的其他几个引脚可能会导致发生处理器中断。但是,本章描述的中断和异常机制并不处理这些中断。这些引脚包括 RESET#、FLUSH#、STPCLK#、SMI#、R/S# 和 INIT# 引脚。它们是否包含在特定处理器上取决于实现。引脚功能在各个处理器的数据手册中进行了描述。 SMI# 引脚在第 31 章“系统管理模式”中进行了描述。
通过 INTR 引脚或通过本地 APIC 传递给处理器的任何外部中断称为可屏蔽硬件中断。 可以通过 INTR 引脚传递的可屏蔽硬件中断包括所有 IA-32 架构定义的从 0 到 255 的中断向量; 可以通过本地 APIC 传递的那些包括中断向量 16 到 255。
EFLAGS 寄存器中的 IF 标志允许将所有可屏蔽的硬件中断作为一个组屏蔽(参见第 6.8.1 节,“屏蔽可屏蔽的硬件中断”)。 请注意,当通过本地 APIC 传递中断 0 到 15 时,APIC 指示接收到非法向量。
INT n 指令允许通过提供中断向量号作为操作数从软件内部生成中断。 例如,INT 35 指令强制对中断 35 的中断处理程序进行隐式调用。从 0 到 255 的任何中断向量都可以用作该指令中的参数。
但是,如果使用处理器的预定义 NMI 向量,处理器的响应将与正常方式产生的 NMI 中断不同。 如果在该指令中使用了 2 号向量(NMI 向量),则会调用 NMI 中断处理程序,但不会激活处理器的 NMI 处理硬件。 使用 INT n 指令在软件中产生的中断不能被 EFLAGS 寄存器中的 IF 标志屏蔽。
使用 INT n 指令在软件中产生的中断不能被 EFLAGS 寄存器中的 IF 标志屏蔽。
处理器从三个来源接收异常:
• 处理器检测到的程序错误异常。
• 软件生成的异常。
• 机器检查异常。
当处理器在应用程序或操作系统或执行程序的执行过程中检测到程序错误时,会产生一个或多个异常。 Intel 64 和 IA-32 架构为每个处理器可检测的异常定义了一个向量编号。 异常分为故障、陷阱和中止(请参阅第 6.5 节“异常分类”)。
INTO、INT1、INT3 和 BOUND 指令允许在软件中生成异常。 这些指令允许在指令流中的点执行异常条件检查。 例如,INT3 导致生成断点异常。
INT n 指令可用于在软件中模拟异常; 但有一个限制。1 如果 INT n 为架构定义的异常之一提供了一个向量,处理器会为正确的向量生成一个中断(以访问异常处理程序),但不会将错误代码压入堆栈。 即使相关的硬件生成的异常通常会产生错误代码也是如此。 异常处理程序在处理异常时仍会尝试从堆栈中弹出错误代码。 因为没有推送错误代码,处理程序将弹出并丢弃 EIP(代替丢失的错误代码)。 这会将退货发送到错误的位置。
P6 系列和 Pentium 处理器提供内部和外部机器检查机制,用于检查内部芯片硬件和总线事务的操作。 这些机制依赖于实现。 当检测到机器检查错误时,处理器会发出机器检查异常信号(向量 18)并返回错误代码。
有关机器检查机制的更多信息,请参阅第 6 章“中断 18 - 机器检查异常 (#MC)”和第 15 章“机器检查架构”。
异常根据报告的方式以及是否可以重新启动导致异常的指令而不会丢失程序或任务连续性,将异常分为故障、陷阱或中止。
• 故障——故障是一种通常可以纠正的异常,一旦纠正,就可以重新启动程序而不会失去连续性。当报告故障时,处理器将机器状态恢复到故障指令开始执行之前的状态。错误处理程序的返回地址(保存的 CS 和 EIP 寄存器的内容)指向错误指令,而不是指向错误指令之后的指令。
• 陷阱——陷阱是在执行陷阱指令后立即报告的异常。陷阱允许继续执行程序或任务,而不会失去程序连续性。陷阱处理程序的返回地址指向陷阱指令之后要执行的指令。
• Aborts — 中止是一种异常,它并不总是报告导致异常的指令的精确位置,并且不允许重新启动导致异常的程序或任务。中止用于报告严重错误,例如硬件错误和系统表中的不一致或非法值。
通常报告为故障的一个异常子集无法重新启动。这种异常会导致某些处理器状态的丢失。例如,在堆栈帧跨越堆栈段末尾的情况下执行POPAD指令会导致报告错误。在这种情况下,异常处理程序会看到指令指针(CS:EIP)已恢复,就像POPAD指令尚未执行一样。但是,内部处理器状态(通用寄存器)将被修改。这种情况被认为是编程错误。导致此类异常的应用程序应由操作系统终止。
为了允许在处理异常或中断后重新启动程序或任务,所有异常(中止除外)都保证在指令边界上报告异常。所有中断都保证在指令边界上进行。
对于故障类异常,返回指令指针(在处理器生成异常时保存)指向故障指令。因此,当在处理故障后重新启动程序或任务时,故障指令将重新启动(重新执行)。重新启动故障指令通常用于处理在访问操作数被阻止时生成的异常。此类错误最常见的示例是页面错误异常 (#PF),当程序或任务引用位于不在内存中的页面上的操作数时,就会发生该异常。当发生页面错误异常时,异常处理程序可以将页面加载到内存中并通过重新启动错误指令来恢复程序或任务的执行。为了确保对当前执行的程序或任务透明地处理重新启动,处理器保存必要的寄存器和堆栈指针以允许重新启动到执行故障指令之前的状态。
对于陷阱类异常,返回指令指针指向陷阱指令之后的指令。
如果在转移执行的指令期间检测到陷阱,则返回指令指针反映转移。例如,如果在执行 JMP 指令时检测到陷阱,则返回指令指针指向 JMP 指令的目的地,而不是 JMP 指令之后的下一个地址。所有陷阱异常都允许程序或任务重新启动而不会失去连续性。例如溢出异常就是陷阱异常。这里,返回指令指针指向测试 EFLAGS.OF(溢出)标志的 INTO 指令之后的指令。此异常的陷阱处理程序解决溢出条件。从陷阱处理程序返回后,程序或任务将继续执行 INTO 指令之后的指令。
中止类异常不支持可靠地重新启动程序或任务。中止处理程序旨在收集有关中止异常发生时处理器状态的诊断信息,然后尽可能优雅地关闭应用程序和系统。
中断严格支持重新启动中断的程序和任务,而不会失去连续性。为中断保存的返回指令指针指向要在处理器接受中断的指令边界处执行的下一条指令。
如果刚刚执行的指令有重复前缀,则中断在当前迭代结束时进行,寄存器设置为执行下一次迭代。
P6 系列处理器推测执行指令的能力不会影响处理器对中断的处理。中断发生在指令执行的退出阶段的指令边界处;所以它们总是被按“顺序”指令流处理。
有关 P6 系列处理器的微架构及其对 out-of -order 指令执行。
请注意,奔腾处理器和早期的 IA-32 处理器也执行不同数量的预取和初步解码。对于这些处理器,异常和中断也不会在实际“按顺序”执行指令之前发出信号。对于给定的代码示例,当代码在任何 IA-32 处理器系列上执行时,异常信号统一发生(除非已定义新异常或新操作码)。
故障:返回时恢复到陷入时的指令。
陷阱:返回时恢复到陷入时的下一条指令。
Aborts:不会返回。
不可屏蔽中断 (NMI) 可以通过以下两种方式之一生成:
• 外部硬件置位 NMI 引脚。
• 处理器通过传递模式 NMI 在系统总线(Pentium 4、Intel Core Duo、Intel Core 2、Intel Atom 和 Intel Xeon 处理器)或 APIC 串行总线(P6 系列和 Pentium 处理器)上接收消息。
当处理器从这些源中的任何一个接收到 NMI 时,处理器会立即通过调用中断向量编号 2 指向的 NMI 处理程序来处理它。处理器还调用某些硬件条件以确保不会收到其他中断,包括 NMI 中断直到 NMI 处理程序完成执行(请参阅第 6.7.1 节,“处理多个 NMI”)。
此外,当从上述任一来源接收到 NMI 时,它不能被 EFLAGS 寄存器中的 IF 标志屏蔽。
可以向向量 2 发出可屏蔽的硬件中断(通过 INTR 引脚)以调用 NMI 中断处理程序;然而,这个中断并不是真正的 NMI 中断。激活处理器的 NMI 处理硬件的真正 NMI 中断只能通过上面列出的机制之一传递。
在执行 NMI 中断处理程序时,处理器会阻止后续 NMI 的传递,直到下一次执行 IRET 指令。 NMI 的这种阻塞阻止了 NMI 处理程序的嵌套执行。 建议通过中断门访问 NMI 中断处理程序以禁用可屏蔽硬件中断(请参阅第 6.8.1 节,“屏蔽可屏蔽硬件中断”)。
即使指令导致错误,IRET 指令的执行也会解除 NMI 阻塞。 例如,如果在 EFLAGS.VM = 1 且 IOPL 小于 3 的情况下执行 IRET 指令,则会生成一般保护异常(参见第 20.2.7 节,“敏感指令”)。 在这种情况下,在调用异常处理程序之前,NMI 会被取消屏蔽。
处理器会根据处理器的状态以及 EFLAGS 寄存器中的 IF 和 RF 标志位来禁止某些中断的产生,如下节所述。
IF 标志可以禁用对处理器 INTR 引脚或通过本地 APIC 接收到的可屏蔽硬件中断的服务(参见第 6.3.2 节,“可屏蔽硬件中断”)。当 IF 标志清零时,处理器禁止传送到 INTR 引脚或通过本地 APIC 的中断产生内部中断请求;当 IF 标志置位时,传送到 INTR 或通过本地 APIC 引脚的中断将作为正常的外部中断处理。
IF 标志不影响传递到 NMI 引脚的不可屏蔽中断 (NMI) 或通过本地 APIC 传递的传递模式 NMI 消息,也不影响处理器生成的异常。与 EFLAGS 寄存器中的其他标志一样,处理器会清除 IF 标志以响应硬件复位。
可屏蔽硬件中断组包括保留的中断和异常向量 0 到 32 的事实可能会导致混淆。在架构上,当设置 IF 标志时,可以通过 INTR 引脚将针对从 0 到 32 的任何向量的中断传递给处理器,并且可以通过本地 APIC 传递从 16 到 32 的任何向量。然后处理器将产生一个中断并调用向量号指向的中断或异常处理程序。因此,例如,可以通过 INTR 引脚(通过向量 14)调用页面错误处理程序;
但是,这不是真正的页面错误异常。这是一个中断。与 INT n 指令一样(请参见第 6.4.2 节,“软件生成的异常”),当通过 INTR 引脚向异常向量生成中断时,处理器不会将错误代码压入堆栈,因此异常处理程序可能无法正确运行。
IF 标志可以分别用 STI(设置中断使能标志)和 CLI(清除中断使能标志)指令设置或清除。这些指令只有在 CPL 等于或小于 IOPL 时才能执行。如果在 CPL 大于 IOPL 时执行它们,则会生成通用保护异常 (#GP)。2 如果 IF = 0,则在执行 STI 后,可屏蔽硬件中断在指令边界上保持禁止。3 禁止结束后传递另一个事件(例如异常)或执行下一条指令。
IF 标志还受以下操作的影响:
• PUSHF 指令将所有标志存储在堆栈中,以便对其进行检查和修改。 POPF 指令可用于将修改后的标志加载回 EFLAGS 寄存器。
• 任务开关和POPF 和IRET 指令加载EFLAGS 寄存器;因此,它们可用于修改 IF 标志的设置。
• 当通过中断门处理中断时,IF 标志会自动清除,从而禁用可屏蔽硬件中断。 (如果通过陷阱门处理中断,则不清除 IF 标志。)
请参阅英特尔® 64 位和 IA-32 架构软件开发人员手册第 2A 卷第 3 章“指令集参考,A-L”和第 4 章“指令集参考,M-U”,在英特尔® 64 位和 IA-32 架构软件开发人员手册第 2B 卷中,详细描述了这些指令允许对 IF 标志执行的操作。
EFLAGS 寄存器中的 RF(恢复)标志控制处理器对指令断点条件的响应(参见第 2.3 节“EFLAGS 寄存器中的系统标志和字段”中对 RF 标志的描述)。
设置时,它会防止指令断点生成调试异常 (#DB); 清除后,指令断点将产生调试异常。 RF 标志的主要功能是防止处理器在指令断点处进入调试异常循环。 有关使用此标志的更多信息,请参见第 17.3.1.1 节,“指令断点异常条件”。
如第 6.8.3 节所述,执行加载 SS 寄存器的 MOV 或 POP 指令会抑制下一条指令上的任何指令断点(就像 EFLAGS.RF 为 1 一样)。
为了切换到不同的堆栈段,软件通常使用一对指令,例如:
MOV SS,AX
MOV ESP,StackTop
(软件也可能使用 POP 指令来加载 SS 和 ESP。)
如果在加载新的 SS 段描述符之后但在加载 ESP 寄存器之前发生中断或异常,则在中断或异常处理程序的持续时间内,这两个部分进入堆栈空间的逻辑地址是不一致的(假设传递中断或异常本身不会加载新的堆栈指针)。
为了解决这种情况,处理器会阻止在执行 MOV 到 SS 指令或 POP 到 SS 指令之后传递某些事件。以下项目提供了详细信息:
• 下一条指令上的任何指令断点都被抑制(就像 EFLAGS.RF 为 1)。
• MOV 到 SS 指令或 POP 到 SS 指令上的任何数据断点都被禁止,直到下一条指令之后的指令边界。
• 将在 MOV 到 SS 指令或 POP 到 SS 指令之后传送的任何单步陷阱(因为 EFLAGS.TF 为 1)被抑制。
• 抑制和禁止在发出异常或执行下一条指令后结束。
• 如果一系列连续指令均加载SS 寄存器(使用MOV 或POP),则只有第一个指令才能保证以这种方式抑制或抑制事件。
Intel 建议软件使用 LSS 指令将 SS 寄存器和 ESP 一起加载。前面发现的问题不适用于 LSS,并且 LSS 指令不会抑制上面详述的事件。
中断描述符表 (IDT) 将每个异常或中断向量与用于服务相关异常或中断的过程或任务的门描述符相关联。与 GDT 和 LDT 一样,IDT 是一个 8 字节描述符数组(在保护模式下)。
与 GDT 不同,IDT 的第一个条目可能包含一个描述符。为了形成 IDT 的索引,处理器将异常或中断向量缩放八倍(门描述符中的字节数)。
因为只有 256 个中断或异常向量,IDT 不需要包含超过 256 个描述符。它可以包含少于 256 个描述符,因为只有可能发生的中断和异常向量才需要描述符。 IDT 中的所有空描述符插槽都应将描述符的当前标志设置为 0。
IDT 的基地址应在 8 字节边界上对齐,以最大限度地提高高速缓存行填充的性能。限制值以字节表示,并与基地址相加,得到最后一个有效字节的地址。
限制值为 0 会导致恰好 1 个有效字节。由于 IDT 条目的长度始终为 8 个字节,因此限制应始终小于 8 的整数倍(即 8N – 1)。
IDT 可以驻留在线性地址空间中的任何位置。如图 6-1 所示,处理器使用 IDTR 寄存器定位 IDT。该寄存器保存 IDT 的 32 位基地址和 16 位限制。
LIDT(加载 IDT 寄存器)和 SIDT(存储 IDT 寄存器)指令分别加载和存储 IDTR 寄存器的内容。 LIDT 指令将内存操作数中保存的基地址和限制加载到 IDTR 寄存器中。该指令只有在CPL为0时才能执行,一般用于创建IDT时操作系统的初始化代码。操作系统也可以使用它从一个 IDT 更改为另一个。 SIDT 指令将存储在 IDTR 中的基值和限值复制到内存中。该指令可以在任何特权级别执行。
如果向量引用超出 IDT 限制的描述符,则会生成一般保护异常 (#GP)。
IDT可以包含三种门描述符中的任何一种:
•任务门描述符
•中断门描述符
•陷阱门描述符
图6-2显示了任务门、中断门和陷阱门描述符的格式。IDT中使用的任务门格式与GDT或LDT中使用任务门的格式相同(见第7.2.5节“任务门描述符”)。任务门包含用于异常和/或中断处理程序任务的TSS的段选择器。
中断门和陷阱门与调用门非常相似(参见第5.8.3节“调用门”)。它们包含一个远指针(段选择器和偏移量),处理器使用它将程序执行转移到异常或中断处理程序代码段中的处理程序过程。这些门在处理器处理EFLAGS寄存器中IF标志的方式上有所不同(参见第6.12.1.3节“异常或中断处理程序的标志使用”)。
处理器处理对异常和中断处理程序的调用,类似于它处理对过程或任务的CALL指令调用的方式。当响应异常或中断时,处理器使用异常或中断向量作为IDT中描述符的索引。如果索引指向中断门或陷阱门,则处理器以类似于调用门的方式调用异常或中断处理程序(参见第5.8.2节“门描述符”到第5.8.6节“从被调用过程返回”)。如果索引指向任务门,处理器以类似于调用任务门的方式执行任务切换到异常或中断处理程序任务(参见第7.3节“任务切换”)。
中断门或陷阱门引用在当前执行任务的上下文中运行的异常或中断处理程序(见图6-3)。门的段选择器指向GDT或当前LDT中可执行代码段的段描述符。门描述符的偏移字段指向异常或中断处理过程的开始。
当处理器执行对异常或中断处理程序的调用时:
- 如果处理程序过程将以数值上较低的特权级别执行,则会发生堆栈切换。发生堆栈切换时:
-
- 处理程序要使用的堆栈的段选择器和堆栈指针是从当前执行任务的TSS获得的。在这个新堆栈上,处理器按下被中断过程的堆栈段选择器和堆栈指针。
- 然后,处理器将EFLAGS、CS和EIP寄存器的当前状态保存在新堆栈上(见图6-4)。
- 如果异常导致错误代码被保存,则在EIP值之后将其推送到新堆栈上。
- 如果处理程序程序将以与中断程序相同的特权级别执行:
-
- 处理器将EFLAGS、CS和EIP寄存器的当前状态保存在当前堆栈上(见图6-4)。
- 如果异常导致错误代码被保存,则在EIP值之后将其推送到当前堆栈上。
要从异常或中断处理程序过程返回,处理程序必须使用IRET(或IRETD)指令。
IRET指令与RET指令相似,只是它将保存的标志恢复到EFLAGS寄存器中。仅当CPL为0时,EFLAGS寄存器的IOPL字段才被恢复。只有当CPL小于或等于IOPL时,if标志才被更改。有关IRET指令执行的完整操作的说明,请参阅“英特尔64与IA-32体系结构软件开发人员手册”第2A卷第3章“指令集参考,A-L”。
如果在调用处理程序过程时发生堆栈切换,IRET指令将在返回时切换回中断过程的堆栈。
异常和中断处理程序的特权级别保护类似于通过调用门调用时用于普通程序调用的特权级别(参见第5.8.4节“通过调用门访问代码段”)。处理器不允许将执行转移到特权低于CPL的代码段(数值上特权级别更高)中的异常或中断处理程序。
试图违反此规则会导致一般保护异常(#GP)。异常和中断处理程序的保护机制在以下方面有所不同:
•由于中断和异常向量没有RPL,因此对异常和中断处理程序的隐式调用不检查RPL。
•仅当INT n、INT3或INTO指令生成异常或中断时,处理器才检查中断或陷阱门的DPL。4此处,CPL必须小于或等于门的DPR。此限制防止以特权级别3运行的应用程序或过程使用软件中断访问关键异常处理程序,例如页面错误处理程序,前提是这些处理程序被放置在特权级别更高的代码段中(数字上的特权级别更低)。对于硬件生成的中断和处理器检测到的异常,处理器忽略中断和陷阱门的DPL。
由于异常和中断通常不会在可预测的时间发生,这些特权规则有效地限制了异常和中断处理过程可以运行的特权级别。
可以使用以下任一技术来避免特权级别冲突。
•异常或中断处理程序可以放置在一致的代码段中。此技术可用于只需要访问堆栈上可用数据的处理程序(例如,划分错误异常)。如果处理程序需要来自数据段的数据,则需要从特权级别3访问数据段,这将使其不受保护。
•处理程序可以放置在特权级别为0的不一致代码段中。无论中断的程序或任务运行的CPL如何,该处理程序都将始终运行。
当通过中断门或陷阱门访问异常或中断处理程序时,处理器在将EFLAGS寄存器的内容保存到堆栈上后,清除EFLAGS寄存器中的TF标志。(在调用异常和中断处理程序时,处理器还将在EFLAGS寄存器中的VM、RF和NT标志保存在堆栈上后清除它们。)清除TF标志可防止指令跟踪影响中断响应,并确保在传递到处理程序后不会传递任何单步异常。随后的IRET指令将TF(以及VM、RF和NT)标志恢复为堆栈上EFLAGS寄存器的保存内容中的值。
中断门和陷阱门之间的唯一区别是处理器处理EFLAGS寄存器中IF标志的方式。当通过中断门访问异常或中断处理程序时,处理器清除IF标志以防止其他中断干扰当前中断处理程序。随后的IRET指令将IF标志恢复为堆栈上EFLAGS寄存器的保存内容中的值。
通过陷阱门访问处理程序过程不会影响IF标志。
当通过IDT中的任务门访问异常或中断处理程序时,会产生任务切换。用单独的任务处理异常或中断有几个优点:
- 中断程序或任务的整个上下文将自动保存。
- 新的TSS允许处理程序在处理异常或中断时使用新的特权级别0堆栈。如果当前特权级别0堆栈损坏时发生异常或中断,通过任务门访问处理程序可以通过为处理程序提供新的特权级别0栈来防止系统崩溃。
- 通过给处理器一个单独的地址空间,处理器可以进一步与其他任务隔离。这是通过给它一个单独的LDT来完成的。
用单独的任务处理中断的缺点是,任务开关上必须保存的机器状态量使其比使用中断门慢,从而导致中断延迟增加。
IDT中的任务门引用GDT中的TSS描述符(见图6-6)。处理程序任务的切换与普通任务切换的处理方式相同(参见第7.3节“任务切换”)。返回到中断任务的链接存储在处理程序任务的TSS的前一个任务链接字段中。如果异常导致生成错误代码,则将此错误代码复制到新任务的堆栈中。
当在操作系统中使用异常或中断处理程序任务时,实际上有两种机制可用于分派任务:软件调度程序(操作系统的一部分)和硬件调度程序(处理器中断机制的一部分。软件调度器需要适应在启用中断时可能分派的中断任务。
由于IA-32体系结构任务不可重入,中断处理程序任务必须在完成处理中断和执行IRET指令之间禁用中断。此操作可防止在中断任务的TSS仍标记为忙碌时发生另一个中断,这将导致一般保护(#GP)异常。
当异常条件与特定的段选择器或IDT向量相关时,处理器将错误代码推送到异常处理程序的堆栈上(无论是过程还是任务)。错误代码的格式如图6-7所示。错误代码类似于段选择器;但是,错误代码包含3个标志,而不是TI标志和RPL字段:
EXT: 外部事件(位0)-设置时,表示在程序外部事件(如中断或早期异常)的传递过程中发生了异常。5如果在交付软件中断(INT n、INT3或INTO)期间发生异常,则清除该位。
IDT: 描述符位置(位1)-设置时,表示错误代码的索引部分引用IDT中的门描述符;如果清除,则指示索引引用GDT或当前LDT中的描述符。
TI: GDT/LDT(位2)-仅在IDT标志清除时使用。当设置时,TI标志指示错误代码的索引部分引用LDT中的段或门描述符;清除时,表示索引引用当前GDT中的描述符。
段选择器索引字段为错误代码所引用的段或门选择器提供IDT、GDT或当前LDT的索引。在某些情况下,错误代码为空(除可能的EXT外,所有位均为空)。空错误代码表示错误不是由对特定段的引用引起的,或者在操作中引用了空段选择器。
页面错误异常(#PF)的错误代码格式不同。请参阅本章中的“中断14页故障超出(#PF)”部分。
控制保护异常(#CP)的错误代码格式不同。请参阅本章中的“中断21控制保护异常(#CP)”部分。
错误代码作为双字或双字(取决于默认的中断、陷阱或任务门大小)推送到堆栈上。为了保持双字推送的堆栈对齐,保留了错误代码的上半部分。请注意,当执行IRET指令以从异常处理程序返回时,错误代码不会弹出,因此处理程序必须在执行返回之前删除错误代码。
对于外部(使用INTR或LINT[1:0]引脚)或INT n指令生成的异常,即使通常为这些异常生成错误代码,也不会将错误代码推送到堆栈上。
在IA-32e模式下,IRET以8字节操作数大小执行。没有任何东西迫使这一要求。堆栈的格式是这样的:对于需要IRET的操作,8字节的IRET操作数大小可以正常工作。
因为在IA-32e模式下,中断堆栈帧推送总是8字节,所以IRET必须从堆栈中弹出8字节项。这是通过在IRET前面加上64位操作数大小前缀来实现的。
弹出的大小由指令的地址大小决定。SS/ESP/RSP大小调整由堆栈大小决定。
IRET仅当SS:RSP以64位模式执行时,才无条件地将其从中断堆栈帧中弹出。在兼容模式下,只有当CPL发生变化时,IRET才会将SS:RSP从堆栈中弹出。这使得在使用IRET指令时,遗留应用程序可以在兼容模式下正确执行。使用IRET退出的64位中断服务例程无条件地将SS:RSP从中断堆栈帧中弹出,即使目标代码段以64位模式运行或CPL=0。这是因为原始中断总是推动SS:RSP。
当启用了卷影堆栈并且目标特权级别不是3时,将来自卷影堆栈帧的CS:LIP与由堆栈的CS:EIP形成的返回线性地址进行比较。如果它们不匹配,则处理器会导致控制保护异常(#CP(FAR-RET/IRET)),否则处理器会从影子堆栈中弹出中断过程的SSP。如果目标特权级别为3,并且卷影堆栈在特权级别3启用,则从IA32_PL3_SSP MSR恢复中断过程的SSP。
在IA-32e模式下,允许IRET在特定条件下加载NULL SS。如果目标模式是64位模式,并且目标CPL≠ 3,IRET允许SS加载NULL选择器。作为堆栈切换机制的一部分,中断或异常会将新SS设置为NULL,而不是从TSS获取新SS选择器并从GDT或LDT加载相应的描述符。新的SS选择器设置为NULL,以便正确处理后续嵌套远传输的返回。如果被调用的过程本身被中断,则在堆栈帧上推送NULL SS。在随后的IRET中,堆栈上的NULL SS充当一个标志,告诉处理器不要加载新的SS描述符。
一个程序已ELF文件格式,存储在磁盘上,然后使用函数调用将他调起开始运行,ELF文件各种有对虚拟地址的排布,而操作系统需要完成虚拟地址对真实物理地址的分配,Intel 有两种方式,支持分段,支持分页,而Intel要求使用分页前必须使用分段。
首先一个进程运行起来,加载到内存中,而访存时间相对于CPU的运行时间又太大,所以又加了一道缓存缓存的数据被称为寄存器,而寄存器中只能存放少量的数据,现在我们有个需求,有一个函数需要调用另一个函数,我们就需要知道另一个函数在哪,所以我们需要维护一个映射,将他保存起来,然后去运行时加载他的地址去直接访问,实现这个功能的是GDT表,将调用的地址存放起来,这里讲的是保护模式下,保护模式是为了任务之间的随意踩踏,所以需要将每个程序隔离开来,不能随意访问代码和数据,这些就变成了私有的数据,有私有的数据就有公有的数据,而内存分布为栈,堆也就是这个原因,栈是保存私有数据,堆来存放公有数据,再加上真实的物理地址是对普通用户的应用程序不开放,他们只能访问虚拟地址。提供一个映射表,而查表就需要一个索引值,而段选择子就是GDT的索引,还需要一个首地址,索引加上首地址就可以定位表项,再从表项中加载到真实的基地址,并和偏移量进行相加从而得到真实的物理地址。GDT表比较重要,而这个GDT表项是只能有特权级为0的应用程序访问,不允许用户的程序来访问,所以我们就知道如果一个函数需要调用另一个函数就需要链接填充,而这个任务就是链接器做的,链接器又分为动态链接器和静态链接器。
动态链接器:
静态链接器:
而这个调用则是简单的函数调用,在一个指令集中自己调用另一个指令集,并不需要保存复杂的信息,只需要保存调用另一个函数之前的指令,如果跳转指令就不需要回去,如果是需要返回继续执行下一条指令就需要保存返回的路,以便找到回家的路。
而还有另一种情况就是,进程和进程之间的切换,比如,R0切换到R3去执行代码,而有些代码是只能由R0才能去执行,这个时候就需要进行权限位校验,每个代码有一个权限位标记,而当前进程的有自己的权限标记,这个时候就去对比权限,如果权限匹配正确,就可以执行,如果权限位匹配错误就抛出异常,这个时候就需要保存更多的信息,因为他需要保存堆栈的信息。
而CPU提供了如下的标记位,来进行校验,我当前需要的权限标记,我当前的权限标记和访问的权限标记。
RPL:程序对段访问的请求权限,意思是当前进程想要的请求权限。
CPL:是一种特殊的RPL,当前正在执行的代码所处的特权级。
DPL:代码本身真正的特权级。
而进程和进程切换,有两部分信息需要保存,一部分是CPU的信息,一部分是操作系统的信息,因为他需要上下文切换,切换的就是寄存器的信息,为了完成切换,每一个进程都会有一个对应描述当前的寄存器信息存放到TSS描述符。而这个TSS描述符他会保存到GDT表中,现在GDT表中有每个进程的代码段,数据段,堆栈段,TSS段,一个进程四个表项,太多了,所以为了节约空间,我们将代码段,数据段,堆栈段,单独抽出来存放到LDT中,将LDT的一个表项存放到GDT中,而寻址LDT的首地址被叫做LDTR。再加上LDT表中的基地址,就只可以找到真实的物理地址。
而现在只要是权限位校验通过就可以访问,而现在进入进程 的特权级想进入内核的特权级去执行,他因为权限位不同,无法进入内核执行,那么这里就需要对当前进程进行权限提升,而执行这个逻辑的就是 GATE ,只要经过了GATE 的访问权限校验,那么请求方和被请求方权限不相同则也会被执行,是将请求方的权限满足为被请求方要求的权限就可以正常访问。
而gate逻辑分为 call gate 调用门,Task gate 任务门,Traps gate 陷阱门,Interrupt gate 中断门
调用门:call 与 jmp 指令调用。栈切换。
异常会触发陷阱门和中断门。可屏蔽中断和不可屏蔽中断。
中断发生在程序执行期间,随机时间,响应来自硬件的信号。系统硬件使用中断来处理处理器外部的事件,例如为外围设备提供服务的请求。软件还可以通过执行INT n指令来产生中断。
中断门和陷阱门之间的区别如下:如果通过中断门调用中断或异常处理程序,处理器将清除EFLAGS寄存器中的中断启用(IF)标志,以防止后续中断干扰处理程序的执行。当通过陷阱门调用处理程序时,IF标志的状态不会改变。
当程序中或者硬件中出现了异常,则会进入异常处理逻辑,如何知道出现了异常呢。查看Intel开发手册得知。
- 外部中断通过处理器上的引脚或通过本地APIC接收。
- 当本地APIC全局/硬件禁用时,这些引脚分别配置为INTR和NMI引脚。断言INTR引脚表明处理器已经发生了外部中断。处理器从系统总线中读取由外部中断控制器提供的中断向量号,如8259A。
- 断言NMI引脚信号是一个不可屏蔽的中断(NMI),它被分配给中断向量2。
而异常又被分为 故障 Faults、陷阱 Traps 或中止 Aborts
发生(Faults)故障时,代码会回到触发异常的时那段代码;发生(Traps)陷阱时,代码会回到触发异常的下一段代码;发生中止(Aborts)时,不会返回。
当触发了中断向量,而CPU会在执行完一条指令后去检测是否有中断触发,如果有中断向量触发,而CPU则会转去执行中断。
而这里被分为
硬中断:硬件生成的中断。
软件中断:软件生成的中断。
操作系统中有一个软中断,这里的软件中断和软中断是两个不同的含义。操作系统Linux中软中断是,触发中断后被分为两个阶段,接收中断和处理中断,而处理中断被称其为软中断,含义不一样,不要混淆了。
当检测到中断触发以后,每一种异常对应这一个 error code 码,触发了对应的异常,则去处理相应的函数。而这个异常和处理函数的映射分为两部分,一部分是CPU自己定义的,一部分是用户自己定义的。
而这个映射会存储在 IDT 表项中,那么这个IDT表项的赋值就是有操作系统去设置的。现在找到了IDT对应的函数地址指针,因为访问IDT只有R0权限的程序可以操作,而R3级别的就需要切换栈结构,一切换栈结构就需要将原来的栈信息保存,找到回家的路。IDT表中存放了 偏移量,而GDT或者IDT中存放了代码的基地址。通过IDT表门逻辑就可以访问。
这个时候因为发生了权限级别的切换,而就需要切换进程,那么就有两部分数据一个是当前权限位执行的代码,切换到另一个权限位的执行的代码,因为权限位不同,权限就是为了控制访问的数据,所以这两块代码将会是独立运行的,包含有私有数据,那么在切换时为了因为是中途停止而去执行另一个私有数据而不共享,则就需要将老的数据先行保存,再去运行另一块私有空间的程序,另一个程序执行后,则会再次回到当前程序中执行,能不能回得来就看当前触发的异常类型是,回到当前代码,还是当前代码的下一行,还是有去无回的类型。
这个数据就包含两个信息,一个是运行时的信息,一个没有运行时静态的信息,这两部分就是一个进程的组成部分。现在我们就引入了进程的概念。静态的信息则使用了ELF文件,而运行时的信息是在寄存器中运行时保存的信息,而这部分信息会保存到 TSS 描述符中,而在切换时则会将 TSS 描述中的信息保存到 TR 寄存器中,而TSS描述符,则存放在 LDT表项中, 一个GDT表项有限,代码段描述符,数据段描述符,栈描述符,现在再加上TSS描述符,而一个进程需要这么多信息,那么GDT中存放如此多的信息,则会缩小可用程序,所以操作系统的解决方案就是再将 栈信息单独出来 而这个信息就是 IDT 表项。所以切换进程就需要切换 TS 寄存器中保存的 TSS 描述符。