805-内部中断分类说明
# 805-内部中断分类说明
现在,我们已经知道了中断处理的基本过程, 那么就来花一点时间,看一看内部中断到底有哪些不同的类型。 我们还是以x86的实模式为例,这个比较简单,但基本原理都是一样的。
在x86的实模式下,我们要来分析的内部中断就是这四个。
那这四个内部中断所使用的类型号,分别是0、1、3、 4,而加在中间的类型2,是留给外部中断的。 这个非屏蔽中断是外部中断的一种。 那么现在,就分别来看一看这四个内部中断
首先来看类型0的中断。 这个中断是和除法有关的, 我们不妨来回顾一下除法器的结构,在这个除法器支持的运算中, 是有一个64位的被除数,和一个32位的除数, 运算产生的商和余数,分别放到两个32位寄存器当中去, 那这样的设置对于多数除法运算来说,都是没有问题的。 但难免有一些比较特别的情况, 例如除数很小,比如说就是2,而被除数很大,那么一个很大的被除数除以2之后,得到的商就会超过32位,没有办法放进这个商寄存器当中去。
当运算器遇到这种情况时,就会产生一个除法错中断,这个中断的类型号是0, 所以CPU就会去中断向量表中,取出0号中断向量,然后去执行对应的中断服务程序, 那由于这种情况,就是用0作除数, 这样得到的商应该是无穷大,肯定超过目标寄存器所能表示的范围, 所以这个除法错中断有时也会被称为除0中断。
然后我们再来看4号中断,这个中断叫作溢出中断, 也就是因为算术运算发生了溢出,而引起的中断, 这个中断的产生要借助一条特殊的指令,也就INTO指令,当执行这条指令时, 硬件电路会去检查溢出标志位OF是否位1, 如果为1,则会引起类型为4的内部中断。 那这条指令的格式是这样的,就是INTO,它是一个没有操作数的指令, 比如这条加法指令,在执行时,就有可能发生了溢出,那么运算器在运行完这个加法后,会去设置标志寄存器当中的标志位, 也就是第11号溢出标志位, 但这个操作本身并不会引发中断,只是将标志位置1, 但如果之后执行了INTO指令,这条指令是会去检查OF标志位, 如果这时OF标志位为1,那就引起了4号中断, 但是如果INTO指令执行时,OF标志位为0,那就什么也不会发生, 这条INTO指令就相当于一条空操作指令, 所以INTO指令通常会安排在算术运算指令之后, 用来检查这个运算是否发生了溢出,并且, 在发生溢出时,就调用中断程序进行处理, 因为这是4号中断,所以我们也可以写成INT 4这样的形式。 注意INT和4之间有一个空格,这个4是INT指令的操作数。
实际上,任何一个类型的中断,都可以采用这样的形式进行调用, 用INT指令带上这个中断的类型号,那我们在后面还会看到这样的例子。 那我们要注意区分的是,这个4号中断, 和刚才介绍的0号中断,在引起中断的时机上是有区别的, 虽然它们都在检查运算时出现的异常情况, 但是0号中断,是在那条除法指令执行后,立刻发生的, 而4号中断则是要在编程时,加入INTO指令进行主动的检查,因为很多时候,这样的加法运算的溢出,并不需要进行处理, 如果每一次溢出,都要引发中断,反而可能影响程序的性能, 所以在指令系统设计的时候,就把是否要检查这种溢出的情况,交给程序员来进行判断。
那么类型0和类型4这两个中断, 都是和运算结果出现了异常情况有关系的, 而另两个内部中断,则是主要用来进行错误调试的。
其中,类型1中断称为单步中断,要引发这个中断, 需要将标志寄存器当中的TF位置1,这时CPU就处于单步工作方式。 在单步工作方式下,CPU每执行完一条指令, 就会自动的产生一个类型1的中断,然后进入类型1中断服务程序。
这种工作方式主要是用来进行错误调试的, 比如说,你发现CPU执行一段程序有错误,但是又不清楚这个错误具体发生在什么地方, 那就可以将TF标志位置为1,在单步工作方式下进行调试。 通常情况下,我们会在这个类型1的中断服务程序当中, 将CPU当中的各个寄存器的内容,在屏幕上显示出来, 这样CPU每执行一条指令,我们就可以在屏幕上看见CPU当前正在执行的,是哪一条指令, 这条指令的地址是什么,执行这条指令的前后,那些通用寄存器又有什么样的变化, 这样我们就有可能发现,到底在哪一步,发生了不符合我们预期的行为。
这个方式对于调试是很有用的,但是CPU每执行完一条指令,就要产生一个中断, 那程序执行的速度,就是非常慢的, 如果想要调试一个很大的程序,仅用单步中断就会变得比较困难, 所以还有一个用于调试的中断,就是类型3,断点中断。 断点中断通常和单步中断配合使用, 在调试一个很大的程序时,一般我们会先通过断点中断,将错误定位在 这个程序的一小段代码中,然后,再对这一小段代码,用单步的方式,进行跟踪调试, 这样就可以大大提升调试的效率,这个思想也是很简明的, 如果我一个大的程序运算结果出现问题,我们并不会马上从这个程序的开头,一条一条指令的顺序检查, 而通常会是将这个程序,切成几个大的部分,然后检查每一个部分的结果是否正确。 我们刚才提过,所有的中断,都可以用指令的形式来调用, 那么用INT加中断类型号的这个形式的指令,都是一个两字节的指令,只有断点中断, 是一个例外,INT 3指令是一条单字节长的指令,这就是INT 3指令的编码, 11001100。
那为什么INT n形式的指令都是两个字节的呢?我们只用想一想这个n要表达多大的范围, 我们一共有256个中断类型,所以这个n要表示0到255, 那要表示这些数,需要多少个二进制位呢? 需要8位,对吧,2的8次方,就是256, 那在前面,还得有一个字节的指令操作码,所以总共是两个字节。
那我们为什么要单独给3号中断,设置一个单字节长的指令编码呢? 那这和它的使用方式是有关系的, 这个断点中断指令的使用,并不那么简单, 我们要在需要调试的程序当中,选择一个希望中断的位置, 然后用这条断点中断指令,去代替这个位置原有的指令, 当然,我们需要把原有的这条指令保存起来。 这个都是要由调试人员手工来完成的, 替换完以后,我们再次运行这个程序,那用户程序 运行到我们选好的这个中断点的时候,那它就执行了INT 3这条指令, 从而进入了对应的中断服务程序, 那我们就可以在这个中断服务程序当中,去将CPU的各个寄存器的值,都打印在屏幕上, 从而判断执行到这个断点的时候,这个用户程序是否还运行正常。 那如果运行正常,可能我们就需要把这个断点在往后挪一挪, 那如果这个时候,已经由寄存器的值不符合我们的预期了, 那我们就需要将断点,放到更靠前的位置,进行进一步的检查。 但我们还得记得,在这个中断服务程序当中, 需要将这个断点位置,与原有的那条指令的编码,再替换回去, 并且将指令指真寄存器的值,再回退一个字节, 也就是指向这个原有的指令,以保证中断返回之后,CPU能从断点的这个地方继续执行。
我们来看一个例子,假设这是我们要调试的一段程序,这里有5条指令, 左边是它们对应的指令的地址,其中有一些是两个字节的指令,有一些是一个字节的指令。 那如果我们想选择这条INC指令,作为断点, 那么就需要把这条指令的编码,替换成INT 3指令的编码,
这时候就体现出了INT 3这条指令,是一个单字节指令的好处, 因为x86的指令当中,最短的就是一个字节的指令,就像这条INC指令, 那如果断点中断指令,是一个两字节的指令,那么在替换进来之后,就会影响到后续的指令, 而后续的指令,却有可能在这个断点之前执行,比如说就像这段程序代码JMP 201H, 在这个断点之前,就有一个转移指令,直接跳到了断点之后,然后经过条件判断,可能又跳转回来, 才继续执行到断点的地方,所以我们将这条INC指令替换成断点中断指令的时候, 一定不能影响后续的指令,这也就是为什么断点中断指令必须要是一个字节的。
那么在这段程序执行的过程中,如果这个条件转移发生了, 那就会运行到断点中断指令,然后CPU内部就会发生中断, 转而去执行3号中断向量所对应的中断服务程序, 那在这个中断服务程序中,我们就可以把AL寄存器的内容打出来, 这样调试人员就可以观察到,这个时候AL寄存器的内容,是否符合我们的预期了, 如果我们发现AL寄存器的内容有错误,那么就可以再次运行这个程序, 并在附近的位置设置CPU进入单步工作模式,进行单步调试, 这样就会比较容易的发现一些隐藏的很深的错误。
那么介绍的这些内部中断,都有共同的特点。首先,它们的中断类型号是由CPU内部产生的,因为这些异常的情况,就是CPU自己在执行指令的过程中发生的, 所以它是知道到底发生的是什么类型的中断
而我们后面要介绍的外部中断,则有可能来自不同的外部设备, 所以CPU需要去读取外设,以得知中断类型号, 这是第一个区别。第二个区别,是屏蔽的方式, 那么在内部中断当中,除了单步中断以外,都不可以用软件的方式来进行屏蔽, 也就是,我们不可以通过设置IF这个标志位,来让CPU不响应内部中断。
第三个是优先级,也就是内部中断 和外部中断同时发生时,CPU先处理哪个中断, 那么除了单步中断以外,所有的内部中断优先级,都比外部中断高。 CPU总是优先处理自己内部发生的异常情况。
现在,我们已经了解了内部中断的基本类型, 有两个是用来处理运算的异常情况,还有两个是CPU用来调试的, 那后来,随着内部中断的类型不断的增加,其中增长的大部分, 都是CPU用来调试和管理用的中断。