写在前面
本文重在讨论8086处理器的中断的原理与分类,以及一些中断向量表的内容。由于笔者水平有限,随笔中难免有些许纰漏和错误,希望广大读者能指正。
中断的分类
我们先来看看中断的分类。大体上来说,中断可以分为外部硬件中断、内部中断、软中断。
中断就是打断、暂停的意思,中断让CPU放下手中正在处理的事,转而过来处理发生的中断信号。为了区别不同的中断事件,我们给每一个中断都赋予了一个编号,称之为中断号。以上三种类型的中断共用一套中断编号。
外部硬件中断
顾名思义,外部硬件中断就是来自CPU之外硬件设备的中断。比如说,一台打印机结束打印了,这是它就会产生一个中断,来告诉CPU先暂停正在处理的任务,然后来处理一下这个中断。为了支持硬件外部中断,有两根信号线接入了8086CPU内部,它们是
- NMI,非屏蔽中断
- INTR,可屏蔽中断
之所以设计两个中断引脚接收外部硬件中断信号,是因为人们希望能够区别对待不同的外部硬件中断信号。接下来我们就探讨一下两种外部硬件中断。
可屏蔽中断
对于一些不是那么紧急的中断信号,它们是通过INTR来输入到CPU中的。由于它们不是十分要紧,当有中断信号从INTR中传到CPU中时,CPU可以选择处理或屏蔽掉这个中断信号的。在处理器的外部,有一个开关控制着INTR中断信号能否被传送到CPU中。也就是说,一个INTR中断信号想要被CPU处理,需要经过两道关卡:
- 能否被传送到CPU中
- CPU是否处理该中断
8259芯片
可屏蔽中断有很多个,但是只有一条INTR线接到CPU中的引脚中。并且,外部设备也可能同时向CPU发送中断信号。为了区分不同的中断事件和处理可屏蔽中断信号的优先级,我们引入了一块芯片:8259。
8259芯片有8个中断输入引脚。后来随着计算机系统的复杂性增加,越来越多的硬件设备加入其中,原来的8个输入引脚已经力不从心了。于是便有了将多片8259芯片级联(Cascade)的做法。以级联2块8259芯片为例,见下图(来自李忠老师的《x86汇编:从实模式到保护模式》):
主片通过INT线与CPU相连,并提供了自己的2号引脚IR2与从片相连。这样,就可以表示15个中断信号了。在实模式中,每个引脚都提供默认的中断信号,这个读者可以自行了解。
IMR
在每块8259芯片内部,有一个名为IMR的8位寄存器(interrupt mask register),也就是中断屏蔽器,它的作用就是我们前面提到的用于控制中断信号能否到达CPU内部的开关。这个8位寄存器的每一位分别对应这块8259芯片上一个中断信号输入引脚,规定0允许1中断。
8259芯片是可以编程的,我们可以使用对应的端口号对8259芯片进行编程。这就我们就可以设置8259芯片的工作方式和IMR的内容。关于对8259芯片进行编程更详细的讨论,笔者以后会更新一篇新的文章。
FLAG中的IF
前面我们说过,一个INTR中断信号能否被CPU处理,除了受IMR影响,还要看CPU自己是否决定处理这个中断信号。在FLAG寄存器中,第9位,也就是IF(interrupt flag)位,决定了CPU是否对这个中断信号进行处理。当IF为0时,所有从处理器INTR引脚来的中断信号都被忽略掉;当其为1时,处理器可以接受和响应中断。我们可以通过cli和sti指令对IF位进行设置。
当CPU正在处理一个中断时,此时又来了另外一个中断,8259芯片决定了这些中断的优先级。总体上来说,中断的优先级和引脚是相关的,主片的IR0引脚优先级最高,IR7引脚优先级最低,从片也是如此。当然,还要考虑到从片是级联在主片的IR2引脚上的。如果CPU正在处理一个低级中断,此时来了一个高级中断,CPU会优先处理高级中断信号。
中断向量表
CPU在处理中断时,依赖一张名为中断向量表(interrupt vector table)的表。这张表说明了如何处理一个中断。它指明了一个中断号与一个处理过程地址的对应关系。
在实模式下,中断向量表存放在内存中从物理地址0x00000开始到0x003ff结束,共1KB的空间内。下图来自李忠老师的《x86汇编:从实模式到保护模式》:
我们可以看到,每个中断向量大小为2个字。
在8259芯片那里,每个中断输入引脚都赋予了一个中断号。而且,这些中断号是可以改变的,可以对8259编程来灵活设置,但不能单独进行,只能以芯片为单位进行。比如,可以指定主片的中断号从0x28开始,那么它每个引脚IR0~IR7所对应的中断号分别是0x28~0x2f。
当中断发生时,如果这个中断能经过我们前面所说的两道关卡,那么,CPU在执行完当前的指令后,会立即着手为硬件服务。它首先会响应中断,告诉8259芯片准备着手处理该中断。接着,它还会要求8259芯片把中断号送过来。在CPU拿到中断号之后,它会按顺序执行以下步骤:
- 保护断点的现场。首先要将标志寄存器FLAGS压栈,然后清除它的IF位和TF位。接着再将当前的代码段寄存器CS和指令指针寄存器IP压栈。
- 执行中断处理程序。CPU将中断号乘以4(每个中断在中断向量表中占4字节),就得到了该中断入口点在中断向量表中的偏移地址。接着,从表中依次取出中断程序的偏移地址和段地址,并分别传送到IP和CS,开始执行中断处理程序。
- 返回到断点接着执行。所有中断处理程序的最后一条指令必须是中断返回指令iret。这将导致处理器依次从栈中弹出(恢复)IP、CS和FLAGS的原始内容,于是转到主程序接着执行。
在第二步中,清除IF位说明CPU在执行中断处理过程时不再理会新的中断。如果希望为更高级的中断服务,可以使用sti指令设置IF位。
中断向量表的建立和初始化工作是由BIOS在计算机启动时负责完成的。BIOS为每个中断号填写入口地址,一律将它们指向一个相同的入口地址,在那里,只有一条指令:iret。也就是说,当这些中断发生时,处理中断就是立即返回。当计算机启动后,操作系统和用户程序再根据自己的需要,来修改某些中断的入口地址,使它指向自己的代码。
非屏蔽中断
与可屏蔽中断的机制相比,非可屏蔽中断就简单多了。虽然非可屏蔽中断不能被CPU忽略,但是在外部依然有一个开关用来控制中断信号能否到达CPU中。在实模式下,NMI有统一的中断号2,所以使用一根INM线接到CPU的一个引脚中就可以了。
下面这段话摘自李忠老师的《x86汇编:从实模式到保护模式》:
Intel规定,NMI中断信号由0跳变到1之后,至少要维持4个以上的时钟周期才算是有效的,才能被识别。
内部中断
内部中断是CPU在执行指令时产生的,内部中断不受标志寄存器IF位的影响,也不需要中断识别总线周期,它们的中断类型是固定的,可以立即转入相应的处理过程。
软中断
在编写程序的时候,我们可以随时用指令来产生中断,这种类型的中断叫作软中断。软中断也不需要中断识别总线周期,中断号在指令中给出。软中断指令包括以下三种:
int3
int imm8
into
其中int3是断点中断指令,机器指令码为0xCC,我们通常在调试程序时的打断点,就是使用这条指令实现的。打断点的原理就是,把需要暂停执行的那条指令的第一个字节设置为0xCC,当处理器执行到int3时,即发生3号中断,转去执行相应的中断处理程序。当然,在执行中断处理程序之前,需要先用push指令保护现场。最后,再恢复那条指令的第一字节,并修改位于栈中的返回地址,执行iret指令。
int imm8表明int指令的操作数为一个8位立即数。注意指令'int3'和指令'int 3'时等价的。
into是溢出中断指令,机器码为0xCE,也是单字节指令。当处理器执行这条指令时,如果标志寄存器的OF位是1,那么,将产生4号中断。否则,这条指令什么也不做。
使用软中断,我们不需要像调用jmp或call指令那样指明跳转的具体地址,很多系统都选择提供了一组已经定义好的软中断过程供我们调用。这样,系统不仅可以隐藏具体的实现,而且在中断处理过程的地址发生变化时,不需要用户去修改代码。
最后
这里只对实模式下的x86中断机制作了十分浅薄介绍,其它的一些内容(比如RTC等)并没有涉及。同时,这篇文章并没有给出具体的实操代码,笔者希望读者在了解中断的一些原理之后,能自己编写汇编汇编代码并使用调试器,感受一下中断的使用。