0%

中断和异常处理-读书笔记

中断和异常处理

中断和异常处理概述

中断和异常是表明在系统、处理器的某个地方存在一个条件的事件,或当前执行的程序或任务中存在需要处理器注意的情况。它们通常会强制将进程从当前运行的程序或任务转移到一个特殊的软件程序或任务上,称为中断处理程序或异常处理程序。处理器为响应中断或异常而采取的行动被称为中断或异常处理。
中断在程序执行过程中随机发生,以响应来自硬件的信号。系统硬件使用中断来处理处理器外部的事件,如服务外围设备的请求。软件也可以通过执行INT n指令来产生中断。当处理器在执行指令时检测到一个错误条件,如除以0,就会发生异常。处理器检测各种错误条件,包括违反保护规定、页面故障和内部机器故障。
奔腾4、英特尔至强、P6系列和奔腾处理器的机器,当检测到内部硬件错误和总线错误时,也允许产生一个机器检查异常。
当处理器执行一个中断或异常处理程序时,当前运行的程序或任务被暂停。当处理程序的执行完成后,处理器恢复被中断的程序或任务的执行。除非无法从异常中恢复,或者中断导致当前运行的程序被终止,否则被中断的程序或任务的恢复不会失去程序的连续性。
在实模式下,中断向量表占据内存最低的1KB,共256个表项。每个表项4子节,包含一个2子节的段地址和2子节的偏移,即中断处理程序的入口地址。但是在保护模式下,中断向量表可以在内存中自由浮动。就像GDT被GDTR指向一样,中断向量表被IDTR(Interrupt Descriptor Table Register,中断描述符表寄存器)指向。该表和GDT非常类似。首先,GDTR和IDTR在格式上完全相同,均包含一个32bit的基地址和16bit的界限。相比之下,CPU中的另外两个关键寄存器LDTR和TR则表现出了相似性,都是16bit大小,分别包含指向LDT和TSS的选择子。从表项上来看,除了指出中断处理程序的目标地址(16bit选择子和32bit偏移)外,IDT表项还为了进行特权级检测而加入的DPL域。此外,IDT表项还包含一个P比特。

有关中断和异常了解性的内容

中断和异常向量

为了帮助处理异常和中断,每个架构上定义的异常和每个中断条件都被分配了一个唯一的识别号,称为向量号。处理器使用分配给一个异常或中断的向量号作为中断描述符表(IDT)的索引。该表提供了一个异常或中断处理程序的入口点。矢量号的允许范围是0到255。在0到31的范围内的向量号是由英特尔64和IA-32架构为架构定义的异常和中断保留了向量号。并非所有的向量号在这个范围内,并不是所有的向量号都有一个当前定义的功能。
在32到255范围内的向量号被指定为用户定义的中断,不被Intel64和IA-32架构保留。这些中断通常被分配给外部I/O设备,使这些设备能够向处理器发送中断。

中断源和异常源

中断来源

处理器接收来自两个来源的中断。

  • 外部(硬件产生的)中断。
  • 软件产生的中断
外部中断

外部中断是通过处理器上的引脚或通过本地APIC接收的。Pentium 4、Intel Xeon、P6系列和Pentium处理器的主要中断引脚是LINT[1:0]引脚,它与本地APIC相连。启用时,LINT[1:0]引脚可以通过APIC的本地向量表(LVT)进行编程,以便与处理器的任何异常或故障相关联。

image-20220410193839865

image-20220410193850982

可屏蔽的硬件中断

任何通过INTR引脚或通过本地APIC传递给处理器的外部中断被称为可屏蔽硬件中断。可以通过INTR引脚传送的可屏蔽硬件中断包括所有IA-32体系结构定义的从0到255的中断向量;那些可通过本地APIC传送的中断包括中断向量16到255。

软件产生的中断

INT n指令允许通过提供一个中断向量作为操作数,从软件内部产生中断。例如,INT 35指令强制调用35号中断的中断处理程序。任何从0到255的中断向量都可以作为该指令的参数。如果使用了处理器预定义的NMI向量,那么处理器的反应将与正常产生的的NMI中断产生的反应不一样。如果在这条指令中使用了2号向量(NMI向量),就会调用NMI中断处理程序,但处理器的NMI处理硬件没有被激活。用INT n指令在软件中产生的中断不能被EFLAGS寄存器中的IF标志所屏蔽。

