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 用来调试和管理用的中断。