[TOC]
中断的意思并不是说程序因为异常而中断,相反,中断的意识是CPU不再接着刚才的指令往下执行,而是转而去处理造成中断的信息,注意这里去处理他并不是说该信息会造成威胁,仅仅是程序需要去这样做。
端口和中断,是CPU进行IO的关键!!!
- CPU可以通过指令在内部进行各种运算,但是CPU除了有运算能力之外,还要有IO能力,即对外部设备进行控制,接收输入和输出;
- CPU与外设要通过接口进行交流,即IO操作。接口有两种类型,一种是控制器,另一种是适配器。控制器即IO设备本身或主板上的芯片组,比如磁盘控制器和USB控制器。适配器则是我们俗称的各种卡,比如图像适配器,即显卡,网络适配器,即网卡。
- 这些控制器或者适配器芯片的内部有若干寄存器,CPU即将这些寄存器当做端口来访问,也就是我们之前所讲的端口寻址的目的所在!!
- 外设的输入并不是直接送入内存和CPU,而是送入相关的接口芯片的端口中,接着CPU利用端口寻址读入相关的数据。
- CPU向外设的输出也不是直接送入外设,而是先利用端口寻址送入端口,再由相关芯片送到外设。
- CPU还可以向外设发出控制命令,这些控制命令也先送到相关接口芯片的端口中,然后由芯片根据命令对外设实施控制;
- CPU可以检测到外设发过来的中断信息,引发中断过程,来处理外设的输入;
内中断是指CPU内部发来的中断,外中断指的是CPU外部,即外设发来的中断。
内中断的中断类型码是在CPU内部产生的,外中断的中断类型码是通过数据总线送入CPU的。
外中断源可以分为两类,即可屏蔽中断和不可屏蔽中断,可屏蔽中断是CPU可以不相应的外中断,大部分外中断都是可屏蔽的。***CPU在执行内中断的时候,会先将IF标志位置为0,禁止处理可屏蔽的外中断。意思就是CPU正在干正事呢,别的人别来打搅CPU的好事!!!!***但是当CPU检测到不可屏蔽的中断信息之后,当执行完当前的指令之后,必须立即响应,引发中断过程。
注意,CPU要先执行完当前指令才回去响应中断,但是在设置栈段寄存器和栈偏移寄存器的时候,执行完当前指令给SS复制之后不会去响应中断,而是要等到SP寄存器也复制完毕才会响应中断。
对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以在中断过程中,不需要取中断类型码。
造成内中断产生的情况:
- 除法错误,比如执行DIV指令的时候的除法溢出,终端类型码为0;
- 单步执行,中断类型码为1;
- 执行
into
指令,溢出中断指令,Interrupt If Overflow,中断类型码为4; - 执行
int
指令,该指令的格式是int n
,指令中的n为字节型立即数,是提供给CPU的中断类型码;
虽然现在知道了上面这四种情况会造成中断,但是CPU并不知道,更不知道怎么去处理这些中断,所以我们引入了中断向量表。我们给每一种中断编号,然后绘制一张表,表中存放的是中断编号以及中断的时候,调用的处理程序所在的段地址和偏移地址(子处理程序的入口地址)。
中断向量表在内存中存放,其中存放了256个中断源所对应的中断处理程序的入口。
对于8086PC机,中断向量表指定放在内存地址0处,从内存0000:0000到0000:03FF的1024个单元存放着中断向量表,注意不能放在别处。对于8086PC机,中断向量表必须放在内存0000:0000到0000:03FF中。这是规定,因为8086CPU就从这个地方读取中断向量表。
一个表项占据两个字:高地址字存放段地址,低地址存放偏移地址。
我们可以使用中断类型码在中断向量表中找到中断处理程序的入口。找到之后,设置CS和IP。该过程是由CPU的硬件自动完成的。CPU硬件完成这个工作的过程被称为中断过程。
CPU硬件完成中断过程之后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序。
还有CPU执行完中断处理程序之后,还要返回原来的执行点继续执行指令,所以说我们需要提前将CS:IP中的值压入栈保存。
下面是8086CPU在收到中断信息之后,所引发的中断过程。
- 从中断信息中获得中断类型码;
- 标志寄存器入栈,因为在中断处理程序中可能会改变标志寄存器的值;
- 设置标志寄存器的第8位TF和第9位IF的值为0;
关于标志寄存器的第8位TF和第9位IF:
IF标志:中断允许标志位,IF置为0的(
CLI
)话,禁止其他的可屏蔽中断;如果允许处理可屏蔽中断,则IF置为1(STI
);
- CS的内容入栈;
- IP的内容入栈;
- 从内存地址为中断类型码*4和中断类型码*4 + 2的两个字单元中读取中断处理程序的入口地址设置IP和CS;
CPU在收到中断信息后,如果处理该中断信息,就完成一个由硬件自动执行的中断过程,该过程的目的就是找到处理终端程序的入口地址,改变CS和IP从而执行相应的程序。需要注意的是,我们在执行中断过程中还要做的一件事情就是设置标志寄存器的TF和IF位,在这里我们要设置是否响应外中断,这决定了,CPU在干好事的时候可不可以被打断。
由于CPU是时时刻刻都在运行的,也就是在任何时候CPU都有可能去执行处理中断程序,所以说中断处理程序必须一直存储在内存空间中。而且中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。
中断处理程序的编写方式和子程序大概一致,一下内容是响应的步骤:
-
保存用到的寄存器;
-
恢复中断;
-
回复用到的寄存器;
-
使用
iret
指令返回;#`iret`指令: pop IP pop CS popf# 弹出相应的寄存器群,和中断过程中的pushf是对应关系
编程处理0号中断:
重新编写一个0号中断处理程序,功能是在屏幕的中间显示“Overflow!”,然后返回到操作系统去。
分析:
引发中断过程的时候,CPU会干的事情如下:
- 取得中断类型码0;
- 标志寄存器入栈,TF、IF设置为0;
- CS、IP入栈;
- (IP)=(0*4),(CS)=(0*4+2);
执行中断处理程序的时候:
- 相关处理;
- 向显示缓冲区送字符串“Overflow!”;
- 返回DOS;
由于在内存中0000:0000~0000:03FF,这个大小为1KB的空间是系统存放中断处理程序的入口地址的中断向量表,8086处理256个中断,但是系统中要处理的中断事件没有达到256,在中断向量表中,很多单元是空的。所以我们可以参考将自己编写的中断处理程序放在这一块空的内存中:
0000:0200~0000:02FF
我们还需要做的事情就是去修改0中断向量表,将跳转的地址修改为我们自定义的中断处理程序的地址。
示例代码:
assume cs:code
# 不在这里定义变量是因为当我们的程序结束之后,该处内存就会被释放
# 那么我们自定义的程序就无法输出指定的字符串
# data segment
# db "Overflow!!!!!",'$'
# data ends
code segment
start:
# do0安装程序,设置中断向量表,以及存放我们所编写的程序;
# 当CPU处理中断的时候,是要先停止当前程序的运行的,反而去执行我们所编写的程序;
# 他是在内存中去寻找我们所编写的程序,我们需要去修改中断向量表的表项;
# ---------问题
# 可不可以直接修改中断向量表项将其直接指向我们所自定义的处理中断程序的入口?????
# 这个是可以的,不过我们确定处理中断函数程序的入口很难确定
mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
# mov cx,do代码的长度
# 此处可以利用编译器来计算do代码的长度
# 利用伪指令offset来编译
# - 是编译器识别的运算符号,编译器可以利用它来进行两个常数的相减、
# 汇编编译器可以处理表达式
# 指令:mov ax,(5+3)*5/10,被编译器处理为指令:mov ax,4
mov cx,offset do0end-offset do0
# 设置传输方向为正
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,1000h
mov bh,1
div bh
mov ax,4c00h
int 21H
do0:jmp short do0start
db "Overflow!"
# 显示字符串“overflow!”
# 之所以将字符串放在这里,是因为当我们的程序结束的时候,我们其中存放数据的内存都会被一起释放
# 那么我们之后再有这种除法的时候,就不会输出同样的字符串,相当于一次修改向量表和永久修改向量表的区别
do0start:
mov ax,cs
mov ds,ax
mov si,202h
mov cx,9
s:
mov dl,[si]
mov ah,02h
int 21H
# 此处注意:
# 此处也是一种中断,第32号中断,该中断会根据ah的值得不同去执行不一样的功能
# 也就是第32号中断对应的处理中断程序中大体是是一个switch语句
# 根据AH寄存器中的值而去做不同的事情;
inc si
loop s
mov ax,4c00h
int 21H
do0end:
nop
code ends
end start
通过下图中可以看到,我们执行程序之后退出后,相应内存中仍然存储着我们的相关数据,输出的字符串也仍在其中:
CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1的话,则产生单步中断,引发中断过程,单步中断的终端类型码是1,引发的中断过程为:
- 取得中断类型码1;
- 标志寄存器入栈,TF、IF设置为0;
- CS、IP入栈;
- (IP)= (1*4),(CS)=(1*4+2);
我们使用debug调试程序的时候,如果使用t命令执行指令时,debug将TF设置为1,使得CPU工作于中断方式下,CPU执行完这一条指令之后就会引发单步中断,执行单步中断的中断处理程序,所有的寄存器的内容显示在屏幕中,并且等待输入命令。
接下来有一个问题是,现在标志寄存器的TF位仍然为1,所以说进入处理中断程序之后,执行完一条指令之后引发单步中断,转而去执行中断处理程序,注意处理中断程序也是由一条条指令所组成的,所以说当执行一条指令之后又会引发单步中断,转而继续去执行中断处理程序········
会发现程序会陷入一种莫名奇妙的循环当中········
所以说我们在进入处理中断程序的之前,需要设置TF为0.从而避免CPU执行中断处理程序的时候发生单步中断。
再看一下中断过程:
- 取得中断类型码N;
- 标志寄存器入栈,TF=0(避免执行中断处理程序的时候发生单步中断),IF=0(禁止处理外中断);
- CS、IP入栈;
- (IP)=(N*4),(CS)=(N*4+2);
一般情况下,CPU在执行当前指令的时候,如果检测到中断信息,就会响应中断,引发中断过程。但是还有一些情况下,CPU执行完当前指令之后,即便发生了中断,也不会响应,比如说下面这一种情况:
在执行完向SS寄存器传送数据的指令之后,即便发生中断,CPU也不会继续响应。这是因为SS:SP联合指向栈顶,而对他们的设置应该连续完成,否则的话SS:SP会指向错误的栈顶。所以说我们设置在执行完设置SS的指令之后,不响应中断。
比如说我们可以利用debug写入以下指令:
mov ax,1000h
mov ss,ax
mov sp,0
mov ax,0
我们对其进行调试:
我们可以看到在单步调试MOV SS,AX
指令的时候,本应该停止,接着去执行MOV SP,0
,但是事实上并未这样去执行,反而直接跳到了MOV SP,0
指令的下一条指令。
上一节中我们介绍了内中断,由int
指令引发的中断,上一节中介绍的大都是由于异常所引发的中断,该节的int
指令是主动去调用任何一个中断的中断处理程序。
CPU指令int n
指令,相当于引发一个中断类型码为n的中断过程,执行过程如下:
- 取中断类型码n;
- 标志寄存器入栈,IF=0,TF=0,我们一般使用指令
pushf
来完成这个过程,pushf
指令对应的是中断处理程序结束指令iret:pop IP,pop CS,popf
; - CS、IP入栈;
- (IP)=(n*4),(CS)=(CS*4+2);
从此处转去执行n号中断的中断处理程序。
BIOS(Basic Input Output System,基本输入输出系统) || DOS(Disk Operating System,磁盘操作系统)
在系统版的ROM(通常我们的硬盘也称为ROM)存放着一套程序,称为BIOS,BIOS中主要包含一下部分内容:
- 硬件系统的检测和初始化程序;
- 外部中断和内部中断的中断例程;
- 用于对硬件设备进行I/O设备操作的中断例程;
- 其他和硬件系统相关的中断例程;
从操作系统的角度来看,DOS的中断例程就是操作系统向程序员提供的编程资源,BIOS和DOS在所提供的中断例程中包含了许多子程序,这些子程序实现了程序员在编程的时候经常使用的功能,我们可以使用int
指令直接调用BIOS和DOS提供的中断例程,来完成某些工作。
和硬件设备相关的DOS中断例程中,一般都调用了BIOS的中断例程,也就是说BIOS是最底层的操作系统。
---------------------------------------------------BIOS和DOS中断例程的安装过程--------------------------------------------------
- 开机后,CPU加电后,初始化(CS)=0FFFFH,(IP)=0,自动从FFFF:0单元开始执行程序。在该处有一个调转指令,会转去执行BIOS中的硬件系统检测和初始化程序。
- 初始化程序将建立BIOS所支持的中断向量;
- 硬件系统检测和初始化完成之后,调用
int 19h
进行操作系统的引导。从此将计算机交由操作系统控制; - DOS启动之后,除了完成其他工作之外,还将它所提供的中断例程装入内存,并建立响应的中断向量;
一般来说,一个供程序员调用的中断例程中往往包含多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号。
比如说我们最经常使用的21号中断,根据ah里面的值,进行数据的读取,另外
int 21h
中断例程是DOS所提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。
外中断源一共可以分为以下两类:
- 可屏蔽中断;
在内中断中将IF置为0的原因就是在进入中断处理程序之后,禁止一切其他的可屏蔽中断。
当然了,如果在中断处理程序中需要处理可屏蔽程序,可以将指令将IF置为1:
sti # 设置IF=1;
cli # 设置IF=0;
- 不可屏蔽中断;
不可屏蔽中断是CPU必须响应的外中断,不可屏蔽中断的中断类型码固定为2,所以说中断过程中,不需要取中断类型码。则不可屏蔽中断过程为:
- 标志寄存器入栈,IF=0,TF=0;
- CS、IP入栈;
- (IP)=(8),(CS)=(0AH)
键盘上每一个键相当于一个开关,键盘中有一个芯片对键盘的每一个键的开关状态进行扫描。
当我们按下一个键的时候,开关接通,该芯片就会产生一个扫描码,该扫描码说明该键的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。
松开按下的键的时候,也会产生一个扫描码,说明了松开的键在键盘上的位置,送开按键时产生的扫描码也被送入60h端口。
按下键盘产生的扫描码称之为通码,松开键产生的扫描码称为断码。扫描码长度为一个字节,通码的第7位为0,断码的第7未为1,即:
断码=通码+80h
比如说g键的通码为 22h (0010_0010),断码为 a2h(1010_0010)。
键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。注意该信息是从总线传送给CPU的。CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9
中断类型。
(程序中一般默认都是允许引发可屏蔽中断,除了进入中断的时候需要设置IF=0,来屏蔽所有的可屏蔽中断信息)
BIOS提供了int 9
中断例程,(从这里可以看出,BIOS和DOS搭配提供各种必须的中断例程),用来处理基本的键盘输入处理:
- 读出60h端口中的扫描码;
- 如果是字符键的扫描码,将该扫描码和它对应的字符
asscii
码送入内存中的BIOS键盘缓冲区;如果时控制键(Ctrl)和切换键(CapLock
)扫描码,则将其转化为状态字节写入内存中存储状态字节的单元; - 对键盘系统进行相关的控制,比如说,向相关的芯片发出应答消息;
键盘输入的处理过程:
a. 键盘产生扫描码;--------------------------------->硬件系统完成
b. 扫描码送入60h端口;----------------------------->硬件系统完成
c. 引发9号中断;-------------------------------------->硬件系统完成,键盘上的芯片会利用总线向CPU传送中断码
d. CPU执行int 9
中断例程处理键盘输入(关键就是将输入的字符放在BIOS缓冲区);----------->我们可以自定义
我们编写9号中断例程的功能:
-
从60h端口读取键盘输入字符的扫描码,注意此时仅仅是扫描码,并不是ASCII码;
-
调用BIOS的
int 9
中断例程,处理其他硬件细节,此处很重要,因为我们现在还没有获得字符的ASCII码,所以我们需要利用原来的int 9
中断例程来将该字符的扫描码和它对应的字符ASCII码送入内存的BIOS键盘缓冲区中,这样的话,我们就可以得到键盘输入的字符了; -
判断是否为ESC的扫描码,如果是的话,改变字体的颜色并返回;如果不是直接返回;
实现细节:
细节1:
我们现在要做的事情是编写新的int 9
中断例程,所以说中断向量表中的int 9
中断例程的入口地址需要改写为我们写的中断处理程序的入口地址。
但是!但是!但是!但是! 我们有的时候还需要去调用原来的int 9
中断例程,这个时候怎么办呢?我们需要先将原来的int 9
中断例程的入口地址保存起来,这样我们在需要调用的时候,我们才能找到原来的中断例程的入口。
在本次的实验中,我们将原来int 9
中断例程的偏移地址和段地址存储在ds:[0]
和ds:[2]
单元,这样我们在需要调用原来的int 9
中断例程时候,就可以在ds:[0]
和ds:[2]
单元找到原来的入口。
细节2:
我们现在已经保存了原来的入口地址了,我们该如何去模拟int 9
来调用原来的中断例程呢?
我们先来看int 9
指令在执行的时候,CPU所进行的工作:
a. 取出中断类型码;
b. 标志寄存器入栈;
c. IF=0,TF=0;
d. CS、IP入栈;
e. (IP)=(n*4),(CS)=(n*4+2);
对照上面的过程,我们模拟调用int 9
原来的中断例程:
a. 标志寄存器入栈,pushf
,注意我们已经知道了中断例程的入口地址,所以说我们不再需要取出中断类型码;
b.TF=0,IF=0
c. CS、IP入栈;
d. (IP)=((ds)*16+0),(CS)=((ds)*16+2),该步骤做的事情就是改变IP和CS寄存器,内容是我们预先存储在ds:[0]
里面的内容;
后两个步骤,我们可以使用call dword ptr ds:[0]
来实现。
示例:
# 编写并安装新的int 9中断例程
assume cs:code
stack segment
db 128 dup(0)
stack ends
data segment
dw 0,0
data ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
mov ax,data
mov ds,ax
mov ax,0
mov es,ax
# 保存原来int 9中断例程的入口地址保存在ds:0,ds:2的子单元中
# 因为我们改变之后,需要将其改变回来
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
# 修改原来int 9中断例程的入口地址,设置为我们自定义的函数地址
mov word ptr es:[9*4],offset int9
mov es:[9*4+2],cs
mov ax,0b800h
mov es,ax
mov al,'a'
mov ah,71h
s:
mov es:[160*12+40*2],ax
call delay
inc al
cmp al,'z'
jna s
mov ax,0
mov es,ax
# 将int 9中断例程的入口地址重置为原来的
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,0010h
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
# -------------------新的int 9中断例程-------------------
int9:
push ax
push bx
push es
in al,60h
# 第一个pushf是为了响应后面调用原来的int 9中断例程程序结尾处的iret指令;
# 第二个pushf就是为了修改标志寄存器的TF、IF位所做的铺垫;
pushf
pushf
# # 设置IF和TF为0
pop bx
and bh,11111100b
push bx
popf
# 对int 指令进行模拟,调用原来的int 9中断例程
call dword ptr ds:[0]
# 比较键盘输入的字符,如果等于1的话,说明键盘输入了Esc
cmp al,1
jne int9ret
# 改变字符颜色
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
mov ah,0
int 16h
mov dl,al
mov ah,02h
int 21h
# mov ah,0cah
# mov es:[160*12+40*2+1],ah
int9ret:
pop es
pop bx
pop ax
iret
code ends
end start