中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
一、中断的分类
根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。
- 外部中断一般是指由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备 的中断请求。
- 内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。
- 软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序。例如:ROM BIOS中的各种外部设备管理中断服务程序(键盘管理中断、显示器管理中断、打印机管理 中断等,)以及DOS的系统功能调用(INT 21H)等都是软件中断。
二、内部中断
1.内部中断的产生
当CPU内部有下面的情况发生的时候,将产生相应的中断信息.
- 除法错误
- 单步执行
- 执行into指令
- 执行int指令
中断信息的来源,简称为中断源,上述的4种中断源,在8086CPU中的中断类型码如下
- 除法错误:0
- 单步执行:1
- 执行into指令:4
- 执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。
2.中断处理程序
- CPU收到中断信息后,需要对中断信息进行处理。而如何对中断信息进行处理,可以由我们编程决定。
- CPU的设计者必须在中断信息和其处理程序的入口地址之间建立某种联系,使得CPU根据中断信息可以找到要执行的处理程序。
3.中断向量表
中断向量表的内存中保存,其中存放着256个中断源所对应的中断处理程序的入口,如图:
可见,CPU只要知道中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址.中断向量表在内存中存放,对于8086CP机,中断向量表指定放在内存地址0处。从内存0000:0000到0000:03FF的1024个单元中存放着中断向量表.对于8086CPU,这个入口地址包括段地址和偏移地址,所以一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。
4.中断过程
从上面的讲解中,我们知道,可以用中断类型码,在中断向量表中找到中断处理程序的入口。找到这个入口地址的最终目的是用它设置CS和IP,使CPU执行中断处理程序。用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动完成的。CPU硬件完成这个工作的过程被称为中断过程。
下面是8086CPU在收到中断信息后,所引发的中断过程:
- (从中断信息中)取得中断类型码
- 标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中)
- 设置标志寄存器的第8位TF和第9位IF的值为0(这一步的目的后面将介绍)
- CS的内容入栈
- IP的内容入栈
- 从内存地址为中断类型码*4中断类型码*4和中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS
更简洁的描述中断过程,如下:
- 取得中断类型码N
- pushf
- TF=0,IF=0
- push CS
- push IP
- (IP)=(N*4),(CS)=(N*4+2)
最后一步完成后,CPU开始执行由程序员编写的中断处理程序.
5.中断处理程序和IRET指令
CPU随时都可能检测到中断处理程序,所以中断处理程序必须一直存储在内存某段空间中。而中断处理程序入口地址,即中断向量,必须存储在对应的中断向量表表项中。
中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
- 保存用到的寄存器
- 处理中断
- 恢复用到的寄存器
- 用iret指令返回
iret指令的功能用汇编语法描述为:
pop IP
pop CS
popf
下面将通过程序来演示中断处理过程,一般情况下,如下指令将会导致除法溢出异常:
mov ax,1000H
mov bl,1
div bl
实际上,当发生除法溢出时,CPU将会产生0号中断,因此我们只需要修改0号中断的中断向量表,使其指向我们自定义的程序段,就可以使发生异常时调用我们自己的程序段.但是,要注意的时,程序退出时,内存可能就会被其他程序覆盖(虚拟8086或8086模式),因此我们需要将我们的代码放在一段安全的内存空间中,这里我们选用0:200H~0:2FFH(注意到这实际上是中断向量表的空间,但是一般情况下,我们用不到这些中断,因此我们可以用这段空间来存放数据),代码如下:
assume cs:code
code segment
start:
mov ax,0
mov es,ax
mov ax,code
mov ds,ax
; 计算CX的大小,也就是传输长度
mov cx,offset eit - offset it
mov di,200h
mov si,offset it
cld
rep movsb
mov bx,0
; 修改0号中断向量表
mov word ptr es:[bx],200h
mov word ptr es:[bx+2],0
mov ax,4c00h
int 21h
; 中断处理过程
it: jmp short sit
db 'Hello Wrold!'
sit: push es
push ds
push si
push di
push cx
mov cx,12
mov ax,0b800h
mov es,ax
mov ax,0
mov ds,ax
mov di,160*12+35*2
mov si,202h
s: movsb
inc di
loop s
pop cx
pop di
pop si
pop ds
pop es
mov ax,4c00h
int 21h
eit: nop
code ends
end start
运行上面的程序后,我们再运行下面的程序,将会在屏幕中间输出’Hello World!’
assume cs:code
code segment
start:
mov ax,1000h
mov bl,1
div bl
mov ax,4c00h
int 21h
code ends
end start
5.单步中断
基本上,CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1,则它所引发的中断过程如下:
- 取得中断类型码1
- 标志寄存器入栈,TF、IF设置为0
- CS、IP入栈
- (IP)=(1*4),(CS)=(1*4+2)
注意,第2步很重要,因为如果不将TF位置0,则将陷入一个永远不能结束的循环(递归)。因为单步中断将引发单步中断的中断程序,而单步中断的中断程序将引发单步中断程序中的中断程序…
现在,我们给出一个实现类似DEBUG程序的方法,修改1号中断,具体代码如下:
assume cs:code
code segment
start:
mov ax,0
mov es,ax
mov ax,code
mov ds,ax
mov cx,offset eit - offset it
mov di,200h
mov si,offset it
cld
rep movsb
mov bx,0
mov word ptr es:[bx+4],200h
mov word ptr es:[bx+6],0
mov ax,4c00h
int 21h
it:
mov dl,'B'
mov ah,2
int 21H
iret
eit: nop
code ends
end start
这里使用了21H中断,向屏幕输出字符.下面程序通过修改TF标志位以达到没执行一条指令调用一次1号中断,向屏幕输出一个字符B:
assume cs:code
code segment
start:
PUSHF
MOV BP,SP
OR WORD PTR[BP+0],0100H
POPF
mov ax,10
mov ax,10
mov ax,10
PUSHF
; 注意 程序退出之前要修改TF标志位为0
MOV BP,SP
AND WORD PTR[BP+0],0FEFFH
POPF
mov ax,4c00h
int 21h
code ends
end start
6.中断响应的特殊情况
一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就响应中断程序,引发中断过程。可是,在有些情况下,CPU在执行完当前指令后,即便是发生中断,也不会响应。比如,在执行完向ss寄存器传送数据的指令后,即便发生中断,CPU也不会响应。这样做的主要原因是,ss:sp联合指向栈顶,而对它们的设置应该连续完成。如果执行完设置ss的指令后,CPU响应中断,引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,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
三、INT指令
int指令的格式为:int n,n为中断类型码,它的功能是引发中断过程.CPU执行int n指令后,相当于引发了一个n号中断的中断过程.例如下面的程序:
assume cs:code
code segment
start:
int 0
mov ax,4c00h
int 21h
code ends
end start
上面程序并没有做除法操作,但程序运行后却输出了Divide Overflow,这是因为int 0 调用了0号中断处理程序.可见int指令的最终功能和call指令相似,都是调用一段程序.
一般情况下,系统将一些具有一定功能的子程序以中断处理程序的方式提供给应用程序调用.我们编程的时候,可以用int指令调用这些子程序.当然也可以自己编写一些中断处理程序供别人使用.
四、端口
各种存储器都通过地址总线,数据总线以及控制总线与CPU相连。CPU对这些各种存储器组成的存储单元进行统一编址,统一寻址。除了各种存储器和CPU相连之外,还有以下几种芯片和CPU相连:
- 各种接口卡(比如网卡,显卡)上的芯片,它们控制接口卡工作
- 主板上的接口芯片,CPU通过它们对部分外设进行访问
- 其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理
在这些芯片中,都有一些可以用CPU读写的寄存器,虽然这些寄存器在物理上不同的芯片,但是有如下两个相同点:
- 都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的
- CPU对它们进行读或写的时候通过控制线向它们的芯片发出端口读写命令
从CPU的角度,CPU对这些端口统一进行寻址,每个端口在地址空间都有独一无二的地址.CPU可以直接从:内存地址空间,端口,CPU内部寄存器这三个地方直接读写数据。
1.端口的读写
CPU和端口所在的芯片通过地址总线相连,所以端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口的地址范围为0~65535.对端口的读写只能用in和out指令,分别用于从端口读取数据和往端口写入数据.
访问内存和访问端口的区别:
(1)访问内存:
mov ax,ds:[8] ;假设执行前(ds)=0
执行步骤:
- CPU通过地址线将地址信息8发出
- CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据
- 存储器将8号单元中的数据通过数据线送入CPU
(2)访问端口:
in al,60h ;从60号端口读入一个字节
执行步骤:
- CPU通过地址线将地址信息60h发出
- CPU通过控制线发出端口命令,告诉芯片将要读取该端口的信息
- 端口所在芯片将60h端口的信息送到CPU中
注意:在in和out指令中,只能使用ax和al来存放从端口中读入的数据或要发送到端口的数据.访问8位端口时用al,访问16位端口用ax
- 对0~255以内的端口进行读写:
in al,20h
out 20,al
- 对256~65535端口进行读写时,端口号放在dx中:
mov dx,3f8h
in al,dx
out dx,al
2.CMOS芯片
在PC中有一个CMOS RAM芯片,该芯片特征如下:
- 包含一个时钟和一个128个存储单元的RAM存储器
- 该芯片靠电池供电。所以关机后仍正常工作
- 128字节的ram,其中0~0dh用来保存时间信息,其余大部分用于保存系统配置,供系统启动时的BIOS读取,BIOS也提供了相关程序,使我们在开机时候可以配置CMOS RAM中的信息
- 该芯片有两个端口70h和71h,用这两个端口实现对CMOS RAM的读写
- 70h为地址端口,存放要读取的CMOS的地址单元。71h为数据端口,存放要写入或者写出的数据
比如读CMOS的2号单元分为以下两步:
- 将2送入70h
- 从71h读出2号单元的内容
下面介绍两个指令SHL和SHR
SHL和SHR为逻辑移位操作.shl为逻辑左移位操作,其功能为:
- 将一个寄存器或者内存单元的数据向左移动
- 将最后移出的一位写入CF中
- 最低位用0补充
比如:
mov al,01001000b
shl al,1
执行后(al)=10010000b,CF=0
如果移动位数大于1,必须将移动位数保存在cl中,比如:
mov al,01010001b
mov cl,3
shl al,cl
可以看出将X逻辑左移一位相当于X=X*2
shr为逻辑右移位操作,其功能为:
- 将一个寄存器或者内存单元的数据向右移动
- 将最后移出的一位写入CF中
- 最低位用0补充
比如:
mov al,10000001b
shr al,1
执行后(al)=01000000b,CF=1,同样的,如果移动位数大于1,必须将移动位数保存在cl中
下面程序用移位指令将AX中的值乘以15:
assume cs:code
code segment
start:
mov ax,15
mov bx,ax
mov cl,3
shl ax,cl
shl bx,1
add ax,bx
mov ax,4c00h
int 21h
code ends
end start
下面程序将从CMOS中读取当前时间的分钟并显示在屏幕上:
assume cs:code
code segment
start:
; 准备操作2号单元数据
mov al,2
; 将2送入地址端口
out 70h,al
; 从数据端口读取一个字节数据(该数据为当前时间的分钟BCD码)
in al,71h
mov dl,al
mov cl,4
shr dl,cl
add dl,30h
mov ah,2
; 这个中断会影响ax的值 保存AX
push ax
int 21H
pop ax
and al,0fh
add al,30h
mov dl,al
mov ah,2
int 21H
mov ax,4c00h
int 21h
code ends
end start
下面程序将读取CMOS输出当前时间:
assume cs:code,ss:stack
stack segment stack
db 256 dup(0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,256
mov cx,3
; 循环输出年月日
s1:
mov al,cl
add al,6
out 70h,al
in al,71h
call showAl
cmp cx,1
je s2
; 这里使用BIOS中断
mov dl,'/'
mov ah,2
int 21H
s2: loop s1
mov dl,' '
mov ah,2
int 21H
mov cx,3
; 循环输出时分秒
s3:
mov al,cl
sub al,1
add al,al
out 70h,al
in al,71h
call showAl
cmp cx,1
je s4
mov dl,':'
mov ah,2
int 21H
s4: loop s3
mov ax,4c00h
int 21h
; 输出AL中的BCD码
showAl:
push bx
push cx
push ax
push dx
mov bl,al
mov cl,4
shr al,cl
add al,30H
mov dl,al
mov ah,2
int 21H
and bl,0fh
add bl,30H
mov dl,bl
mov ah,2
int 21H
pop dx
pop ax
pop cx
pop bx
ret
code ends
end start
五、外中断
1.接口芯片和端口
我们知道,PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干个寄存器,CPU将这些寄存器当作端口来对待。外设的输入不直接送入内存和CPU,而是先送入端口中;CPU向外设的输出也不是直接输入外设,而是先送入端口中,再由相关的芯片送到外设,CPU还可以向外设输出控制信息,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。可见,CPU通过端口和外部设备进行联系
2.外中断信息
现在,我们知道了外设的输入被存放在端口中,可是外设的输入随时都有可能到达,CPU如何及时地知道,并进行处理呢?CPU提供中断机制来满足这种需要。前面讲过,当CPU的内部有需要处理的事情发生的时候,将产生中断信息,引发冲断过程。这种中断信息来自CPU内部。还有一种中断信息,来自于CPU外部,当CPU外部有需要处理的事情发生的时候,比如,外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。在PC系统中,外中断源一共有以下两类:
- 可屏蔽中断
可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置。当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU执行完当前指令后响应中断,引发中断过程。如果IF=0,则不响应可屏蔽中断。我们回忆一下引发内中断的一个步骤,IF,TF=0。这样,在引发中断进入中断处理后,不会再响应可以屏蔽的中断,这种设置其实是非常合理的。当然,我们可以通过指令来手动设置IF位。sti 设置IF=1,可响应可屏蔽中断 cti 设置IF=0,不可响应可屏蔽中断。
- 不可屏蔽中断
不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即相应,引发中断过程。对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以,在中断过程中,不需要取中断类型码。
几乎所有由外设引发的外中断,都是可屏蔽中断,当外设有需要处理的时间,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断信息是在系统有必须要处理的紧急情况发生时用来通知CPU的中断信息。在我们现在,讨论的都是可屏蔽中断
下面程序将修改9号中断向量,该程序循环输出a~z,当按下ESC键时改变输出颜色:
assume cs:code,ss:stack
stack segment stack
db 256 dup(0)
stack ends
data segment
dw 0,0
data ends
code segment
start:
;设置IF=1响应屏蔽中断
sti
mov ax,stack
mov ss,ax
mov sp,256
mov ax,0
mov es,ax
mov ax,data
mov ds,ax
; 保存9号中断向量表的原始位置
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
; 修改9号中断向量表,使其指向我们的程序
mov word ptr es:[9*4+2],cs
mov word ptr es:[9*4],offset int9
; 循环输出字母
mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,01000h
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp dx,0
jne s1
pop dx
pop ax
ret
int9:
push bp
push ax
push bx
push es
mov bp,sp
in al,60h
; 这里模拟调用了原始的int 9号中断
; 经过测试不调用也没有什么问题
pushf
pushf
pop bx
and bx,11111100B
push bx
popf
call dword ptr ds:[0]
; 如果是ESC则改变字符颜色
cmp al,1
jnz int9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
int9ret:
pop es
pop bx
pop ax
pop bp
iret
code ends
end start
注意到上述程序可以进一步修改(已经由注释方式给出原因):
assume cs:code,ss:stack
stack segment stack
db 256 dup(0)
stack ends
data segment
dw 0,0
data ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,256
mov ax,0
mov es,ax
mov ax,data
mov ds,ax
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
; 当修改9号中断的中断向量表时发生了按键,这里可能会出错
; 屏蔽可屏蔽中断,避免在设置中断向量表的时候出错
cli
mov word ptr es:[9*4+2],cs
mov word ptr es:[9*4],offset int9
sti
; 循环输出字母
mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,01000h
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp dx,0
jne s1
pop dx
pop ax
ret
int9:
push bp
push ax
push bx
push es
mov bp,sp
in al,60h
; 因为这里本身就是中断调用 TF IF已经置0
pushf
call dword ptr ds:[0]
; 如果是ESC则改变字符颜色
cmp al,1
jnz int9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
int9ret:
pop es
pop bx
pop ax
pop bp
iret
code ends
end start