12.1 内中断的产生
中断是CPU处理外部突发事件的一个重要技术。
它能使CPU在运行过程中对外部事件发出的中断请求及时地进行处理,处理完成后又立即返回断点,继续进行CPU原来的工作。
引起中断的原因或者说发出中断请求的来源叫做中断源。根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。
外部中断一般是指由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备 的中断请求。
内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算 溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。
软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序以及DOS的系统功能调用(INT 21H)等都是软件中断。
CPU为了处理并发的中断请求,规定了中断的优先权,中断优先权由高到低的顺序是:(1)除法错、溢出中断、软件中断 (2)不可屏蔽中断 (3)可屏蔽中断 (4)单步中断。
12.2 中断处理程序
CPU的设计者必须在中断信息和其处理程序的入口地址之间建立某种联系,使得CPU根据中断信息可以找到要执行的处理程序。
我们知道,中断信息中包含有标识中断源的类型码。根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。
比如CPU 根据中断类型码 4,就可以找到4号中断的处理程序。
可随之而来的问题是,若要定位中断处理程序,需要知道它的段地址和偏移地址,而如何根据 8位的中断类型码(8086中断类型码为一个字节)得到中断处理程序的段地址和偏移地址呢? 这就要引入“中断向量表”了。
12.3 中断向量表
CPU用 8 位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。
那么什么是中断向量表呢?
中断向量表就是中断向量的列表
中断向量表在内存中存放,对于8086PC机,中断向量表指定放在内存地址0处。
中断向量表在内存中保存,其中存放着 256个中断源所对应的中断处理程序的入口(8位中断类型码)
从内存0000:0000到0000:03FF的1024个字节单元中存放着中断向量表。(因为一个物理地址包括16位段地址和16位偏移地址,要占4个字节,256*4=1024)
12.4 中断过程
从上面的讲解中,我们知道,可以用中断类型码,在中断向量表中找到中断处理程序的入口。
找到这个入口地址的最终目的是用它设置CS和IP,使CPU执行中断处理程序。
用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动完成的。
CPU 硬件完成这个工作的过程被称为中断过程。
8086CPU的中断过程:
(1)(从中断信息中)取得中断类型码;
(2)标志寄存器的值入栈(保护标志位);
(3)设置标志寄存器的第8位TF 和第9位IF的值为0;(这一步的目的后面将介绍)
(4)CS的内容入栈;
(5)IP的内容入栈;
(6)从内存地址为中断类型码*4 和中断类型码 *4+2 的两个字单元中读取中断处理程序的入口地址设置IP和CS。
可以看到CPU将CS、IP保存在栈中。
我们注意到,在中断过程中还要做的一个工作就是设置标志寄存器的TF、IF位。
对于这样做的目的,我们将在后面的内容和下一章中进行讨论。
我们更简洁的描述中断过程,如下:
(1)取得中断类型码N;
(2) pushf
(3) TF = 0,IF = 0
(4) push CS
(5) push IP
(6)(IP) = (N4),(CS) = (N4+2)
在最后一步完成后,CPU 开始执行由程序员编写的中断处理程序。
12.5 中断处理程序
由于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 除法错误中断的处理
下面的内容中,我们通过对 0号中断,即除法错误的中断处理,来体会一下前面所讲的内容。
当CPU执行div等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为 0 的中断信息,CPU将检测到这个信息,然后引发中断过程,转去执行 0 号中断所对应的中断处理程序。
12.7 编程处理 0 号中断
实例:try.asm
现在我们考虑改变一下0号中断处理程序的功能,即重新编写一个0号中断处理程序,它的功能是在屏幕中间显示“Welcome to Fishc.com!”的广告语,然后返回到操作系统。
程序分析一步步:
一)、当发生除法溢出的时候,产生0号中断信息,从而引发中断过程。
此时,CPU将进行以下工作:
① 取得中断类型码0;
② 标志寄存器入栈,TF、IF设置为0;
③ CS、IP入栈;
④ (IP) = (04),(CS) = (04+2)
二)、可见 ,当中断 0 发生时,CPU将转去执行中断处理程序。
只要按如下步骤编写中断处理程序,当中断0发生时,即可显示“Welcome to Fishc.com!”。
① 相关处理。
② 向显示缓冲区送字符串“Welcome to Fishc.com!”。
③ 返回DOS
我们将这段程序称为do0。
三)、现在的问题是:do0 应放在内存中。
因为除法溢出随时可能发生,CPU随时都可能将 CS:IP指向 do0的入口,执行程序。
那么do0应该放在哪里呢?
由于我们是在操作系统之上使用计算机,所有的硬件资源都在操作系统的管理之下,所以我们要想得到一块内存存放do0,应该向操作系统申请。
但在这里出于两个原因我们不想这样做:
原因之一:过多地讨论申请内存将偏离问题主线;
原因之二:我们学习汇编的一个重要目的就是要获得对计算机底层的编程体验。
所以,在可能的情况下,我们不去理会操作系统,而直接面向硬件资源。
问题变得简单而直接,我们只需找到一块别的程序不会用到的内存区,将do0传送到其中即可。
前面讲到,内存0000:0000~0000:03FF,大小为1KB的空间是系统存放中断处理程序入口地址的中断向量表。一般情况下,从0000:0200至0000:02FF的256个字节的空间所对应的中断向量表项都是空的,操作系统和其他应用程序都不占用。
根据以前的编程经验,我们可以估计出,do0的长度不可能超过256个字节。
结论:我们可以将do0传送到内存0000:0200处。
四)、我们将中断处理程序do0放到 0000:0200 后,若要使得除法溢出发生的时候,CPU转去执行do0,则必须将do0的入口地址。
即0000:0200登记在中断向量表的对应表项中。
因为除法溢出对应的中断类型码为0,它的中断处理程序的入口地址应该从0×4地址单元开始存放,段地址存放在 0×4+2 字单元中,偏移地址存放在0×4字单元中。
也就是说要将do0的段地址0存放在 0000:0002 字单元中 ,将偏移地址200H存放在0000:0000字单元中。
总结上面的分析,我们要做以下几件事情:
(1)编写可以显示“Welcome to Fishc.com!”的中断处理程序:do0;
(2)将do0送入内存0000:0200处;
(3)将do0的入口地址0000:0200存储在中断向量表0号表项中。
程序框架:
assume cs:code
code segment
start:
do0安装程序
设置中断向量表
mov ax,4c00h
int 21h
do0:
显示字符串“Welcome to Fishc.com!”
mov ax,4c00h
int 21h
code ends
end start
我们可以看到,上面的程序分为两部分:
(1)安装do0,设置中断向量的程序
(2)do0
程序执行时,do0的代码是不执行的,它只是作为do0安装程序所要传送的数据。
执行do0安装程序,将 do0 的代码拷贝到内存 0:200处,然后设置中断向量表,即偏移地址200H和段地址0,保存在0号表项中。这两部分工作完成后,程序就返回了。
程序的目的就是在内存0:200处安装do0 的代码,将0号中断处理程序的入口地址设置为0:200。
do0的代码虽然在程序中,却不在程序执行的时候执行。它是在除法溢出发生的时候才得以执行的中断处理程序。
do0部分代码的最后两条指令是依照我们的编程要求,用来返回DOS的。
现在,我们在反过来从CPU的角度看一下,什么是中断处理程序?
do0变成0号中断的中断处理程序的过程:
(1)"shiyan"这个程序在执行时,被加载到内存中,此时do0的代码在程序所在的内存空间中,它只是存放在程序的代码段中的一段要被传送到其他单元中的数据,我们不能说它是0号中断的中断处理程序;
(2)程序中安装do0 的代码执行完后,do0的代码被从程序的代码段中拷贝到0:200处。此时,我们也还不能说它是0号中断的中断处理程序,它只不过是存放在0:200处的一些数据;
(3)程序12.1中设置中断向量表的代码执行完后,在0号表项中填入了do0的入口地址0:200,此时0:200 处的信息,即do0 的代码,就变成了0号中断的中断处理程序。
当除法溢出(即0号中断)发生时,CPU就会执行0:200处的代码。
下面的内容中,我们讨论每一部分程序的具体编写方法。
提示:我们可以使用movsb指令,将do0的代码送入0:0200处。
更详细的程序框架:
assume cs:code
code segment
start:
;设置es:di指向目的地址
;设置ds:si指向源地址
;设置cx为传输长度
;设置传输方向为正
rep movsb
;设置中断向量表
mov ax, 4c00h
int 21h
do0:
;显示字符串“Welcome to Fishc.com!”
mov ax, 4c00h
int 21h
code ends
end start
我们来看一下,用rep movsb指令的时候需要确定的信息:
(1)传送的原始位置,段地址:code,偏移地址:offset do0;
(2)传送的目的位置:0:200;
(3)传送的长度:do0部分代码的长度;
(4)传送的方向:正向。
更明确的程序.asm
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, do0部分代码的长度 ;设置cx为传输长度
cld ;设置传输方向为正
rep movsb
;设置中断向量表
mov ax, 4c00h
int 21h
do0:
;显示字符串“Welcome to Fishc.com!”
mov ax, 4c00h
int 21h
code ends
end start
问题是,我们如何知道do0代码的长度?
最简单的方法是,计算一下do0 所有指令码的字节数。
但是这样做太麻烦了,因为只要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:
;显示字符串“Welcome to Fishc.com!”
mov ax,4c00h
int 21h
do0end:
nop
code ends
end start
“-”是编译器识别的运算符号,编译器可以用它来进行两个常数的减法。
比如:mov ax,8-4,被编译器处理为指令: mov ax,4。
另外,编译器还可以处理表达式。
比如指令: mov ax,(5+3)*5/10,被编译器处理为指令: mov ax,4
接下来是do0程序,do0程序的主要任务是显示字符串,程序如下:do0.asm
do0:
;设置ds:si指向字符串
mov ax, 0b800h
mov es, ax
mov di, 12160+362 ;设置es:di指向显存空间的中间位置
mov cx, 21 ;设置cx为字符串长度
s:
mov al, [si]
mov es:[di], al
inc si
add di, 2
loop s
mov ax, 4c00h
int 21h
do0end: nop
把以上所有的元素都放进程序中,我们得出:program1.asm
assume cs:code
data segment
db "Welcome to Fishc.com!"
data ends
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:
mov ax, data
mov ds, ax
mov si, 0 ;设置ds:si指向字符串
mov ax, 0b800h
mov es, ax
mov di, 12*160+36*2 ;设置es:di指向显存空间的中间位置
mov cx, 21 ;设置cx为字符串长度
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
程序program1.asm看似合理,可实际上却大错特错。
错误分析:》》T_T
注意,“Welcome to Fishc.com!”在程序program1的data段中。程序program1执行完成后返回,它所占用的内存空间被系统释放,而在其中存放的“Welcome to Fishc.com!”也将很可能被别的信息覆盖;
而do0程序被放到了0:200处,随时都会因发生了除法溢出而被CPU 执行,很难保证 do0 程序从原来程序program1所处的空间中取得的是要显示的字符串“Welcome to Fishc.com!”。
因为 do0 程序随时可能被执行,而它要用到字符串“Welcome to Fishc.com!”,所以该字符串也应该存放在一段不会被覆盖的空间中。
我们修改下源代码:progarm2.asm
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: jmp short do0start
db "Welcome to Fishc.com!"
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, 21 ;设置cx为字符串长度
s: mov al, [si]
mov es:[di], al
inc si
add di, 1
mov al, 02h ;设置颜色
mov es:[di], al
add di, 1
loop s
mov ax, 4c00h
int 21h
do0end: nop
code ends
end start
这样进行修改后,我们的字符串就能跟随do0保存在安全空间中,但是问题又来了,do0程序执行过程中必须要找到“Welcome to Fishc.com!”,那么它在哪里呢?
首先来看段地址,“Welcome to Fishc.com!”和do0的代码处于同一个段中,而除法溢出发生时,CS中必然存放do0的段地址,也就是“Welcome to Fishc.com!”的段地址;
再来看偏移地址,0:200处的指令为jmp short do0start ,这条指令占两个字节,所以“Welcome to Fishc.com!”的偏移地址为202h
最后,我们将do0的入口地址0:200,写入中断向量表的 0 号表项中,使do0成为0 号中断的中断处理程序。
0号表项的地址为0:0,其中0:0字单元存放偏移地址,0:2字单元存放段地址。
完整程序实现如下:program3.asm
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, 0 ;设置中断向量表
mov es, ax
mov word ptr es:[0*4], 200h
mov word ptr es:[0*4+2], 0
mov ax,4c00h
int 21h
do0: jmp short do0start
db "Welcome to Fishc.com!"
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, 21 ;设置cx为字符串长度
s: mov al, [si]
mov es:[di], al
inc si
add di, 1
mov al, 02h ;设置颜色
mov es:[di], al
add di, 1
loop s
mov ax, 4c00h
int 21h
do0end: nop
code ends
end start
演示:yanshi.exe
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, 0 ;设置中断向量表
mov es, ax
mov word ptr es:[0*4], 200h
mov word ptr es:[0*4+2], 0
mov ax,4c00h
int 21h
do0: jmp short do0start
db "Welcome to Fishc.com!"
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, 21 ;设置cx为字符串长度
s: mov al, [si]
mov es:[di], al
inc si
add di, 1
mov al, 02h ;设置颜色
mov es:[di], al
add di, 1
loop s
mov ax, 4c00h
int 21h
do0end: nop
code ends
end start
12.8 安装
12.9 do0
12.10 设置中断向量
12.11 单步中断
什么是单步中断?
CPU为什么要提供这样的功能呢?
我们在使用Debug的T命令的时候,有没有想过这样的问题,Debug如何能让CPU在执行一条指令后,就显示各个寄存器的状态?
假想:如果CPU不提供其他功能的话,就按正常方式工作,只要CPU一加电,它就从预设的地址开始一直执行下去……不可控制!
可是,我们在Debug中看到的情况却是,Debug可以控制CPU执行被加载程序中的一条指令,然后让它停下来,显示寄存器的状态。
Debug有特殊的能力吗?
我们只能说Debug利用了CPU提供的一种功能。
只有CPU提供了在执行一条指令后就转去做其他事情的功能,Debug或是其他的程序才能利用CPU提供的这种功能做出我们使用T命令时的效果。
好了,我们先来谈谈CPU是如何实现单步中断机制,然后再来简要地考虑一下Debug是如何利用CPU所提供的单步中断的功能的。
CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。
单步中断的中断类型码为1,则它所引发的中断过程如下:
(1)取得中断类型码1;
(2)标志寄存器入栈,TF、IF设置为0;
(3) CS、IP入栈;
(4)(IP)=(14),(CS)=(14+2)。
如上所述,如果TF=1,则执行一条指令后,CPU就要转去执行1号中断处理程序。
同样的道理,Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令。
在使用 T 命令执行指令时,Debug 将TF设置为 1,使得CPU在工作于单步中断方式下,则在CPU执行完这条指令后就引发单步中断,执行单步中断的中断处理程序,所有寄存器中的内容被显示在屏幕上,并且等待输入命令。
总之,当TF=1时,CPU在执行完一条指令后将引发单步中断,转去执行中断处理程序。执行完中断处理程序后,又返回原来的位置继续……
我们再来看一下中断过程
(1)取得中断类型码N;
(2)标志寄存器入栈,TF=0、IF=0;
(3)CS、IP入栈;
(4)(IP) = (N4),(CS) = (N4+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,
好了,现在我们回过来看一下,实验2 中的“(3)下一条指令执行了吗?”。
现在你知道原因了吧? !