目录

计算机组成学习笔记(七)

摘要
计算机组成学习笔记(七)。

8 中断和异常

8.1 中断和异常的来源

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E4%B8%AD%E9%81%87%E5%88%B0%E7%9A%84%E5%BC%82%E5%B8%B8%E6%83%85%E6%B3%81.png
  1. 出现运算结果过大,无法全部保存的情况,这就叫做溢出。
  2. 如果从这个模型机上来看,溢出就会发生在ALU这个运算单元上。这个运算单元的元操作数要么来自于寄存器,要么来自于内存。我们假设都是32位的,对它们进行运算,比如说进行加法, 那么运算的结果就有可能超过32位。那么假设在内存当中的某一条指令是这条运算指令发生了溢出,那么CPU能怎么办呢?
  3. 运算发生了溢出,CPU的硬件电路是很容易检测到的。但问题就在于检测到了溢出之后应该怎么办? 如果完全用硬件电路来处理溢出,那样不够灵活。一旦CPU制造出来我们就没办法改变这个电路,从而没有办法修改处理溢出的方法,所以最好还是用软件的方法来解决。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E7%9A%84%E8%B5%B7%E6%BA%90.png
  1. 这是第一个带有异常处理的计算机系统。如果它在进行运算时发生了异常情况,比如算术运算溢出,它就会转向地址0,执行两条修复指令。
  2. 这个方法的思想是非常重要的。不然的话,一旦出现异常的情况,就只能停机,然后等人工来解决问题以后才能继续运行。而如果我们能在地址0这里存放 几条指令,用于修复这个异常的情况,从而就有可能在修复完成之后继续执行刚才的运算。这样就可以避免停机。
  3. 而且如果之后发现这样的修复方法不太合适,那只需要通过修改存放在地址0的这个地方的指令就可以。用这样的方法来处理异常就非常的灵活和便利。而这种思想也一直延续到现在。现代计算机 的CPU处理异常从根本上来说也是采用了这个方法。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/ALU%E8%BF%90%E7%AE%97%E7%BB%93%E6%9E%9C%E5%92%8CPC%E7%9A%84%E6%9B%B4%E6%96%B0.png
  1. 在ALU进行运算完成之后,硬件电路就很容易检测出这次运算是否发生了溢出。
  2. 所以我们可以在ALU这里增加一根信号线,连到PC的更新部件,那原先这个PC的更新部件会从几个来源对PC进行选择, 要么增加一个固定的值,比如说PC+4,要么是根据转移目标地址产生一个新的PC。
  3. 那么现在增加一种新的情况,就是当运算溢出的信号有效时,这个部件就固定产生0,送到PC寄存器,这样在下一个周期,PC寄存器就会从地址0取出指令进行执行。那么执行若干条指令之后,就可以修复这个异常带来的影响。或者至少标记出了这次运算时错误的,以免对后续造成影响。
  4. 那如果在通过一些方式,可以记住刚才发生异常时的PC的值,那就可以再通过PC更新的部件,将这个值装入PC寄存器当中去,从而从刚才发生异常的地- 方开始继续执行。这就是一个简单的处理运算结果异常的方式。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E9%9C%80%E8%A6%81%E4%B8%AD%E6%96%AD%E7%8E%B0%E6%9C%89%E7%A8%8B%E5%BA%8F%E7%9A%84%E8%BF%90%E8%A1%8C.png
  • 后来的计算机就不仅仅是只进行数学运算,还要与外界进行很多的交互,我们现在的计算机当中有很多的输入输出的部件,那当CPU在运行程序的时候,有可能我们就会在键盘上一个键,或者网络上就会来了一个数据包,这些输入输出的操作都需要CPU中断现有程序的运行来进行处理。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E7%9A%84%E8%B5%B7%E6%BA%90.png
  1. 1954年的DYSEAC是第一个带有外部终端处理的系统。
  2. 和UNEVAC处理异常的方式不太一样,它设置了两个程序计数器,然后根据外部输入输出设备的信号,可以在这两个程序计数器之间进行切换,这样它就可以交叉执行两端不同的程序,可以一段程序是运算的功能,而另一端程序是专门用于处理外部的中断。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/CPU%E9%81%87%E5%88%B0%E7%9A%84%E4%BA%8B%E4%BB%B6.png
  1. CPU在运行的时候遇到的一些事件, 这些事件有可能是线性程序本身出现的,那也有可能是在CPU的外部 甚至是整个系统的外部出现的事件。那么 这时就要求CPU强行中止现在正在运行的程序。并且从一个新的存储器区域启动相应的程序去处理这些事件。
  2. 还有一点也很重要,就是在处理完这些事件之后,CPU需要恢复到原来的程序继续运行。那这些事件就被称为中断或者异常。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.1%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E7%9A%84%E6%9D%A5%E6%BA%90/%E4%BA%8B%E4%BB%B6%E7%9A%84%E5%91%BD%E5%90%8D.png
  • 中断和异常又有什么区别呢? 那其实在这件事情上也没有明确的定论,不同的体系结构,不同的书籍,不同的研究群体,对它们的定义都有区别。

