在12章学习过两种中断,分别是
- 除法溢出产生的0号中断
- 检测到标志寄存器的TF位为1,产生的单步中断
这一章,学习由于int
指令引发的中断
13.1 int指令
int
指令的格式为:
int n ; n为中断类型码
该指令的功能是引发中断过程
CPU执行int n
指令,相当于引发一个n号中断的中断过程,执行过程如下。
- 取中断类型码n;
- 标志寄存器入栈,IF=0,TF=0;
- CS和IP入栈
- (IP)=(n4), (CS)=(n4+2)
从此转去执行n号中断的中断处理程序。
可以在程序中使用int
指令调用任何一个中断的中断处理程序。例如,下面的程序
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov byte ptr es:[12*160+40*2],'!'
int 0
code ends
end start
程序虽然没有做除法,但是在结尾处使用了int 0
指令,这个指令的功能是和除法溢出产生的中断功能是一样的。
一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。我们在编程的时候,可以用int
指令调用这些子程序,我们将中断处理程序称为中断例程。
13.2 编写供应用程序调用的中断例程
前面,我们已经编写过中断0的中断例程了,现在我们讨论可以供应用程序调用的中断例程的编写方法。
问题一:编写、安装中断7ch的中断例程
- 功能:求一个字型数据的平方
- 参数:(ax)=要计算的数据
- 返回值:dx、ax中存放结果的高16位和低16位
- 应用举例:求2*3456^2
assume cs:code
code segment
start: mov ax,3456 ; (ax)=3456
int 7ch ; 调用7ch中断例程,计算(ax)^2
add ax,ax
adc dx,dx ; 2*(ax)^2,并dx:ax存放结果
mov ax,4c00h
int 21h
code ends
end start
分析一下,我们要做以下3部分工作
- 编写实现求平方功能的程序;
- 安装程序,将其安装在0:200处
- 设置中断向量表,将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程
安装程序如下
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset sqr ; 设置ds:si指向源地址
mov ax,0
mov es,ax
mov di,200h ; 设置es:di指向目的地址
mov cx,offset sqrend-offset sqr ; 设置cx为传输长度
cld ; 设置传输方向为正
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h
mov word ptr es:[7ch*4+2],0
mov ax,4c00h
int 21h
sqr:mul ax
iret
sqrend:nop
code ends
end start
注意,在中断例程sqr
的最后,要使用iret
指令。用汇编语法描述,iret
指令的功能为:
pop IP
pop CS
popf
CPU执行完int 7ch
指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈中,在执行完中断例程后,应该用iret
指令恢复int 7ch
执行前的标志寄存器和CS、IP的值,从而接着执行应用程序。
问题二:编写、安装中断7ch的中断例程。
- 功能:将一个全是字母,以0结尾的字符串,转化为大写
- 参数:ds:si 指向字符串的首地址
- 应用举例:将data段中的字符串转化为大写
assume cs:code
data segment
db 'conversation',0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0 ; 源地址ds:si = data:0
int 7ch ; 引起7c中断例程
mov ax,4c00h
int 21h
code ends
end start
安装程序如下。
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset capital
mov ax,0
mov es,ax
mov di,200h
mov cs,offset capitalend-offset capital
cld
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h
mov word ptr es:[7ch*4+2],0
mov ax,4c00h
int 21h
capital: push cx
push si
change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok: pop si
pop cx
iret
capitalend: nop
code ends
end start
在中断例程capital
中用到了寄存器si和cx,编写中断例程和编写子程序的时候具有同样的问题,就是要避免寄存器的冲突。应该注意例程中用到的寄存器的值的保存和恢复。
13.3 对int、iret和栈的深入理解
问题:用7ch中断例程完成loop
指令的功能。
loop s
的执行需要两个信息,循环次数和 s 的位移。所以,7ch中断例程要完成loop
指令的功能,也需要这两个信息作为参数。我们用cx
放循环的次数,用bx
存放位移。
应用举例:在屏幕中间显示80个”!”。
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov di,160*12
mov bx,offset s-offset se ; 设置从标号se到标号s的转移位移
mov cx,80
s: mov byte ptr es:[di],'!'
add di,2
int 7ch ; 进入7ch例程,如果(cx)≠0,转移到标号s处
se: nop
mov ax,4c00h
int 21h
code ends
end start
在上面的程序中,使用int 7ch
调用 7ch 中断例程进行转移,用bx传递转移的位移。
分析:为了模拟loop
指令,7ch中断例程应具备以下的能力
dec cx
;- 如果(cx)≠0,转移到标号s处执行,否则向下执行
下面我们分析7ch中断例程如何实现到目的地址的转移
- 转到标号s显然应设%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E6%AE%B5%E5%9C%B0%E5%9D%80#card=math&code=%28CS%29%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E6%AE%B5%E5%9C%B0%E5%9D%80&id=mXDpz),%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E5%81%8F%E7%A7%BB%E5%9C%B0%E5%9D%80#card=math&code=%28IP%29%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E5%81%8F%E7%A7%BB%E5%9C%B0%E5%9D%80&id=vAJRl)
- 那么,中断例程如何得到标号s的段地址和偏移地址呢?
int 7ch
引发中断过程后,进入7ch中断例程,在中断过程中,当前的标志寄存器、CS和IP都要压栈,此时压入的CS和IP中的内容,分别是调用程序的段地址和int 7ch
后一条指令的偏移地址。
可见,在中断例程中,可以从栈里取得标号 s 的段地址和标号 se的偏移地址,而用标号 se的偏移地址加上bx中存放的转移位移就可以得到标号 s的偏移地址。 - 现在知道,可以从栈中直接和间接地得到标号 s 的段地址和偏移地址,那么如何用它们设置 CS:IP呢?
可以利用iret
指令,我们将栈中的 se 的偏移地址加上bx中的转移位移,则栈中的 se的偏移地址就变为了 s的偏移地址。我们再使用iret
指令,用栈中的内容设置CS、IP,从而实现转移到标号 s处。
7ch中断例程如下
lp: push bp
mov bp,sp
dec cx
jcxz lpret
add [bp+2],bx
lpret: pop bp
iret
因为要访问栈,使用了bp,在程序开始处将bp 入栈保存,结束时出栈恢复。当要修改栈中 se的偏移地址的时候,栈中的情况为:栈顶处时 bp原来的数值,下面是 se的偏移地址,再下面时 s 的段地址,再下面是标志寄存器的值。
而此时,bp中为栈顶的偏移地址,所以*16%2B(bp)%2B2#card=math&code=%28ss%29%2A16%2B%28bp%29%2B2&id=Y1VOV)处为 se 的偏移地址,将它加上 bx 中的转移位移就变为 s 的偏移地址。最后用iret
出栈返回,CS:IP即从标号 s 处开始执行指令。
如果%3D0#card=math&code=%28cx%29%3D0&id=EBF1S),则不需要修改栈中 se 的偏移地址,直接返回即可。CPU从标号 se 处向下执行指令。
13.4 BIOS和DOS所提供的中断例程
在系统板的ROM种存放着一套程序,称为BIOS(Basic Input Output System,基本输入输出系统),BIOS种主要包含以下内容
- 硬件系统的检测和初始化程序;
- 外部中断(第15章中进行讲解)和内部中断的中断例程;
- 用于对硬件设备进行 I/O 操作的中断例程
- 其他和硬件系统相关的中断例程
操作系统DOS也提供了中断例程,从操作系统的角度来看,DOS的中断例程就是操作系统向程序员提供的编程资源。
BIOS 和 DOS 在所提供的中断例程中包含了许多子程序,这些子程序实现了程序员在编程的时候经常需要用到的功能。程序员在编程的时候,可以用**int**
指令直接调用 BIOS 和 DOS 提供的中断例程,来完成某些工作。
和硬件设备相关的 DOS 中断例程中,一般都调用了 BIOS 的中断例程。
13.5 BIOS和DOS中断例程的安装过程
前面的课程中,我们都是自己编写中断例程,将他们放到安装程序中,然后运行安装程序,将它们安装到指定的内存中。此后,别的应用程序才可以调用。
而 BIOS 和 DOS 提供的中断例程是如何安装到内存中的呢?我们下面讲解它们的安装过程。
- 开机后,CPU一加电,初始化 (CS)=0FFFFH, (IP)=0, 自动从FFFF:0 单元开始执行程序。FFFF:0 处有一条跳转指令,CPU执行该指令后,转去执行 BIOS 中的硬件检测和初始化程序。
- 初始化程序将建立 BIOS 所支持的中断向量,即将 BIOS 提供的中断例程的入口地址登记在中断向量表中。注意,对于BIOS所提供的中断例程,只需将入口地址登记在中断向量表中即可,因为它们一直存在在ROM中,一直在内存中存在。
- 硬件系统检测和初始化完成后,调用
int 19h
进行操作系统的引导,从此将计算机交友操作系统控制 - DOS 启动后,除了完成其他工作外,还将它提供的中断例程装入内存,并建立相应的中断向量。
13.6 BIOS中断例程应用
下面我们举几个例子,来看一下 BIOS 中断例程的应用。
int 10h
中断例程是 BIOS 提供的中断例程,其中包含了多个和屏幕输出相关的子程序。
一般来说,一个供程序员调用的中断例程中往往包含多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS 和 DOS提供的中断例程,都用 ah来传递内部子程序的编号。
下面看一下int 10h
中断例程的设置光标位置功能。
mov ah,2 ; 置光标
mov bh,0 ; 第0页
mov dh,5 ; dh中放行号
mov dl,12 ; dl中放列号
int 10h
(ah)=2
表示调用第 10h 号中断例程的 2 号子程序,功能为设置光标位置,可以提供光标的位置,分别为
- 行号:80*25字符模式下(0~24)
- 列好:80*25字符模式下(0~79)
- 页号
上述的代码通过(bh)=0, (dh)=5, (dl)=12
将光标设置到了第0页,第5行,第12列。
bh 中页号的含义:内存地址空间中,B8000H~BFFFFFH 共占 32KB 的空间,为80*25彩色字符模式的显示缓冲区,一屏的内容在显示缓冲区共占 4000 个字节。
显示缓冲区分为 8 页,每页 4KB,显示器可以显示任意一页的内容。一般情况下,显示第0页的内容。也就是说,通常情况下,B8000H~B8F9FH 中的 4000个字节的内容将出现在显示器上。
再看一下 int 10h
中断例程的在光标位置显示字符功能
mov ah,9 ; 在光标位置显示字符
mov al,'a' ; 字符
mov bl,7 ; 颜色属性
mov bh,0 ; 第0页
mov cx,3 ; 字符重复个数
int 10h
(ah)=9
表示调用第10号中断例程的9号子程序,功能为在光标位置显示字符,可以提供要显示的字符、颜色属性、页号、字符重复个数作为参数。
bl
中的颜色属性格式如下所示:
可以看出,和现存中的属性字节的格式相同。
编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的 ‘a’
assume cs:code
code segment
mov ah,2 ; 置光标
mov bh,0 ; 第0页
mov dh,5 ; dh中放行号
mov dl,12 ; dl中放列好
int 10h
mov ah,9 ; 在光标位置显示字符
mov al,'a' ; 字符
mov bl,11001010b ; 颜色属性
mov bh,0 ; 第0页
mov cx,3 ; 字符重复个数
int 10h
mov ax,4c00h
int 21h
code ends
end
注意,闪烁的效果必须在全屏DOS方式下才能看到。
13.7 DOS中断例程应用
int 21h
中断例程是 DOS 提供的中断例程,其中包含了 DOS提供给程序员在编程时调用的子程序。
我们前面使用的是 int 21h
中断例程的 4ch号功能,即程序返回功能,如下:
mov ah,4ch ; 程序返回
mov al,0 ; 返回值
; 或者为 mov ax,4c00h
int 21h
; (ah)=4ch 表示调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数
我们看一下int 21h
中断例程在光标位置显示字符串的功能
ds:dx 指向字符串 ; 要显示的字符串需要用 $ 作为结束符
mov ah,9 ; 功能号9,表示在光标位置显示字符串
int 21h
; (ah)=9表示调用第 21h 号中断例程的 9 号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数
编程:在屏幕的5行12列显示字符串 “Welcom to masm!”
assume cs:code
data segment
db 'Welcome to masm','$'
data ends
code segment
start: mov ah,2 ; 置光标
mov bh,0 ; 第0页
mov dh,5 ; dh中放行号
mov dl,12 ; dl中放列好
int 10h
mov ax,data
mov ds,ax
mov dx,0 ; ds:dx指向字符串的首地址data:0
mov ah,9
int 21h
mov ax,4c00h
int 21h
code ends
end start
上述程序在屏幕的 5 行 12 列显示字符串Welcom to masm!
,直到遇见$
($
本身不显示,知识起到边界的作用)。
如果字符串比较长,遇到行尾,程序会自动转到下一行开头继续显示;如果到了最后一行,还能自动上卷一行。
DOS系统为程序员提供了许多可以调用的子程序,都包含在**int 21h**
中断例程中。