内中断
任何一个通用的 CPU,比如 8086,都具备一种能力,可以在执行完当前正在执行的指令之后,检测到从 CPU 外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,我们可以称其为:中断信息。中断的意思是指,CPU 不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
注意,我们这里所说的中断信息,是为了便于理解而采用的一种逻辑上的说法。它是对几个具有先后顺序的硬件操作所产生的事件的统一描述。“中断信息”是要求 CPU 马上进行某种处理,并向所要进行的该种处理提供了必备的参数的通知信息。
12.1 内中断的产生
当 CPU 的内部有什么事情发生的时候,将产生需要马上处理的中断信息呢?对于 8086CPU,当 CPU 内部有下面的情况发生的时候,将产生相应的中断信息。
(1)除法错误,比如,执行 div 指令产生的除法溢出;
(2)单步执行;
(3)执行 into 指令;
(4)执行 int 指令。
这四种情况代表着不同的中断信息,既然是不同的信息,就需要进行不同的处理。要进行不同的处理,CPU 首先要知道,所接收到的中断信息的来源。所以中断信息中必须包含识别来源的编码。8086CPU 用称为中断类型码的数据来标识中断信息的来源。中断类型码为一个字节型数据,可以表示 256 种中断信息的来源。以后,我们将产生中断信息的事件,即中断信息的来源,简称为中断源,上述的 4 种中断源,在 8086CPU 中的中断类型码如下。
(1)除法错误:0
(2)单步执行:1
(3)执行 into 指令:4
(4)执行 int 指令,该指令的格式为 int n,指令中的 n 为字节型立即数,是提供给 CPU 的中断类型码。
12.2 中断处理程序
CPU 收到中断信息后,需要对中断信息进行处理。而如何对中断信息进行处理,可以由我们编程决定。我们编写的,用来处理中断信息的程序被称为中断处理程序。一般来说,需要对不同的中断信息编写不同的处理程序。
CPU 在收到中断信息后,应该转去执行该中断信息的处理程序。我们知道,若要 8086CPU 执行某处的程序,就要将 CS:IP 指向它的入口(即程序第一条指令的地址)。可见首要的问题是,CPU 在收到中断信息后,如何根据中断信息确定其处理程序的入口。
我们知道,中断信息中包含有标识中断源的类型码。根据 CPU 的设计,中断类型码的作用就是用来定位中断处理程序。
12.3 中断向量表
CPU 用 8 位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。那么什么是中断向量表呢?中断向量表就是中断向量的列表。那么什么又是中断向量?所谓中断向量,就是中断处理程序的入口地址。展开来讲,中断向量表,就是中断处理程序入口地址的列表。
中断向量表在内存中保存,其中存放着 256 个中断源所对应的中断处理程序的入口。
这样一来,CPU 只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址。这样一来,CPU 如何找到中断向量表?中断向量表在内存中存放,对于 8086PC 机,中断向量表指定放在内存地址 0 处。从内存 0000:0000 到 0000:03FF 的 1024 个单元中存放着中断向量表(8086CPU 只会从这段内存读取中断向量表)。
那么在中断向量表中,一个表项占多大的空间呢?一个表项存放一个中断向量,也就是一个中断处理程序的入口地址,对于 8086CPU,这个入口地址包括段地址和偏移地址,所以一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。
12.4 中断过程
从上面的讲解中,我们知道,可以用中断类型码,在中断向量表中找到中断处理程序的入口。找到这个入口地址的最终目的是用它设置 CS 和 IP,使 CPU 执行中断处理程序。用中断类型码找到中断向量,并用它设置 CS 和 IP,这个工作是由 CPU 的硬件自动完成的。CPU 硬件完成这个工作的过程被称为中断过程。
CPU 收到中断信息后,要对中断信息进行处理,首先将引发中断过程。硬件在完成中断过程后,CS:IP 将指向中断处理程序的入口,CPU 开始执行中断处理程序。
有一个问题需要考虑,CPU 在执行完中断处理程序之后,应该返回原来的执行点继续执行下面的指令。所以在中断过程中,在设置 CS:IP 之前,还要将原来的 CS 和 IP 的值保存起来。在使用 call 指令调用子程序时有同样的问题,子程序执行后还要返回到原来的执行点继续执行,所以,call 指令先保存当前 CS 和 IP 的值,然后再设置 CS 和 IP。
下面是 8086CPU 在收到中断信息后,所引发的中断过程、
(1)(从中断信息中)取得中断类型码;
(2)标志寄存器的值入栈(因为要在中断过程中改变标志寄存器的值,所以先将其保存在栈中);
(3)设置标志寄存器的第 8 位 TF 和第 9 位 IF 的值为 0;
(4)CS 的内容入栈;
(5)IP 的内容入栈;
(6)从内存地址为中断类型码*4 和中断类型码*4+2 的两个字单元中读取中断处理程序的入口地址设置 IP 和 CS。
CPU 在收到中断信息后,如果处理该中断信息,就完成一个由硬件自动执行的中断过程(程序员无法改变这个过程中所要做的工作)。中断过程的主要任务就是用中断类型码在中断向量表中找到中断处理程序的入口地址,设置 CS 和 IP。
我们更简洁地描述中断过程,如下:
(1)取得中断类型码 N;
(2)pushf
(3)TF=0,IF=0
(4)push CS
(5)push IP
(6)(IP)=(N*4),(CS)=(N*4+2)
在最后一步完成后,CPU 开始执行由程序员编写的中断处理程序。
12.5 中断处理程序和 iret 指令
由于 CPU 随时都可能检测到中断信息,也就是说,CPU 随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间之中。而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。
中断处理程序的编写方法和子程序的比较相似,下面是常规步骤:
(1)保存用到的寄存器
(2)处理中断
(3)恢复用到的寄存器
(4)用 iret 指令返回
iret 指令的功能用汇编语法描述为:
pop IP
pop CS
popf
iret 指令通常和硬件自动完成的中断过程配合使用。可以看到,在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP,而 iret 的出栈顺序是 IP、cs、标志寄存器,刚好和其相对应,实现了用执行中断处理程序前的 CPU 现场恢复标志寄存器和 CS、IP 的工作。iret 指令执行后,CPU 回到执行中断处理程序前的执行点继续执行程序。
12.6 除法错误中断的处理
默认情况下,除法溢出的时候,显示信息提示:“Divide overflow”后,返回到操作系统中。
12.7 编程处理 0 号中断
现在我们考虑改变一下 0 号中断处理程序的功能,即重新编写一个 0 号中断处理程序,它的功能是在屏幕中间显示“overflow!”,然后返回到操作系统。
编程:当发生除法溢出时,在屏幕中间显示 "overflow!",返回 DOS。
分析:
(1)当发生溢出的时候,产生 0 号中断信息,从而引发中断过程。
此时,CPU 将进行以下工作。
① 取得中断类型码
② 标志寄存器入栈,TF、IF 设置为 0
③ CS、IP 入栈
④ (IP)=(0*4),(CS)=(0*4+2)
(2)可见,当中断 0 发生时,CPU 将转去执行中断处理程序。
只要按如下步骤编写中断处理程序,当中断 0 发生时,即可显示 "overflow!"。
① 相关处理
② 向显示缓冲区送字符串 "overflow!"
③ 返回 DOS
我们将这段程序称为:do0。
(3)现在的问题是:do0 应放在内存中。因为除法溢出随时可能发生,CPU 随时都可能将 CS:IP 指向 do0 的入口,执行程序。
我们把程序 do0 传送到内存 0000:0200 处。
(4)将中断处理程序 do0 放到 0000:0200 后,若要使得除法溢出发生的时候,CPU 转去执行 do0,则必须将 do0 的入口地址,即 0000:0200 登记在中断向量表的对应表项中,因为除法溢出的中断类型码为 0,它的中断处理程序的入口地址应该从 0*4 地址单元开始存放,段地址存放在 0*4+2 字单元中,偏移地址存放在 0*4 字单元中。也就是说要将 do0 的段地址 0 放在 0000:0002 字单元中,将偏移地址 200H 存放在 0000:0000 字单元中。
总结上面的分析,我们要做以下几件事情。
(1)编写可以显示“overflow!”的中断处理程序:do0
(2)将 do0 送入内存0000:0200 处
(3)将 do0 的入口地址 0000:0200 存储在中断向量表 0 号表项中。
程序的框架如下。
assume cs:code code segment start: do0 安装程序 设置中断向量表 mov ax,4c00h int 21h do0: 显示字符串"overflow!" mov ax,4c00h int 21h code ends end start
可以看到,上面的程序可分为两部分:
(1)安装 do0,设置中断向量的程序
(2)do0
上面的程序执行时,do0 的代码是不执行的。因为没有除法溢出,只有在除法溢出的时候才会执行该中断。
回忆一下:
我们如何让一个内存单元称为栈顶?将它的地址放入 SS、SP 中;
我们如何让一个内存单元中的信息被 CPU 当作指令来执行?将它的地址放入 CS、IP 中;
那么,我们如何让一段程序称为 N 号中断的中断处理程序?将它的入口地址放入中断向量表的 N 号表项中。
12.8 安装
assume cs:code code segment start: 设置 es:di 指向目的地址 设置 ds:si 指向源地址 设置 cx 为传输长度 设置传输方向为正 rep movsb 设置中断向量表 mov ax,4c00h int 21h do0: 显示字符串 "overflow!" mov ax,4c00h int 21h code ends end start
我们来看一下,用 rep movsb 指令的时候需要确定的信息。
(1)传送的原始地址,段地址:code,偏移地址:offset do0
(2)传送的目的地址:0:200
(3)传送的长度:do0 部分代码的长度
(4)传送的方向:正向
问题是,我们如何确定 do0 代码的长度?我们可以利用编译器来计算 do0 的长度,具体做法如下:
assume cs:code code segment start: mov ax,cs mov ds,ax mov si, offset do0 ;设置 ds:si 指向源地址 mov ax,0 mov es,ax mov di,200h ;设置 es:di 指向目的地址 mov cx, offset do0end - offset do0 ;设置 cx 为传输长度 cld ;设置传输方向为正 rep movsb 设置中断向量表 mov ax,4c00h int 21h do0: 显示字符串"overflow!" mov ax,4c00h int 21h do0end: nop code ends end start
“-” 是编译器识别的运算符号,编译器可以用它来进行两个常熟的减法。
比如,指令:mov ax,8-4,被编译器处理为指令:mov ax,4
汇编编译器可以处理表达式。
12.9 do0
do0 的主要任务是显示字符串,程序如下:
do0: 设置 ds:si 指向字符串 mov ax,0b800h mov es,ax mov di,12*160+36*2 ;设置 es:di 指向显存空间的中间位置 mov cx,9 ;设置 cx 为字符串长度 s: mov al,[si] mov es:[di],al inc si add di,2 loop s mov ax,4c00h int 21h do0end: nop
可是上面的程序中,需要输出的字符串并不在其中保存,所以我们不能保证每次除法溢出的时候都是输出同一个字符串。
但是我们可以把需要输出的字符串也放到 do0 的内存空间中,如下:
do0: jmp short do0start db "overflow!" do0start: mov ax,cs mov ds,ax mov si,202h ;设置 ds:si 指向字符串(jmp 占两个字节) mov ax,0b800h mov es,ax mov di,12*160+36*2 ;设置 es:di 指向显存空间的中间位置 mov cx,9 s: mov al,[si] mov es:[di],al inc si add di,2 loop s mov ax,4c00h int 21h do0end: nop code ends end start
12.10 设置中断向量
下面,将 do0 的入口地址 0:200,写入中断向量表的 0 号表项中,使 do0 成为 0 号中断的中断处理程序。
mov ax,0 mov es,ax mov word ptr es:[0*4],200h mov word ptr es:[0*4+2],0
12.11 单步中断
基本上,CPU 在执行完一条指令之后,如果检测到标志寄存器的 TF 位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为 1,则它所引发的中断过程如下。
(1)取得中断类型码 1
(2)标志寄存器入栈,TF、IF 设置为 0
(3)CS、IP 入栈
(4)(IP)=(1*4),(CS)=(1*4+2)
如上所述,如果 TF=1,则执行一条指令后,CPU 就要转去执行 1 号中断处理程序。
当 TF=1 时,CPU 在执行完一条指令后将引发单步中断,转去执行中断处理程序。中断程序也是由一条条指令组成的,因此我们需要在中断处理程序里面设置 TF=0,否则就是无限单步中断了。我们来看一下中断的过程。
(1)取得中断类型码
(2)标志寄存器入栈,TF=0、IF=0
(3)CS、IP 入栈
(4)(IP)=(N*4),(CS)=(N*4+2)
最后,CPU 提供单步中断功能的原因就是,为单步跟踪程序的执行过程,提供了实现机制。
12.12 响应中断的特殊情况
一般情况下,CPU 在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。可是,在有些情况下,CPU 在执行完当前指令后,即便是发生中断,也不会响应。下面用一种情况来说明。
在执行完向 ss 寄存器传送数据的指令后,即便是发生中断,CPU 也不会响应。这样做的原因是,ss:sp 联合指向栈顶,而对它们的设置应该连续完成。如果在执行完设置 ss 的指令后,CPU 响应中断,引发中断过程,要在栈中压入标志寄存器、CS 和 IP 的值。而 ss 改变,sp 并未改变,ss:sp 指向的是不正确的栈顶,将引起错误。所以 CPU 在执行完设置 ss 的指令后,不响应中断。这给连续设置 ss 和 sp 指向正确的栈顶提供了一个时机。即,我们应该利用这个特性,将设置 ss 和 sp 的指令连续存放,使得设置 sp 的指令紧接着设置 ss 的指令执行,而在此之间,CPU 不会引发中断过程。比如,我们要将栈顶设为 1000:0,应该:
mov ax,1000h
mov ss,ax
mov sp,0
而不应该:
mov ax,1000h
mov ss,ax
mov ax,0
mov sp,0
Debug 利用单步中断来实现 T 命令的功能,也就是说,用 T 命令执行一条指令后,CPU 响应单步中断,执行 Debug 设置好的处理程序,才能在屏幕上显示寄存器的状态,并等待命令的输入。而在 mov ss,ax 指令执行后,CPU 根本就不响应任何中断,其中也包括单步中断,所以 Debug 设置好的用来显示寄存器状态和等待输入命令的中断处理程序根本没有得到执行,所以我们看不到预期的结果。
附:完整的自定义中断处理程序
assume cs:code code segment start: mov ax,cs mov ds,ax mov si,offset do0 ;设置 ds:si 指向源地址 ;设置中断向量 mov ax,0 mov es,ax mov word ptr es:[0], 200h mov word ptr es:[2],0 mov ax,0 mov es,ax mov di,200h ;设置 es:di 指向目的地址 mov cx,offset do0end - offset do0 ;设置 cx 为传输长度 cld rep movsb mov al,10 mov ah,0 div ah mov ax,4c00h int 21h do0: jmp short do0start db "overflow!" do0start: mov ax,cs mov ds,ax mov si,202h ;设置 ds:si 指向字符串 mov ax,0b800h mov es,ax mov di,12*160 + 36*2 ;设置 es:di 指向显存空间的中间位置 mov cx,9 s: mov al,[si] mov es:[di],al inc si add di,2 loop s mov ax,4c00h int 21h do0end: nop code ends end start