8.2 中断向量表的结构

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E7%9A%84%E8%B5%B7%E6%BA%90.png
  1. 在存储器当中,如果从地址0开始只预留了两条指令的空间用于修复指令。那如果后来要改变修复的方式,需要再增加几条指令。那这里预留的空间是否就有可能不够了。
  2. 另外当需要处理的异常情况变多的时候,那就需要根据异常情况的不同区执行不同的异常处理指令。那如果遇到异常都只能转向地址0,就没有办法处理多种不同的异常了。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/Intel%208086.png
  1. 8086是一个十六位的cpu。它内部有四个16位的通用寄存器。对外则有16根数据线,但是它的地址显要更多一些,一共有20根。这样可以寻址的内存空间就是2的20次方,也就是一兆个byte。
  2. 由于它内部的寄存器与运算器都是16位宽的。要生成20位宽的地址,就得用一定的转换方法。8086采用的是“段加偏移”的方式。
  3. 然后对于这一兆的空间不都是可以任意使用的。有两个区域保留作专门的用途。在这一兆字节的内存空间中, 最低的1K个字节保留作中断向量表区;而最高的十六个字节保留为初始化程序区

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/%E5%AE%9E%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84%E5%AD%98%E5%82%A8%E5%99%A8%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4.png
  1. 8086cpu复位之后,它第一次取指令的操作发出的地址是四个F一个0。这个地址就是在这一兆内存空间的最高的16个字节的这个地方。那这个区域实际上是很小的,只能放很少的几条指令。通常放在这里的是一条无条件的转移指令。转移到内存空间当中的另一个地方。在那个地方存放着后续的系统程序。
  2. CPU复位之后,为什么不从全0的地址开始取址呢?那样看起来岂不是更自然一些。那因为从0开始的地址空间也已经被占用了。这个1K的字节被用作中断向量表区,它一共存放了256个中断向量。每个中断向量占四个字节。这样正好就是1K个字节。
  3. 除了这两块专用的区域,其他区域就可以用来存储一般的程序指令和数据。那在这块区域,还有那些用于进行中断处理的程序。这些程序就被称为中断服务程序。而这些程序代码起始地址则被称为中断服务程序的入口地址。这就是中断程序向量的定义

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F.png
  1. 现在的CPU一般都能够处理多种不同的中断类型。那么每个中断类型就对应一个中断向量,一共4个字节。这四个字节当中,前两个字节用于存放中断服务程序入口的偏移量。而且是低字节在前,高字节在后。
  2. 因此对于这个中断向量, 这两个字节就会被存放到指令指针寄存器中(IP)去。而且前一个字节是寄存器当中的低字节,后一个字节是寄存器当中的高字节。
  3. 中断向量当中的后两个字节,则对应了终端服务器入口的地址的段基址,用来存放到代码段计算器(CS),同样,前面那个字节对应了寄存器当中的低字节,后面的字节对应了寄存器当中的高字节。
  4. 在8086当中或者是后来X86处理器的实模式下,就需要用CS和IP这一对寄存器,来指定一个内存的地址。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/%E6%AE%B5%E5%8A%A0%E5%81%8F%E7%A7%BB%E7%9A%84%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%E4%BA%A7%E7%94%9F%E6%96%B9%E5%BC%8F.png
  1. CS寄存器就是一个段寄存器。它是16位的, 而刚才的IP寄存器则对应了偏移量,也是一个16位。那这两个16位的地址就构成了逻辑地址。通常的表示方式就是用一个冒号分隔开这两个16位数。
  2. 在cpu产生地址是,会将段寄存器当中的数左移4位,然后加上偏移量,这样加法运算的结就是20位的物理地址。这就是逻辑地址生成物理地址的方式。实际上是段基值乘以十六加上偏移量。那对于二进制来说左移四位就相当于乘以16了。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E5%92%8C%E4%B8%AD%E6%96%AD%E6%9C%8D%E5%8A%A1%E7%A8%8B%E5%BA%8F1.png
  • 基于这样的方式,每个中断向量都由两个段基值和两个向量的偏移地址组成,还因为每一个中断向量占四个字节。在整个中断向量表中,一共有256个中断向量。(因为总大小为1KB)分别命名为0号、1号、一直到255号中断。
/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E5%92%8C%E4%B8%AD%E6%96%AD%E6%9C%8D%E5%8A%A1%E7%A8%8B%E5%BA%8F2.png
  1. 假设1号向量的初始值是这样的,那当cpu接收到中断时, 如果发现时1号中断。那因为各个中断向量放置的地址是固定的,那cpu不需要通过执行指令,直接通过硬件电路的设置,就可以发出内存访问来读取这四个字节的内容。然后将其中高两个字节送到CS寄存器当中去, 低两个字节送到IP寄存器当中去。
  2. 那对于8086来说,这两个寄存器的功能 就相当于我们在之前介绍处理器内部结构时,提到的PC寄存器。所以这两个寄存器的值一旦发生改变, 下一个周期cpu就会从那个新的地址开始取下一条指令。
  3. 那根据段加偏移的计算方法,cpu发出的地址均是43006H。因此也就是说,在遇到一号中断时, cpu就会转到43006这个地址开始执行程序。那当然,需要实现把一号中断的服务程序存放在这里。
  4. 与此类似,我们还会把0号中断的服务程序放在存储器的另一个地方。然后将中断程序的起始地址放在段基值和偏移地址,存放在0号中断向量所在的位置。那当cpu遇到中断时,如果发现是0号中断,则会将0号中断向量对应的内容取出,分别填到CS和IP寄存器当中去。这样cpu就会从0号中断符合的起始地址,开始对指令进行执行。
  5. 那我们注意到,这些中断服务程序在内存当中的存放顺序并没有要求。并不需要按照中断类型的顺序。先放0号中断服务程序,再放1号中断服务程序。而是可以随意放置。只需要把它的起始地址存放在中断向量表的对应位置就可以了。这样做就比UNIVAC的方式要灵活的多。一来中断服务程序就可以可长可短, 不用担心在从0开始的地址到底要预留多少的空间才够。二来中断服务程序的存放位置如果发生改变,也没有关系。不需要修改cpu的硬件设计,而只需要修改中断向量表中对应的中断向量就可以了。这样只用初始化好中断向量表, 并在存储器当中准备好对应的中断服务程序。cpu在遇到中断时就可以自动的对应的跳到中断服务程序进行处理了。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.2%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84/8086%E7%9A%84%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8.png
8086的中断向量表