异常来源

处理器接收来自三个来源的异常。

  • 处理器检测到的程序错误异常。
  • 软件产生的异常。
  • 机器检查的异常。
程序错误异常

当处理器在应用程序或操作系统或执行程序的执行过程中检测到程序错误时,会产生一个或多个异常。英特尔64和IA-32架构为每个处理器检测到的异常定义了一个向量号。

软件产生的异常

INTO, INT 3, 和 BOUND 指令允许在软件中产生异常。这些指令允许异常条件的检查在指令流中执行。例如,INT 3产生一个断点异常。INT n指令可以用来模拟软件中的异常;但是有一个限制。如果INT n提供了一个架构定义的异常的向量,处理器会产生一个中断到正确的向量(来访问异常处理程序),但不会在堆栈上推送错误代码。

机器检查异常

P6系列和奔腾处理器提供内部和外部机器检查机制,用于检查内部芯片硬件和总线交易的操作。这些机制取决于执行情况。当检测到机器检查错误时,处理器发出机器检查异常信号(向量18)并返回一个错误代码。

异常的分类:故障、陷阱和中止

异常被归类为故障、陷阱或中止,这取决于它们的报告方式以及引起异常的指令是否能在不损失程序或任务连续性的情况下重新启动。

  • 故障

故障是一种通常可以被纠正的异常,一旦被纠正,就可以在不损失程序或任务连续性的情况下重新启动程序。
当一个故障被报告时,处理器将机器状态恢复到在开始执行故障指令之前的状态。故障处理程序的返回地址(保存在CS和EIP寄存器的保存内容)指向故障指令,而不是指向故障指令之后的指令。

  • 陷阱

    陷阱是在执行陷阱指令后立即报告的异常。陷阱允许程序或任务的执行继续进行而不损失程序的连续性。陷阱处理程序的返回地址指向陷阱指令后要执行的指令。

  • 终止

    终止是一种异常,它并不总是报告引起异常的指令的精确位置,并且不允许重新启动引起异常的程序或任务。中止是用来报告严重的错误,例如硬件错误和系统表中不一致的或非法的值。

注意

一个通常被报告为故障的异常子集是不能重新启动的。这种异常会导致损失一些处理器的状态。例如,在执行POPAD指令时,堆栈框架执行POPAD指令时,堆栈框架越过了堆栈段的末端,导致报告了一个故障。在这种情况下,异常处理程序看到指令指针(CS:EIP)已经被恢复,就像POPAD 指令没有被执行。然而,内部处理器状态(通用寄存器)将被修改。这种情况被认为是编程错误

程序或任务的重新执行

