目录:
中断和异常概述
中断和异常向量
中断和异常的来源
使能和禁止中断
Interrupt Descriptor Table(IDT)
异常和中断处理过程
错误代码Error Code
参考文献
0x1 中断和异常概述
中断和异常表明系统中有些地方出现了意外状况,需要处理器来处理一下,可以是指令运行出错、程序访问越界或外设需要处理器服务等,这时处理器通常会停下手中的工作,根据意外情况的类型,转移到相应的中断或异常处理程序或任务来执行。在《汇编语言 王爽》一书中,将异常和中断都称之为中断,只是将异常叫做内中断,中断叫做外中断,在Intel CPU手册中,对两者进行了严格的区分: 中断随时可以发生,且来自于CPU外部。异常发生在处理器执行当前指令时,来自于CPU内部,表示处理器检测到该指令的执行出现了错误状况。你有可能在别的地方看到这样的论述:”中断是异步的,异常是同步的“,这里的异步同步指的就是中断和当前CPU执行的指令没有关系,任何时刻都可以发生,异常和当前CPU执行的指令伴随而生,CPU只有在执行当前指令时检测到了错误情况(如违反保护机制、页错误等)才会产生异常。1.1 中断和异常向量(Vector)
引起中断和异常的情况有很多,我们需要为他们进行编码来区分它们,因此CPU对每一个中断和异常情况分配了一个唯一标识号,称作向量号(vector number),Intel规定,不论是实模式,还是保护模式,都只有256个向量号,其中0~31的向量号被CPU保留,用户不得使用,但是这32个向量号,Intel也没有全部都使用,用户可以根据自己的需要使用几个,但是未来Intel使用时,也要给人家让位置。 在实模式下,0x0000~0x03FF的1KB区域被称之为中断向量表(Interrupt Vector Table,IVT),存储着256个中断向量,每个中断向量4字节,由16位的基地址和16位的偏移地址组成,两者共同组成中断/异常处理程序的入口。当中断和异常发生时,处理器拿着向量号作为索引,在IVT中寻找对应的中断向量,例如向量号为1,则0x0004~0x0007的4字节区域就是我们需要的中断向量,然后将中断向量的16位基地址加载到CS寄存器中,16位的偏移地址加载到IP寄存器中,转移到中断/异常处理程序执行。当然转移之前还有一些保护现场的工作,读者只需要有这么个步骤在就行。 保护模式和实模式几乎没有任何区别,只是IVT变成了IDT(Interrupt Descriptor Table,中断描述符表),4字节的中断向量变成了8字节的门描述符。如果读者现在还不知道门描述符是什么,可以将门描述符视作一个指向中断/异常处理程序的指针,当我们得到了门描述符,就可以填充CS和EIP寄存器,从而转到中断/异常处理程序执行。IDT和GDT一样,属于全局数据,每个任务共享一个GDT和IDT,相应的,也有一个48位寄存器IDTR来存储IDT的信息(32位的起始地址和16位的界限)。值得一提的是,我们前面说了,Intel规定只有256个向量号,既然保护模式下门描述符8字节,因此IDT最多可以有256*8=2048=2K的空间,16位段界限可以表示最大空间为2^16=64K,出现了不一致的情况,但没有办法,IDT最大只能2KB,段界限的高5位只能浪费掉。现在知道有个寄存器放着IDT的起始地址就好办了,当中断或异常发生时,处理器从IDTR中获取IDT的起始地址,拿着中断号,乘以8后和IDT的起始地址相加,获得门描述符的位置,得到门描述符后,向CS和EIP寄存器中填入新值,转移到中断/异常处理程序执行。1.2 中断和异常的来源
1.2.1 中断的来源:外部中断和软中断
- 外部(硬件产生)中断
下面来讲讲非屏蔽中断和可屏蔽中断
- 非屏蔽中断NMI几乎所有触发NMI的事件对处理器来说都是致命的,甚至是不可纠正的。因此NMI被赋予了统一的中断号2,不再细分,一旦发生了2号中断,处理器和软件系统通常会放弃继续正常工作的”念头“,也不会试图纠正已经发生的问题和错误,很可能只是软件系统给出的一个提示信息。当出现NMI中断时,处理器立即调用2号中断处理程序,同时也有一些硬件措施来确保中断程序完成前不会出现其他中断包括NMI中断。
- 可屏蔽中断和NMI不同,更多的时候,发往处理器的中断信号通常不会意味着灾难,这类中断有两个特点,第一是数量很多,毕竟有很多外部设备,第二是他们可以被屏蔽,这样处理器可以不对他们进行处理,所以这类硬件中断称为可屏蔽中断。可屏蔽中断时通过INTR引脚进入处理器内部,像NMI一样,不可能为每一个中断源都提供一个引脚,而且,处理器每次只能处理一个中断。在这种情况下,需要一个代理,来接受外部设备发出的中断信号。还有,多个设备同时发出中断请求的几率也是很高的,所以该代理的任务还包括对他们进行仲裁,以决定让他们中的哪一个优先向处理器提出请求。如下图,在个人计算机中用的最多的中断代理就是8259芯片,他就是通常所说的中断控制器(Programmable Interrupt Controller,PIC),从8086处理器开始,他就一直提供这种服务,即使是现在,绝大多数单处理器的计算机中,也依然有他的存在,当出现了多处理器时,通常使用我们上面提到的APIC(Advanced Programmable Interrupt Controller)来进行中断代理。
Intel 处理器允许256个中断,中断号的范围是0~255,8259负责提供其中的15个,但中断号并不固定。之所以不固定,是因为当初设计的时候,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in和out指令来改变它的状态,包括各引脚的中断号。正是因为这样,它又叫可编程中断控制器(Programmable Interrupt Controller,PIC)。
软件产生的中断(又称软中断)
INT n
指令允许软件提供向量号来触发中断。n可以取0~255的任意值,但是如果取处理器预先为NMI分配的向量号时,效果和硬件触发的NMI不同,只是调用NMI中断处理程序,但是硬件上对NMI中断的处理不被激活。软中断不会被EFLAGS.IF屏蔽。
1.2.2 异常的来源
- 处理器检测到的程序错误异常当处理器检测到程序指令出现错误时,会生成一个或多个异常,如除法除数为0,页错误等。
- 软件产生的异常INTO、INT1、INT3和BOUND指令允许软件产生异常,这些指令允许在指令流的某个节点检查条件来判断异常是否发生。INT n指令可以用来模拟软件异常,但是存在限制:INT n产生的error code不会被压入栈中,当异常处理程序想要pop弹出error code时就会将EIP给丢弃了,最后返回到错误的地址。
- 机器检测异常(Machine-Check Exception)P6家族和奔腾Pentium处理器提供了处理器内部和外部的检查机制,来检测内部芯片硬件和总线实物的操作。当出现错误时,会触发18号异常。
1.2.3 异常的分类
异常被划分为三类:错误、陷阱和中断,划分方式取决于他们被报告的方式和是否可以被恢复。- Fault(错误)错误通常是可以被更正的,且更正后重新执行引发异常的指令,而不是下一条指令。
- Trap(陷阱)陷阱也是可以被更正的,返回地址是引起异常指令的下一条指令(确切的说,是下一条要执行的指令,可以不紧挨着异常指令,如JMP指令的下一条指令)。
- Aborts(终止)终止是往往无法报告导致异常指令的精确位置,且不允许程序或任务的重新执行,终止通常用来报告严重的错误。
0x2 使能和禁止中断
处理器通过清除和设置处理器EFLAGS的IF和RF字段来禁止一些中断。2.1 屏蔽可屏蔽硬件中断
EFLAGS的IF位可以用来屏蔽处理器INTR引脚收到的可屏蔽中断,当IF位清除时,处理器屏蔽INTR传来的可屏蔽中断;IF置位为1,INTR引脚传来的中断被当作正常的外部中断处理。IF不影响非屏蔽中断NMI和异常。2.1.1 如何设置IF位
IF位可以通过STI和CLI指令来设置和清除,但是这些指令只能在CPL<=IOPL的情况下才可以执行,否则会触发异常。除此以外,IF位还可以被其他指令影响:
- PUSHF压入EFLAGS寄存器,更改后POPF弹出修改后的EFLAGS寄存器的值(PUSHF和POPF也需要CPL<=IOPL
- 任务切换。
- 当使用中断门指向的中断处理程序时,IF自动被清除;如果是陷阱门指向的中断处理程序,IF不会被清除。
2.2 屏蔽指令断点
读者只需知道和EFLAGS.RF位有关即可。2.3 屏蔽切换栈的中断和异常
切换栈时,软件通常采用以下指令(也可以是pop)
mov ss, axmov esp, stackTop
如果加载SS段描述符时(mov ss,ax)出现了异常或中断,此时新的esp话没有加载,那么栈空间的两部分逻辑地址ss和esp在进入中断处理程序时是不一致的,为了解决这种情况,处理器在设置mov新值到ss寄存器或pop新值到ss寄存器的指令边界阻止下列事情的发生:(笔者能力有限,故不作翻译来保持文章的准确性)
0x3 Interrupt Descriptor Table(IDT)
前面提过IDT,现在进行详细讲解,IDT中存放的是8字节门描述符,门描述有三种,分别是
- 中断门
- 陷阱门
- 任务门
中断门和陷阱门类似于调用门,包含着目标代码段选择符和偏移地址,中断门和陷阱门的区别在于处理器对待EFLAGS寄存器IF位的方式。任务门和中断门调用门不同,中断门和陷阱门指向的中断处理程序和被中断程序处于同一任务上下文,但是任务门指向的中断处理程序和被中断程序不属于同一个任务,其中存在任务切换。详情可参考Intel x86 CPU 32位保护模式杂谈之任务切换 上
0x4 异常和中断处理过程
当发生异常和中断时,处理器拿着向量号作为IDT中门描述符的索引,如果索引指向中断门或陷阱门,处理器调用中断和异常处理程序,如果指向任务门,则执行任务切换,类似于我们之前说的CALL指令调用任务门。
4.1 异常门和中断门处理程序
当索引指向中断门和陷阱门时,门描述符中的段选择符指向GDT和LDT中的可执行代码段,偏移字段指向中断和异常处理程序的开始。
当处理器执行异常和中断处理程序之前会通过特权级规则进行检查,和call指令调用调用门规则类似,处理器不允许从高特权级转移到低特权级代码段的转移,只有当CPL>=目标代码段的DPL时才可以执行异常和中断处理程序,同时和调用门规则不同的是:
- 因为中断和异常只有向量号,没有选择符,也就没有所谓的RPL,因此不需要检查
- 处理器只有在INT n、INT3 和INTO指令导致的中断异常时才会检测中断或异常门描述符的DPL,此时,CPL应当数值上小于门描述符的DPL,这样便阻止了运行在特权级3的程序使用软中断来访问一些关键中断/异常处理程序,如页中断,因为他们的门描述符DPL数值小,特权极高,所以阻止了低特权级的访问。对于硬件产生的中断和处理器检测到的异常,门描述符的DPL对他们没影响。(附上一张32位x86CPU代码段转移特权级总结图,由于微信压缩图片问题,可能不清晰,公众号聊天框发送“代码段转移特权级总结图”获取图片链接?)
当符合上述特权级规则后,处理器还会检查目标代码段的的特权级以确定是否会改变特权级。
- 如果中断/异常处理程序代码段级别高(数值小),发生栈切换
- 从当前任务的TSS中获取高特权级栈段选择符和栈顶指针的值,在新栈中压入旧栈的栈选择符和栈顶指针。
- 处理器保存当前的EFLAGS、CS和EIP寄存器,依次压入新栈中
- 如果异常导致错误码(error code),将错误码压入新栈中。
- 如果中断/异常处理程序代码级别相同,不发生栈切换
- 处理器保存当前的EFLAGS、CS和EIP寄存器到栈中。
- 如果异常导致错误码(error code),将错误码压入栈中。
4.1.1 中断和异常处理程序影响到的EFLAGS位
当访问中断和异常处理程序时,处理器会在将EFLAGS寄存器保存到栈上后,清除EFLAGS的TF位,以及VM、RF和NT位。(注:清除NT位在我们的意料之内,不论是从任务门返回还是从中断或陷阱门返回,都是使用IRET指令,NT位是区分这两个的关键,向量号指向任务门时,通过中断导致的任务切换会将新任务的EFLAGS寄存器置1。当NT位为0,表示通过中断门或陷阱门进入此代码段,应返回到当前任务上下文的另一段代码空间,当NT位为1表示通过任务门进入此代码段,应返回到前一个任务中去继续执行,详情可以参加上一篇文章Intel x86 CPU 32位保护模式杂谈之任务切换 上)。当IRET指令返回时,会从栈中恢复EFLAGS的值。
中断门和陷阱门的唯一区别在于处理器对待EFLAGS寄存器IF位的方式。当通过中断门进入中断/异常处理程序时,处理器清除IF位来阻止其他可屏蔽中断的干扰,通过陷阱门则不会改变IF位。
4.2 任务门进行任务切换
当向量号指向IDT中的任务门时,发生任务切换。通过一个单独的任务来处理中断和异常有以下几个优点:
- 被中断程序或任务的状态被自动保存下来
- 一个新的TSS允许处理程序使用一个新的特权级为0的栈来处理中断和异常,如果异常或中断发生时,特权级为0的栈崩溃了,通过任务门来处理中断和异常会使用新的栈从而可以阻止系统崩溃。
- 可以为处理程序提供单独的LDT,从而具有了和其他任务隔离的新的地址空间。
缺点也很容易看得出来,就是开销比较大,转移到中断/异常处理程序之前必须保存一些机器状态,对于一些反复出现的异常和中断,不建议用一个单独的任务来处理,比如处理键盘请求。
任务切换的过程参见Intel x86 CPU 32位保护模式杂谈之任务切换 上
0x5 错误代码(Error Code)
当异常情况和特定的段选择符相关时,处理器会压入一个错误代码到异常处理程序的栈中,错误代码格式如下:
里面其实就是一个段选择符,TI仍用来表示是GDT还是LDT,RPL被IDT和EXT位取而代之。当EXT被置1时,表明异常是由外部中断导致的,如果为0表示异常是由软中断(INT n,INT3或INTO)导致的。IDT为1时表示选择符的索引为向量号,指向IDT中的门描述符,为0表示索引指向GDT或LDT的描述符。因此,只有当IDT位为0时,TI位才会发挥作用。