8.3 中断向量表的发展

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/%E5%AE%9E%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84%E5%AD%98%E5%82%A8%E5%99%A8%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4.png
实模式下的存储器地址空间
/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/8086%E7%9A%84%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8.png
8086的中断向量表
/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/80386%20~%20Core2%E7%9A%84%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8.png
80386 ~ Core2的中断向量表
/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E5%AD%98%E6%94%BE%E7%9A%84%E4%BD%8D%E7%BD%AE.png
  1. 这是现在比较流行的个人计算机的内部结构。那它如果运行在实模式下,那就可以认为它是一个非常快的8086。
  2. 因此当CPU复位之后,也会去1兆地址空间的最高的16个byte的位置去取第一条指令。那么这个地址会被南北桥芯片组引导到BIOS芯片。
  3. 那CPU执行BIOS芯片当中的指令,对主板上的各个设备进行基本的配置。其中一项工作,就是在组成地址0的地方构建出中断向量表。那所谓构建中断向量表,也就向主存中最低的那1K的字节填写那些中断向量。那构建好了中断向量表,又准备好了中断服务程序之后,CPU再遇到中断就可以访问主存中的中断向量表,并调用对应的中断服务程序了。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/1A-32%E7%9A%84%E5%AD%98%E5%82%A8%E5%99%A8%E5%AF%BB%E5%9D%801.png
  1. 现在的CPU其实主要不是运行在实模式下,内存也并不是仅仅只有一兆了,所以CPU对存储器的访问方式也发生了变化。
  2. 我们以指令的寻址为例,在实模式下是用代码断计存器CS和指令指针计存器IP进行组合。这两个计存器都是16位的,它们的组合用断加偏移的方式产生一个20位的地址。但是从386开始,指令指针计存器就从16位扩展到了32位。也就是EIP计存器。那么它的寻址能力就有了2的32次方也就是4G个字节单元。而从386开始,32位的CPU对外也就是32位的地址线,能够寻址的范围也是2的32次方。那在这时候,指令指针计存器的宽度和实际需要寻址的范围已经是一一对应的了。
  3. 所以在保护模式下,虽然逻辑地址还是写成CS计存器和EIP计存器的形式,但如果你地址的产生方式已经和实模式完全不同了。在保护模式下,段基址并不是存放 在CS计存器当中,而是存放在内存中的。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/1A-32%E7%9A%84%E5%AD%98%E5%82%A8%E5%99%A8%E5%AF%BB%E5%9D%802.png
  1. 在保护模式下,段基址并不是存放 在CS计存器当中,而是存放在内存中的。
  2. 在内存当中的某个地方,存放着一张表,称为描述服务表。这张表一共有8192个表项,每个表项由8个字节构成,就被称为一个描述符。这个描述符当中,第2,3,4个字节和第7个字节这一共四个字节是基地址,这个地址就对应了实模式下保存在CS计存器当中的内容。
  3. 这个描述符当中,除了基地址之外,还有一些别的内容。例如断界限,它指明了这个段究竟有多长。还有权限,这里面有若干个比特,指明了这个段的内容,是否可读,是否可写等等。
  4. 这些描述符是存放在内存中的, CPU又是如何能访问到呢?所以在CPU当中实际上是修改了CS计存器的使用方式。我们可以想一想为什么总共有8192个描述符?这和CS计存器的宽度是有关系的,CS计存器是16位宽的,所以它一共可以寻址2的16次方个内存单元, 这就是64K。而因为每个描述符是8个字节,所以8192个描述符刚好是64K。因此用CS计存器 就正好可以查找这么多个描述符。
  5. 这个描述符表存放的地址并不是从0开始的,所以想要找到对应的描述符,CPU还得知道这个描述符表存放的起始地址。所以在CPU当中还需要再设置一个新的计存器,叫"GDTR",它用于保存这张描述符表的起始地址。
  6. 这个GDTR的内容是从哪里来的呢? 实际是因为x86的CPU在启动的时候都会先进入实模式。在实模式下,就会在内存的某一个地方,先把这张描述符表都填好,然后将这张表的起始地址填到GDTR计存器当中去。这也是CPU内部的一个计存器。只不过它不象EX,EDX 这些计存器可以用作数据的运算,它是一个特殊的计存器,但也是可以用特定的指令进行访问的。
  7. 在保护模式下,CPU每次要访问存储器,都得先用CS计存器的内容,加上GDTR计存器的内容得到一个地址,用这个地址去访问存储器,然后取出这个描述符,再把这个描述符当中4个字节的基地址提取出来,然后再和指令指针计存器EIP的内容 进行组合,从而得到要访问的存储器地址。然后再用这个地址去访问存储器,得到想要的指令编码。
  8. 既然在保护模式下,每一次存储器的访问都必须要经历这个 过程,那访问中断向量表也就不例外。而且不但如此,中断向量表的位置也发生了变化。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.3%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E8%A1%A8%E7%9A%84%E5%8F%91%E5%B1%95/%E4%BF%9D%E6%8A%A4%E6%A8%A1%E5%BC%8F%E7%9A%84%E4%B8%AD%E6%96%AD%E6%93%8D%E4%BD%9C.png
  1. 在保护模式下,中断向量表也就没有放在地址为0的存储器区域了,而是可以放在内存的任意地方。而且它的名字也有了一些变化,叫作中断描述符表。
  2. 所以现在,在内存的某一个地方,存放的这张中断描述符表 总共有256个描述符。每个描述符是8个字节,这8个字节当中,字节0和字节1,以及字节6,字节7组合起来,一共是32位的地址。而字节2和字节3 则是一个段选择符。
  3. 当CPU发生中断时,还是会根据中断类型号来查找这个中断描述符表。那因为现在中断描述符表的起始地址不是0, 所以CPU必须要先知道这个中断描述符表的起始地址。那个这起始地址也保存在CPU当中的一个计存器, 称为IDTR,就是中断描述符表的地址计存器。这个计存器的内容也需要由系统初始化软件在建立好中断描述符表后填写进去。
  4. 现在,CPU就需要将中断类型号乘以8再加上IDTR计存器中的内容,就可以得到对应的中断描述符的地址。然后把这个描述符取回之后,将段选择符这16位存放到CS计存器当中,然后将地址对应的这 32位存放到EIP计存器当中。
  5. 我们注意这个操作的动作实际上和实模式下是类似的, 只不过实模式下每个中断向量是4个字节。然后把其中两个字节放到CS计存器中,两个字节放到IP计存器中。现在每个中断描述符是8个字节,那我们也是将其中的一部分放到CS计存器中,另一部分放到EIP计存器中。
  6. 在完成了这个动作之后,如果在实模式下,下一个周期就直接可以从新的地址开始取址了。而在保护模式下,则没有那么简单。我们还需要按照刚才讲的那样,用CS计存器和GDTR计存器配合, 去内存中找出对应的段基值,然后再和EIP计存器组合, 才能得到对应的内存地址。而那一个地址,才是我们需要调用的中断服务程序的入口地址。直到那时,CPU才会从中断服务程序的入口 取回指令,真正开始中断的处理。