为了允许在处理异常或中断后重新启动程序或任务,所有的异常(除了中止)都保证在指令边界上报告异常。所有的中断都被保证为在一个指令边界上进行。
对于故障类异常,返回指令指针(在处理器产生异常时保存)指向发生故障的指令。因此,当一个程序或任务在处理完故障后被重新启动时,发生故障的指令被重新启动(重新执行)。重启出错指令通常用于处理当对操作数的访问被阻止时产生的异常。这种类型的故障最常见的例子是页面故障异常(#PF),它发生在程序或任务引用位于不在内存中的页面上的操作数时。
当页面故障异常发生时,异常处理程序可以将该页面加载到内存中,并通过重启来恢复程序或任务的执行。为了确保重启对当前执行的程序或任务来说是透明的,处理器保存了必要的寄存器和堆栈指针 ,以允许重新启动到执行故障指令之前的状态。
对于陷阱类异常,返回指令的指针指向陷阱指令之后的指令。如果在转移执行的指令中检测到一个陷阱,返回指令指针反映了转移。例如,如果在执行JMP指令时检测到一个陷阱,返回指令的指针指向JMP指令的目的地,而不是JMP指令之后的下一个地址。所有的陷阱异常都允许程序或任务的重新启动而不会失去连续性。例如,溢出异常就是一个陷阱异常。在这里,返回指令的指针指向INTO指令之后的指令,该指令测试了EFLAGS.OF(溢出)标志。这个异常的陷阱处理程序解决了溢出的问题。从陷阱处理程序返回后,程序或任务在INTO指令之后的指令继续执行。
终止类异常不支持程序或任务的可靠重启。终止处理程序被设计为收集关于终止异常发生时处理器状态的诊断信息,然后尽可能优雅地关闭应用程序和系统。
中断程序严格地支持重新启动被中断的程序和任务而不损失连续性。返回为中断保存的返回指令指针指向要在指令边界执行的下一条指令 。如果刚刚执行的指令有一个重复的前缀,那么中断就会在当前迭代结束时进行,寄存器被设置为执行下一个迭代。

开启和禁止中断

处理器会抑制一些中断的产生,这取决于处理器的状态和EFLAGS寄存器中的IF和RF标志的状态。

屏蔽可屏蔽硬件中断

IF标志可以禁止对处理器INTR引脚上或通过本地APIC接收的可屏蔽硬件中断进行服务。当IF标志清除时,处理器会抑制传递到INTR引脚或通过本地APIC的中断产生内部中断请求;当IF标志被设置时,传递到INTR或通过本地APIC引脚的中断被作为正常的外部中断处理。
IF标志不影响传递到NMI引脚的非屏蔽中断(NMI)或通过本地APIC传递的交付模式NMI消息,也不影响处理器产生的异常。与EFLAGS寄存器中的其他标志一样,处理器在响应硬件复位时清除IF标志。
事实上,可屏蔽的硬件中断组包括保留的中断和异常向量0到32可能会引起混淆。从结构上看,当IF标志被设置时,矢量0到32的中断可以通过INTR引脚传递给处理器,而矢量16到32的中断则可以通过本地接口传递。然后,处理器将产生一个中断并调用中断或异常处理程序,该处理程序由矢量编号指向。
IF标志可以通过STI(设置中断使能标志)和CLI(清除中断使能标志)指令来设置或清除。这些指令只有在CPL等于或小于IOPL的情况下才能执行。如果在CPL大于IOPL的情况下执行这些指令,会产生一般保护异常(#GP)。
IF标志也受到以下操作的影响。

  • PUSHF指令将所有的标志存储在堆栈上,在那里可以检查和修改它们。POPF指令可以用来将修改后的标志加载到EFLAGS寄存器中。
  • 任务开关、POPF和IRET指令加载EFLAGS寄存器;因此,它们可以用来修改IF标志的设置。
  • 当一个中断通过中断门处理时,IF标志会被自动清除,这就禁止了可屏蔽的硬件中断。

屏蔽指令断点

EFLAGS寄存器中的RF(恢复)标志控制处理器对指令断点条件的响应。当设置时,它阻止指令断点产生调试异常(#DB);当清除时,指令断点将产生调试异常。RF标志的主要功能是防止处理器在以下情况下进入调试异常循环 。

切换堆栈时屏蔽异常和中断

为了切换到一个不同的堆栈段,软件经常使用一对指令,例如:
MOV SS, AX
MOV ESP, StackTop
如果在段选择器被加载到SS寄存器之后,但在ESP寄存器被加载之前,发生了中断或异常。ESP寄存器中,这两部分进入堆栈空间的逻辑地址在中断或异常处理过程中是不一致的。为了防止这种情况的发生,处理器在MOV到SS指令或POP到SS指令之后,处理器抑制中断、调试异常和单步陷阱异常,直到到达下一条指令后的指令边界。所有其他的故障仍然可以被产生。如果LSS指令被用来修改SS寄存器的内容(推荐的方法),这个问题就不会发生。

异常和中断的优先级

如果在一个指令边界有一个以上的异常或中断等待处理,处理器会以可预测的顺序处理它们。下表显示了异常和中断源类别之间的优先级。

image-20220410202420157

注意:虽然在表中列出的这些类别的优先级在整个架构中是一致的,但是每个类别中的例外情况是与实现有关的,可能因处理器不同而不同。处理器首先处理来自具有最高优先级的类的未决异常或中断,将执行转移到处理程序的第一条 指令。较低优先级的异常被丢弃;较低优先级的中断被搁置。当中断处理程序将执行返回到程序或任务中出现异常和/或中断的位置时,被丢弃的异常会重新产生。

中断描述符表

中断描述符表(IDT)将每个异常或中断向量与用于服务相关异常或中断的门描述符联系起来。与GDT和LDT一样,IDT是一个由8字节描述符组成的数组。。与GDT不同,IDT的第一个条目可以包含一个描述符。为了
为了形成对IDT的索引,处理器将异常或中断向量按8(门描述符的字节数)的比例进行调整。因为只有256个中断或异常向量,IDT不需要包含多于256个描述符。它可以包含少于256个描述符,因为描述符只需要用于可能发生的中断和可能出现的异常向量需要描述符。IDT中所有空的描述符槽都应该将描述符的当前标志设置为0。
IDT的基地址应该在8字节的边界上对齐,以最大限度地提高缓存行的性能 。极限值以字节为单位,并与基地址相加,得到最后一个有效字节的地址。极限值为0时,正好是一个有效的字节。因为IDT条目总是8个字节长,所以极限值应该是始终是8的整数倍(即8N-1)。
IDT可以驻留在线性地址空间的任何地方。如图6-1所示,处理器使用IDTR寄存器来定位IDT 。这个寄存器持有32位的基地址和16位的IDT限制。
LIDT(加载IDT寄存器)和SIDT(存储IDT寄存器)指令分别加载和存储IDTR的内容。LIDT指令将IDTR寄存器中的基地址和限值加载到一个内存操作数中。这条指令只有在CPL为0时才能被执行。它通常由操作系统的初始化代码在创建IDT时使用。操作系统也可以用它来改变一个IDT到另一个IDT。
SIDT指令将IDTR中存储的基数和极限值复制到内存中。这条指令可以在任何权限级别下执行。如果一个向量引用的描述符超过了IDT的限制,就会产生一个一般保护异常(#GP)。
注意
由于中断只传递给处理器内核一次,一个不正确配置的IDT可能导致不完整的中断处理和/或中断传递的阻塞。在设置IDTR基础/限制/访问字段和门描述符中的每个字段时,需要遵循IA-32架构的规则。

image-20220410202728708

IDT 描述符

IDT可能包含三种门描述符中的任何一种。

  • 任务门描述符
  • 中断门描述符

陷阱门描述符
图6-2显示了任务门、中断门和陷阱门描述符的格式。任务门的格式在IDT中使用的任务门的格式与在GDT或LDT中使用的任务门的格式相同。任务门包含一个异常和/或中断处理任务的TSS的段选择器。
中断和陷阱门与调用门非常相似。它们包含一个远指针(段选择器和偏移量),处理器用它来把程序的执行转移到异常或中断处理程序代码中的一个处理程序。
这些门的不同之处在于处理器处理EFLAGS寄存器中的IF标志的方式。

image-20220410205107225

中断与异常处理

处理器处理对异常处理程序和中断处理程序的调用的方式类似于处理对过程或任务的CALL指令来处理对过程或任务的调用。当响应一个异常或中断时,处理器使用异常或中断向量作为IDT中描述符的索引。如果该索引指向一个中断门或陷阱门,处理器调用异常或中断处理程序,其方式类似于调用门的CALL。如果 index 指向一个任务
门,处理器将执行一个任务切换到异常或中断处理任务,其方式类似于 CALL到一个任务门

异常或中断处理程序

中断门或陷阱门引用了一个异常或中断处理程序,该程序在当前执行的任务的上下文中运行(见图6-3)。该门的段选择器指向一个段描述符,该段描述符位于GDT或当前LDT中的一个可执行代码段。门描述符的偏移字段指的是
异常或中断处理程序的开头。

image-20220410205743923

当处理器执行一个对异常或中断处理程序的调用时。

  • 如果处理程序要在一个较低的权限级别上执行,就会发生堆栈切换。
    当堆栈切换发生时 :
    a. 处理程序使用的堆栈的段选择器和堆栈指针是从当前执行任务的TSS中获得的。在这个新的堆栈中,处理程序推送了被中断的堆栈段选择器和堆栈指针。
    b. 然后,处理器将EFLAGS、CS和EIP寄存器的当前状态保存在新的堆栈中(见图6-4)。
    c. 如果一个异常导致错误代码被保存,它将被推到EIP值之后的新栈上。
  • 如果处理程序要在与被中断程序相同的权限级别下执行。
    a. 处理器将EFLAGS、CS和EIP寄存器的当前状态保存在当前堆栈中(见图6-4)。
    b. 如果一个异常导致错误代码被保存,那么它将在EIP值之后被推到当前堆栈中。

image-20220410210127965

要从一个异常或中断处理程序中返回,处理程序必须使用IRET(或IRETD)指令。IRET指令与RET指令类似,只是它将保存的标志恢复到EFLAGS寄存器中。只有当CPL为0时,EFLAGS寄存器的IOPL字段才被恢复。

异常和中断处理程序的保护

异常和中断处理程序的特权级别保护与通过调用门调用普通程序的保护相似。处理器不允许将执行转移到一个异常或中断处理程序中。试图违反这一规则会导致一般保护异常(#GP)。异常处理程序和中断处理程序的保护机制在以下方面有所不同:

  • 因为中断和异常向量没有RPL,RPL在隐式调用异常和中断处理程序的隐式调用不检查RPL。
  • 只有在异常或中断产生的时候,处理器才会检查中断或陷阱门的DPL,如果有一个INT n, INT 3, 或 INTO 指令产生的异常或中断,处理器才会检查中断或陷阱门的 DPL。这里,CPL必须小于或等于门的DPL。这一限制防止了运行在权限级别3的应用程序或程序使用软件中断来访问关键的异常处理程序。中断来访问关键的异常处理程序,例如页面故障处理程序,条件是这些处理程序被放在更有权限的代码段中。对于硬件产生的中断和处理器检测到的异常,处理器忽略了中断和陷阱门的DPL。

由于异常和中断通常不会在可预测的时间发生,这些特权规则有效地对异常和中断处理程序可以运行的权限级别进行了限制。以下两种技术都可以用来避免违反权限级别:

  • 异常或中断处理程序可以放在一个符合要求的代码段中。这种技术可以用于处理程序,这些处理程序只需要访问堆栈上的数据(例如,划分错误异常)。如果处理程序需要来自数据段的数据,数据段需要从权限级别3访问,这将使其不受保护。
  • 处理程序可以被放置在一个特权级别为0的不符合要求的代码段中。这个处理程序将始终运行,不管被中断的程序或任务是在哪个CPL下运行。

异常或中断处理程序的标志用法

当通过中断门或陷阱门访问异常或中断处理程序时,处理器在将EFLAGS寄存器的内容保存在堆栈中后,清除EFLAGS寄存器中的TF标志。(在调用异常和中断处理程序时,处理器也会清除EFLAGS寄存器中的VM、RF和NT标志,然后将它们保存在堆栈中。)清除TF标志可以防止指令跟踪影响中断响应。A 后续的IRET指令将TF(以及VM、RF和NT)标志恢复到堆栈中EFLAGS寄存器的保存内容中的值。
中断门和陷阱门的唯一区别是处理器处理IF标志的方式。当通过中断门访问一个异常或中断处理程序时,处理器会清除IF标志以防止 其他中断干扰当前的中断处理程序。随后的IRET指令将IF标志恢复到堆栈上EFLAGS寄存器的保存内容中的值。通过陷阱门访问处理程序并不影响IF标志。

中断任务

当异常或中断处理程序通过IDT中的任务门被访问时,会产生一个任务切换。处理异常或中断的单独任务有几个优点。

  • 被中断的程序或任务的整个上下文被自动保存。
  • 一个新的TSS允许处理程序在处理异常或中断时使用一个新的权限级别0的堆栈。如果异常或中断发生时,当前权限级别0的堆栈被破坏,通过任务门访问处理程序可以通过为处理程序提供一个新的权限级别0的堆栈来防止系统崩溃。
  • 处理程序可以通过给它一个单独的地址空间来进一步与其他任务隔离。这可以通过以下方式实现给它一个单独的LDT。

用一个单独的任务来处理中断的缺点是,在任务切换时必须保存大量的机器状态,使得它比使用中断门要慢,从而导致中断延迟的增加。
IDT中的任务门引用GDT中的TSS描述符(见图6-5)。切换到处理程序任务的方式与普通任务切换相同。返回到被中断的任务的链接 被存储在处理程序任务的TSS的前一个任务链接域中。如果一个异常导致一个错误代码,这个错误代码被复制到新任务的堆栈中。
在操作系统中使用异常或中断处理任务时,实际上有两种机制可以用来调度任务:软件调度器(操作系统的一部分)和硬件调度器(处理器的中断机制的一部分)。软件调度器需要容纳中断任务当中断被激活时,可能会被调度。
注意
由于IA-32体系结构的任务不是可重入的,一个中断处理程序任务在它完成处理中断和执行IRET指令时,必须禁用中断。这个动作可以防止在中断任务的TSS仍被标记为忙时发生另一个中断,这将导致一般保护(#GP)异常。

image-20220411091420768