8.4 中断的处理过程

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E7%9A%84%E6%A3%80%E6%B5%8B.png
  1. 如果在运算时发生了异常的情况,比如出现除法错误,那运算器就会产生相应的中断信号。那如果遇到其他指令产生的中断,就会产生其他对应的控制信号。那这些中断都是和正在运行的程序本身有关的, 所以我们叫它软件中断,或者是内部中断。
  2. 还有来自CPU外部的中断。这些外设有可能是键盘、网络、打印机,等等。那当键盘按键,或者有收发网络包的情况,这些外设的芯片或者板卡就会通过主板上的物理连线,发出中断请求信号, 这些信号最终都会连接到CPU内部。不管来源是哪里,CPU检测到了中断请求信号,就会进行处理。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B1.png
  1. CPU会关闭中断响应,也就是不再接受后面其他的外部中断请求了。注意,只是不接收外部中断请求。
  2. 将发生中断处的这个指令的地址 保存到栈中,也就是内存当中的一个区域,这个信息必须要保存好,以便于处理完中断后可以正确地返回当前的程序继续执行。那究竟是保存发生中断的这条指令的地址, 还是发生中断的这条指令之后的一条指令的地址,和这个中断具体的类型会有关系。
  3. 由CPU识别中断的来源, 确定中断类型号,从而能够找到相应的中断服务程序的入口地址。
  4. 这三步一般都是由硬件自动完成的。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B2.png
  • 第四步是保存现场,也就将中断服务程序中可能会改变的寄存器先压栈,也就是放到内存中保存起来。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%BF%9D%E6%8A%A4%E7%8E%B0%E5%9C%BA%E7%9A%84%E4%BD%9C%E7%94%A8.png
  1. 在这个主程序片段中, 我们假设执行到这条触发指令的时候发生了中断,那这时就会调用中断服务程序。
  2. 假设在这中断服务程序当中,有这么一条指令, 它会改写CX寄存器的值。那如果我们没有对CX寄存器进行保护的话,在中断返回之后,CX寄存器的内容就已经改变了。然后回到主程序继续往下执行时,这个CX寄存器 的内容,就不再是主程序中之前传到CX寄存器中的内容了。这样就会导致主程序的错误。
  3. 所以在中断服务程序中,如果要用到CX寄存器,就应该先进行压栈,把当前CX寄存器的值保存到存储器中,然后在中断返回之前,再执行弹栈的操作。这样中断返回之后,CX寄存器的内容就没有变化,不会影响主程序的运行。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B3.png
  • 第五步,就是执行这个中断服务程序的主体内容。而且在中断服务程序中,可以在适当的时候重新开放中断, 以便响应其他高优先级的外部中断。以免中断服务程序本身要执行比较长的时间,造成有高优先级的外部中断长时间无法得到响应。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E8%BD%AF%E4%BB%B6%E5%BC%80%E6%94%BE%E5%92%8C%E5%85%B3%E9%97%AD%E4%B8%AD%E6%96%AD%E5%93%8D%E5%BA%94%E7%9A%84%E6%96%B9%E6%B3%95.png
  1. 如何在中断服务程序当中,重新开放中断呢?那这就涉及到对标志寄存器的操作。
  2. 在标志寄存器当中有一些标志位是状态标志。状态标志一般情况下,是由硬件设置,然后由软件读取。例如进位标志,就是在运算器产生进位时,由硬件自动设置,然后由软件的指令读取出来,可能会进行相关的累加操作。
  3. 而另一类标志称为控制标志,这些标志通常是由软件进行设置,然后硬件电路根据这些标志设置的不同,而执行不同的功能。那么标志寄存器的第九位就是中断标志。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E6%8E%A7%E5%88%B6%E6%A0%87%E5%BF%97%E7%9A%84%E5%8A%9F%E8%83%BD%E7%AE%80%E8%BF%B0%28IF%29.png
  1. 那外部 中断分为两大类,一类是可屏蔽中断,一类是非屏蔽中断。IF标志只对外部中断当中的可屏蔽中断起作用。
  2. 如果IF等于1,那就允许CPU响应可屏蔽中断请求; 如果IF等于0,那就不允许CPU响应可屏蔽中断请求。
  3. 怎么设置IF标志位的值呢?有两条 指令。STI指令就是把IF位置为1,而CLI指令就是把IF标志位清零。这两条指令都是没有操作数的指令。当然,IF指令对非屏蔽中断和内部中断都是不起作用的。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B4.png
  • 中断处理过程的最后一步,那就是恢复现场并且返回主程序继续运行。那返回主程序就需要执行中断返回指令。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E5%A6%82%E4%BD%95%E4%BB%8E%E4%B8%AD%E6%96%AD%E6%9C%8D%E5%8A%A1%E7%A8%8B%E5%BA%8F%E4%B8%AD%E8%BF%94%E5%9B%9E.png
  1. 中断返回指令它可以不带操作数,那么这条指令的操作,就是从当前的栈顶弹出三个字, 分别送到IP、CS和FLAGS寄存器当中去。那么这条指令是放在中断服务程序的末尾,那么在中断调用时,CPU中的硬件电路会将这三个寄存器的值 压入栈中,所以在执行中断返回指令时,也会由硬件从栈顶弹出这三个字,再放回到这三个寄存器当中。那当CS和IP寄存器改变之后, 下一条指令就会回到程序发生中断的地方继续执行了。
  2. 这条中断返回指令操作的是CS和IP寄存器,那在32位和64位的x86中,这个指令 指针寄存器,又被扩展为EIP和RIP寄存器,它们的宽度都是不一样的,所以后来又有了不同的对应的指令。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B5.png
  1. 当CPU执行到某条指令,例如就是这条指令。
  2. 如果此时发生中断,CPU内部就会产生中断信号。
  3. 相关的中断处理电路会判断中断的来源,并产生中断类型号。
  4. CPU的硬件电路会将CS和IP寄存器压栈, 这样就保存好了处理完中断后要返回的地址。同时硬件上还会将FLAGS寄存器压栈,以便保存好当前的各项标志,以免中断处理程序当中,有些指令会改变程序的标志位。
  5. 在硬件上还会清除IF标志位,以起到关中断的作用。
  6. 然后根据中断类型号,找到对应的中断向量,也就是新的CS和IP的值,并以此更新CPU当中的CS和IP寄存器。
  7. 完成这个操作后,CPU就会转到中断服务程序开始执行。
  8. 那么在中断服务程序中,也可以执行STI指令,以开放中断。
  9. 当完成了中断服务程序之后, 最后一条就是执行中断返回指令。
  10. 这条指令会从存储器当中将刚才压栈的三个字弹出来, 并按照对应的顺序,存到CS、IP和FLAGS寄存器当中去,这样就完成了返回主程序的动作。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.4%E4%B8%AD%E6%96%AD%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B/%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B%E7%9A%84%E4%BB%BB%E5%8A%A1%E5%88%86%E5%B7%A5.png
  1. 这就是中断处理过程的六个主要的步骤。
  2. 通常情况下前三步都是由处理中断的硬件电路来完成, 后三部则是由软件,也就是中断服务程序来完成。
  3. 这只是一个大体的分工,在真的要涉及中断服务程序的时候,必须要针对具体的系统,弄清楚软硬件的分工究竟是怎么样的。
  4. 例如在保存现场这件事情上,刚才我们介绍的,标志寄存器是由硬件来负责保存的,但是在另外的一些系统上,可能硬件就不会自动地保存标志寄存器, 就需要由中断服务程序的软件来进行保存。那么在设计中断服务程序时,必须要搞清楚这些问题,不然就有可能发生错误。

8.5 内部中断分类说明

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%C3%9786%E5%AE%9E%E6%A8%A1%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%9A%84%E4%B8%AD%E6%96%AD%E6%9D%A5%E6%BA%90.png
×86实模式系统的中断来源
/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%EF%BC%88%E8%BD%AF%E4%BB%B6%E4%B8%AD%E6%96%AD%EF%BC%89.png
  • 在x86的实模式下,我们要来分析的内部中断就是这四个。那这四个内部中断所使用的类型号,分别是0、1、3、 4,而加在中间的类型2,是留给外部中断的。这个非屏蔽中断是外部中断的一种。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%EF%BC%88%E7%B1%BB%E5%9E%8B0%EF%BC%89.png
  1. 在这个除法器支持的运算中,是有一个64位的被除数,和一个32为的除数,运算产生的商和余数,分别放到两个32位寄存器当中去, 那这样的设置对于多数除法运算来说,都是没有问题的。
  2. 但难免有一些比较特别的情况, 例如除数很小,比如说就是2,而被除数很大,那么一个很大的被除数除以2之后,得到的商就会超过32位,没有办法放进这个商寄存器当中去。当运算器遇到这种情况时,就会产生一个除法错中断,这个中断的类型号是0,所以CPU就会去中断向量表中,取出0号中断向量,然后去执行对应的中断服务程序。
  3. 那由于这种情况,就是用0作除数, 这样得到的商应该是无穷大,肯定超过目标寄存器所能表示的范围,所以这个除法错中断有时也会被称为除0中断。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%EF%BC%88%E7%B1%BB%E5%9E%8B4%EF%BC%89.png
  1. 再来看4号中断,这个中断叫作溢出中断,也就是因为算术运算发生了溢出,而引起的中断。
  2. 这个中断的产生要借助一条特殊的指令,也就INTO指令,当执行这条指令时,硬件电路会去检查溢出标志位OF是否位1, 如果为1,则会引起类型为4的内部中断。
  3. 这条指令的格式是这样的,就是INTO,它是一个没有操作数的指令,比如这条加法指令,在执行时,就有可能发生了溢出,那么运算器 在运行完这个加法后,会去设置标志寄存器当中的标志位,也就是第11号溢出标志位, 但这个操作本身并不会引发中断,只是将标志位置1,但如果之后执行了INTO指令,这条指令是会去检查OF标志位,如果这时OF标志位为1,那就引起了4号中断,但是如果INTO指令执行时,OF标志位为0,那就什么也不会发生,这条INTO指令就相当于一条空操作指令。
  4. 所以INTO指令通常会安排在算术运算指令之后,用来检查这个运算是否发生了溢出,并且, 在发生溢出时,就调用中断程序进行处理,因为这是4号中断,所以我们也可以写成INT 4这样的形式。注意INT和4之间有一个空格,这个4是INT指令的操作数。实际上,任何一个类型的中断,都可以采用这样的形式进行调用,用INT指令带上这个中断的类型号。
  5. 要注意区分的是,这个4号中断, 和刚才介绍的0号中断,在引起中断的时机上是有区别的,虽然它们都在检查运算时出现的异常情况, 但是0号中断,是在那条除法指令执行后,立刻发生的,而4号中断则是要在编程时,加入INTO指令进行主动的检查,因为很多时候,这样的加法运算的溢出,并不需要进行处理, 如果每一次溢出,都要引发中断,反而可能影响程序的性能,所以在指令系统设计的时候,就把是否要检查这种溢出的情况,交给程序员来进行判断。
  6. 那么类型0和类型4这两个中断, 都是和运算结果出现了异常情况有关系的,而另两个内部中断,则是主要用来进行错误调试的。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%EF%BC%88%E7%B1%BB%E5%9E%8B1%EF%BC%89.png
  1. 类型1中断称为单步中断,要引发这个中断,需要将标志寄存器当中的TF位置1,这时CPU就处于单步工作方式。
  2. 在单步工作方式下,CPU每执行完一条指令,就会自动的产生一个类型1的中断,然后进入类型1中断服务程序。
  3. 这种工作方式主要是用来进行错误调试的,
  4. 可以将TF标志位置为1,在单步工作方式下进行调试。通常情况下,我们会在这个类型1的中断服务程序当中, 将CPU当中的各个寄存器的内容,在屏幕上显示出来,这样CPU每执行一条指令,我们就可以在屏幕上看见CPU当前正在执行的,是哪一条指令,这条指令的地址是什么,执行这条指令的前后,那些通用寄存器又有什么样的变化,这样我们就有可能发现,到底在哪一步,发生了不符合我们预期的行为。
  5. 这个方式对于调试是很有用的,但是CPU每执行完一条指令,就要产生一个中断, 那程序执行的速度,就是非常慢的,如果想要调试一个很大的程序,仅用单步中断就会变得比较困难。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%EF%BC%88%E7%B1%BB%E5%9E%8B3%EF%BC%89.png
  1. 还有一个用于调试的中断,就是类型3,断点中断。
  2. 断点中断通常和单步中断配合使用, 在调试一个很大的程序时,一般我们会先通过断点中断,将错误定位在这个程序的一小段代码中,然后,再对这一小段代码,用单步的方式,进行跟踪调试, 这样就可以大大提升调试的效率。
  3. 这个思想也是很简明的,如果我一个大的程序运算结果出现问题,我们并不会 马上从这个程序的开头,一条一条指令的顺序检查,而通常会是将这个程序,切成几个大的部分,然后检查每一个部分的结果是否正确。
  4. 我们刚才提过,所有的中断,都可以用指令的形式来调用,那么用INT加中断类型号的这个形式的指令,都是一个两字节的指令,只有断点中断, 是一个例外,INT 3指令是一条单字节长的指令,这就是INT 3指令的编码,11001100。
  5. 为什么INT n形式的指令都是两个字节的呢?我们只用想一想这个n要表达多大的范围,我们一共有256个中断类型,所以这个n要表示0到255, 那要表示这些数,需要多少个二进制位呢?需要8位,对吧,2的8次方,就是256, 那在前面,还得有一个字节的指令操作码,所以总共是两个字节。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E6%96%AD%E7%82%B9%E4%B8%AD%E6%96%AD%E7%9A%84%E4%BD%BF%E7%94%A8.png
  1. 为什么要单独给3号中断,设置一个单字节长的指令编码呢? 那这和它的使用方式是有关系的。
  2. 这个断点中断指令的使用,并不那么简单, 我们要在需要调试的程序当中,选择一个希望中断的位置,然后用这条断点中断指令,去代替这个位置原有的指令, 当然,我们需要把原有的这条指令保存起来。这个都是要由调试人员手工来完成的。
  3. 替换完以后,我们再次运行这个程序,那用户程序运行到我们选好的这个中断点的时候,那它就执行了INT 3这条指令,从而进入了对应的中断服务程序, 那我们就可以在这个中断服务程序当中,去将CPU的各个寄存器的值,都打印在屏幕上,从而判断执行到这个断点的时候,这个用户程序是否还运行正常。
  4. 如果运行正常,可能我们就需要把这个断点在往后挪一挪,那如果这个时候,已经由寄存器的值不符合我们的预期了, 那我们就需要将断点,放到更靠前的位置,进行进一步的检查。
  5. 但我们还得记得,在这个中断服务程序当中, 需要将这个断点位置,与原有的那条指令的编码,再替换回去,并且将指令指真寄存器的值,再回退一个字节,也就是指向这个原有的指令,以保证中断返回之后,CPU能从断点的这个地方继续执行。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E6%96%AD%E7%82%B9%E7%9A%84%E8%AE%BE%E7%BD%AE%E7%A4%BA%E4%BE%8B.png
  1. 我们来看一个例子,假设这是我们要调试的一段程序,这里有5条指令,左边是它们对应的指令的地址,其中有一些是两个字节的指令,有一些是一个字节的指令。
  2. 如果我们想选择这条INC指令,作为断点, 那么就需要把这条指令的编码,替换成INT 3指令的编码, 这时候就体现出了INT 3这条指令,是一个单字节指令的好处,因为x86的指令当中,最短的就是一个字节的指令,就像这条INC指令,那如果断点中断指令,是一个两字节的指令,那么在替换进来之后,就会影响到后续的指令,而后续的指令,却有可能在这个断点之前执行。
  3. 比如说就像这段程序代码,在这个断点之前,就有一个转移指令,直接跳到了断点之后,然后经过条件判断,可能又跳转回来,才继续执行到断点的地方,所以我们将这条INC指令替换成断点中断指令的时候,一定不能影响后续的指令,这也就是为什么断点中断指令必须要是一个字节的。
  4. 那么在这段程序执行的过程中,如果这个条件已经发生了,那就会运行到断点中断指令,然后CPU内部就会发生中断, 转而去执行3号中断向量所对应的中断服务程序,那在这个中断服务程序中,我们就可以把AL寄存器的内容打出来,这样调试人员就可以观察到,这个时候AL寄存器的内容,是否符合我们的预期了。
  5. 如果我们发现AL寄存器的内容有错误,那么就可以再次运行这个程序,并在附近的位置设置CPU进入单步工作模式,进行单步调试, 这样就会比较容易的发现一些隐藏的很深的错误。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.5%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E5%88%86%E7%B1%BB%E8%AF%B4%E6%98%8E/%E5%86%85%E9%83%A8%E4%B8%AD%E6%96%AD%E7%9A%84%E7%89%B9%E7%82%B9.png
  1. 首先,它们的中断类型号是由CPU内部产生的,因为这些异常的情况,就是CPU自己在执行指令的过程中发生的,所以它是知道到底发生的是什么类型的中断, 而我们后面要介绍的外部中断,则有可能来自不同的外部设备,所以CPU需要去读取外设,以得知中断类型号,这是第一个区别。
  2. 第二个区别,是屏蔽的方式,那么在内部中断当中,除了单步中断以外,都不可以用软件的方式来进行屏蔽,也就是,我们不可以通过设置IF这个标志位,来让CPU不响应内部中断。
  3. 第三个是优先级,也就是内部中断 和外部中断同时发生时,CPU先处理哪个中断,那么除了单步中断以外,所有的内部中断优先级,都比外部中断高。CPU总是优先处理自己内部发生的异常情况。

8.6 基于中断的功能调用

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.6%E5%9F%BA%E4%BA%8E%E4%B8%AD%E6%96%AD%E7%9A%84%E5%8A%9F%E8%83%BD%E8%B0%83%E7%94%A8/INT%E6%8C%87%E4%BB%A4%E8%AF%B4%E6%98%8E.png
  1. 在x86指令系统当中,其实还提供了一条中断指令, 它的格式是INT,加上一个操作数n,用这条指令,就可以直接调用对应的中断服务程序, 这个n是0到255当中的一个数,对应的中断类型码。
  2. 当CPU执行到指令时,首先会将标志寄存器压栈,然后清除IF和TF两个标志位,也就是关中断。
  3. 再将CS和IP两个寄存器的内容压栈, 然后根据指令中提供的类型码,也就是这个n,去查找中断向量表,找到对应的中断服务程序的入口地址。
  4. 再将入口地址装到CS和IP寄存器当中去,这样下一步CPU就会到中断服务程序的入口取出下一条指令,继续执行。
  5. 看到这一个步骤,和我们之前介绍的,CPU处理中断的过程是一样的, 但是区别在于,这是由运行指令主动触发的。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.6%E5%9F%BA%E4%BA%8E%E4%B8%AD%E6%96%AD%E7%9A%84%E5%8A%9F%E8%83%BD%E8%B0%83%E7%94%A8/%E4%B8%AD%E6%96%AD%E5%90%91%E9%87%8F%E5%92%8C%E4%B8%AD%E6%96%AD%E6%9C%8D%E5%8A%A1%E7%A8%8B%E5%BA%8F.png
  1. 之前CPU当中,如果发生的1号中断, CPU就会来中断向量表当中,取出1号中断向量,然后转向对应的中断服务程序开始执行。
  2. 如果当前CPU执行程序的过程当中,并没有满足触发1号中断的条件,而是直接写了一条指令,INT 1,CPU也会到中断向量表当中,取出1号中断向量, 装入CS,IP寄存器,再转到1号中断服务程序,开始执行。
  3. 那这样做有什么意义呢?明明没有发生中断,我们为什么要调用这个中断服务程序, 那这种中断指令的使用场景其实有两类,一类就是CPU的一些专用的中断,就是需要通过调用指令的方式来实现的, 比如说3号中断,那就得写INT 3这样的指令,才可以产生。另外一种情况,我们称为BIOS中断。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.6%E5%9F%BA%E4%BA%8E%E4%B8%AD%E6%96%AD%E7%9A%84%E5%8A%9F%E8%83%BD%E8%B0%83%E7%94%A8/BIOS%E4%B8%AD%E6%96%AD.png
  1. BIOS中断,BIOS就是基本输入输出系统, 这是一套不大但是还挺复杂的程序,存放在ROM当中, 寄存器在刚通电或者复位时,CPU就会从BIOS ROM当中取出第一条指令开始执行,所以在BIOS当中会提供系统加电自检,和主要的输入输出设备处理程序,等功能模块。
  2. 所谓的功能模块,也就是一个一个具有独立功能的程序, 比如从键盘接收一个字符,或是在显示器上显示一个字符,这些工作不但在系统初始化时需要使用,在后来用户使用计算机的过程中,其实也会用到,而如果要用户自己去编写,处理这些输入输出设备的程序,那就太麻烦了,所以BIOS的设计者,就将其中的这些功能各个包装起来,形成了很多个独立的功能模块,然后将这些独立功能模块的入口地址,放在中断的向量表当中,那如果我们想使用这些功能,比如说就像在显示器上显示一个字符,用户就不用去关心,到底用的是什么显示器,要占用多少个像素,等等。那只需要使用BOIS提供的功能模块,用INT指令调用对应的中断服务程序就可以了。 而且我们可能希望,向这些功能模块传递一些参数,那就可以通过寄存器进行传递。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.6%E5%9F%BA%E4%BA%8E%E4%B8%AD%E6%96%AD%E7%9A%84%E5%8A%9F%E8%83%BD%E8%B0%83%E7%94%A8/%E8%B0%83%E7%94%A8BIOS%E4%B8%AD%E6%96%AD.png
  1. 想使用BOIS中断,首先就得查找BIOS中断的手册,这个手册一般会提供这样一个表格,列出了BIOS这些功能模块所在用的中断号,比如说10H,就是用于在显示器上进行显示的一个中断服务程序,而1AH,则是设置系统时钟的一个中断服务程序。
  2. 我们以1AH为例,那如果我们想要改变现在的系统时钟,当然我们可以去分析时钟管理芯片的功能,通过查找它的手册,来分析如何去改变系统时钟的设置,这可能要花很多个步骤。那BIOS的设计者,就帮我们封装好了这个功能, 我们只要这样写代码就可以了。
  3. 因为我们通过这个表可以看到,如果要设置时钟,我们需要提供一个功能号为1,因为1AH这个中断里头,其实有多种功能, 我们可能想读出当前时钟的值,也可能要改变当前时钟的值,这个中断服务程序怎么识别呢,它就要求你在AH寄存器当中放入一个数,那么在中断服务程序的开始,会先检查AH寄存器,如果里面是0,那它就按照读时钟的操作,运行后续的代码,如果AH里面的值是1,它就按照设置时钟的操作,执行后续的代码。
  4. 那现在我们要设置时钟,所以在AH里面先放上1,然后我们查这张表,知道我们要设置的时间是放在这几个寄存器当中,CH放在要设置的小时数,CL放要上要设的分钟数,DH是秒,DL是百分之一秒,而CL和CH组成的寄存器是CX,DL和DH组成的寄存器是DX,所以我们直接可以通过对CX和DX赋值,来设置这个时间, 那么现在为了简单,我们就设成0点0分0秒,这些参数准备好以后,我们最后写INT 1AH, 接下来就像是之前介绍过,发生中断的时候一样,CPU会去中断向量表当中,找到1AH对应的中断向量,然后转移到对应的 中断服务程序开始执行,而这段中断服务程序就是位于BIOS所在的存储区域,那在这个中断服务程序当中,就会去操作管理 系统时钟的芯片或者部件,完成时钟的更改,然后再返回到这个主程序当中,继续执行下面的代码。那这样就把一个和底层硬件细节非常相关的操作给 封装起来,让编程人员可以比较轻松的完成这样的工作。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.6%E5%9F%BA%E4%BA%8E%E4%B8%AD%E6%96%AD%E7%9A%84%E5%8A%9F%E8%83%BD%E8%B0%83%E7%94%A8/DOS%E4%B8%AD%E6%96%AD.png
  1. DOS早起的一种操作系统,它占用了一个中断类型号,21H,和BIOS占据了多个中断类型号不同,DOS中断只有这一个类型号, 但它的功能非常的丰富,常用的文件管理、存储管理等很复杂的功能,都可以种这个中断服务程序来解决。
  2. 怎么区分我们到底想使用哪个功能呢?其实刚才BOIS中断给我们提供了这样的思路,我们可以通过一个寄存器,传入一个参数,来告诉中断服务程序,我们到底想调用那样的功能,所以所有的DOS中断都只使用这一个中断入口。
  3. DOS是一个操作系统,它所提供的中断功能,比BIOS中断更为齐全、完整, 而且进一步品比了设备的物理特性,让编程的使用,变得更加的方便。

/images/Computer_Organization/8%E4%B8%AD%E6%96%AD%E5%92%8C%E5%BC%82%E5%B8%B8/8.6%E5%9F%BA%E4%BA%8E%E4%B8%AD%E6%96%AD%E7%9A%84%E5%8A%9F%E8%83%BD%E8%B0%83%E7%94%A8/%E8%B0%83%E7%94%A8DOS%E4%B8%AD%E6%96%AD.png
  1. 如果我们想在屏幕上输出一个 $字符,那我们可以查找DOS中断所提供的表格,DOS中断都是21H,所以这个表里面只需要列出功能号。
  2. 那么发现,6号功能是在进行输入输出的操作,所以我们现在AH寄存器当中,存入6。
  3. 然后我们进一步发现,如果我们想输出一个字符,就在DL寄存器当中,放入我们想显示的这个字符,而如果我们想通过键盘输入这个字符,则只需要在DL寄存器当中,存入FF,而最后输入的字符,会放在AL寄存器当中。
  4. 那我们现在还是来看输出,所以我们在DL寄存器当中,存上$这个字符, 然后调用INT 21H, 这样CPU就会转向21H号中断所对应的中断服务程序,在这个服务程序当中,首先会检查AH里面的值,确定功能号, 然后就进入到这个功能对应的程序代码段,再根据DL寄存器的内容,判断出这是一次输出,那这个服务程序接下来就会对显示器进行操作,让对应的字符显示在屏幕的合适的位置。
  5. 那这些繁琐的工作,都不需要用户来关心了,只要简单的调用这个DOS中断就可以了。当然前提是,这些操作都已经由其它的程序员帮你写好, 并且封装起来,还安装在了你这个电脑上,这样你才可以使用,归根到底,这些操作都还是要有人来写出程序, 只不过一次写好之后,其他的用户就可以直接调用了,这样会很方便。
  6. 所以说无论是BIOS中断,还是DOS中断,或者是其它类似的中断方式,其本质并不是计算机运行当中发生了异常的情况, 而是利用了现有的中断这种机制,来实现一些系统函数、代码的调用,以便向高层的软件屏蔽底层硬件的细节, 从而提高编程的便利性,正确性,和可一致性。