1.写在前面
前面我介绍了内中断的一些知识,通过修改中断程序和中断向量表,来达到修改0号中断程序,使得除法溢出的时候,屏幕上打印overflow!今天我们来讲下int指令和端口。
2.本篇博客的概述
3.int指令
3.1int指令
int指令的格式为:int n, n为中断类型码,它的功能就是引发中断过程。
CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下:
- 取中断类型码n
- 标志寄存器入栈,IF=0,TF=0;
- CS、IP入栈
- (IP)=(n4),CS=(n4+2)
从此处转去执行n好中断的中断处理程序。
例子:
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
上面的程序,将在屏幕中间显示一个"!",然后显示"Divide overflow"后返回到系统中。"!"是我们编程显示的,而"Devide overflow"是我们自己结尾调用的int 0。
一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。我们在编程的时候,可以用int指令调用这些子程序。当然,也可以自己编写一些中断处理程序供别人使用。以后,我们可以将中断处理程序简称为中断例程。
3.2编写供应用程序调用的中断例程
本节我们主要看两个问题。
问题一:编写、安装中断7ch的中断例程。
功能:求一Word型数据的平方。
参数:(ax)=要计算的数据
返回值:dx、ax中存放结果的高26位和低16位。
应用举例:求2*3456^2
assume cs:code
code segment
start:mov ax,3456 ;(ax)=3456
int 7ch ;调用中断7ch的中断例程,计算ax中的数据的平方
add ax,ax
adc dx,dx ;dx:ax存放结果,将结果乘以2
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
CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈中,在执行完中断例程后,应该用iret指令恢复int 7ch执行前的标志寄存器和CS、IP的值,从而接着执行应用程序。
int指令和iret指令的配合使用与call指令和ret指令的配合使用具有相似的思路。
问题二:编写、安装中断7ch的中断例程。
功能:将一个全是字母,以0结尾的字符串,转化为小写。
参数:ds:si指向字符串的首地址。
应用举例:将data段中的字符串转换为大写。
assume cs:code
data segemnt
db 'conversation',0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
int 7ch
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 cx,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,4c000h
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
3.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 ;如果(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显然应设(CS)=标号s的段地址,(IP)=标号s的偏移地址。
-
那么,中断例程如何得到标号s的段地址和偏移地址呢?
Int 7ch引发中断过程后,进入7ch中断例程,在中断过程中,当前的标志寄存器、CS和IP都要压栈,此时压入的CS和IP中内容,分别是调用程序的段地址(可以认为是标号s的段地址)和int 7ch后一条指令的偏移地址(即标号se的偏移地址)。
可见,在中断例程中,可以从栈里取得标号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中为栈顶的偏移地址,所以((ss)*16+(bp)+2)处为se的偏移地址,将它加上bx中的转移位移就变为s的偏移地址,最后用iret出栈返回,CS:IP即从标号s处开始执行指令。
3.4BIOS和DOS所提供的中断例程
BIOS(基本输入输出系统),主要包含以下几部分内容:
- 硬件系统的检测和初始化程序
- 外部中断和内部中断的中断例程
- 用于对硬件设备进行I/O操作的中断例程
- 其他和硬件系统相关的中断例程。
3.5BIOS和DOS中断例程的安装过程
BIOS和DOS提供的中断例程是如何安装到内存中?
- 开机后,CPU一加电,初始化(CS)=0FFFH,(IP)=0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条跳转指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
- 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。注意,对于BIOS所提供的中断例程,只需要将入口地址登记在中断向量表中即可,因为它们是固化到ROM中的程序,一直在内存中存在。
- 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
- DOS启动后,除完成其他的工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
3.6BIOS中断例程应用
Int10h 中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出的相关子程序。
一般来说,一个供程序员调用的中断例程中往往包含多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS和DOS提供的中断例程,都用ah来传递内部的子程序的编号。
下面看一下int 10h中断例程的设置光标位置功能。
mov ah,2 ;置光标
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,15 ;dl中放列号
int 10h
(ah)=2表示调用第10h号中断例程的2号子程序,功能为设置光标位置,可以提供光标所在的行号(80*25子模式下:024)、列号(80*25子模式下:079)和页号作为参数。
bh中页号的含义:内存地址空间中,B8000H~BFFFFH共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表示调用的第10h号的中断例程的9号子程序,功能为在光标位置显示字符,可以提供要显示的字符,颜色属性,页号字符重复个数作为参数。
bl中的颜色属性的格式如下:
7 6 5 4 3 2 1 0
含义 BL R G B I R G B
闪烁 背景 高亮 前景
编程:在屏幕的第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
运行的结果如下:
3.7DOS中断例程应用
int21h中断例程是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。
我们前面一直使用的是int 21h中断例程的4ch号功能,即程序返回功能,如下:
mov ah,4ch ;程序返回
mov al,0 ;返回值
int 21h
(ah)=4ch表示调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数。
我们看一下int 21h中断例程在光标位置显示字符串的功能:
ds:dx指向字符串 ;要显示的字符串需要'$'作为结束符
mov ah,9 ;功能号9,表示在光标位置显示字符串
int 21h
(ah)=9表示调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数。
编程:在屏幕的第5行13列显示字符串"Welcome 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
运行的结果如下:
4.端口
在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下3钟芯片
- 各种接口卡上的接口芯片,它们控制接口卡进行工作。
- 主板上的接口芯片,CPU通过它们对部分外设进行访问。
- 其他芯片,用来存储相关的系统信息,或进行相关的输入输出的处理。
这些芯片中,都有一组可以由CPU读写的寄存器。这些寄存器,它们在物理上可能处于不同的芯片中,但是它们在以下两点上相同。
- 都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的;
- CPU对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写命令。
可见,从CPU的角度,将这些寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。每一个端口在地址空间中都有一个地址。
CPU可以直接读写以下的3个地方的数据
- CPU内部的寄存器
- 内存单元
- 端口
4.1端口的读写
在访问端口的时候,CPU通过段口地址来定位端口。因为端口所在的芯片和CPU通过 总线相连,所以,端口地址和内存地址一样,通过地址总线来传送。
对端口的读写不能用mov、push、pop等内存读写指令。端口的读写指令只有两条in和out。分别用于从端口读取数据和往端口写入数据。
先来看一下CPU执行内存访问指令和端口访问指令时候,总线上的信息:
访问内存:
mov ax,ds:[8];假设执行前(ds)=0
执行时与总线相关的操作如下所示
- CPU通过地址线将地址信息8发出;
- CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
- 存储器将8号单元中的数据通过数据线送入CPU。
访问端口:
in al,60h ;从60h号端口读入一个字节
执行时与纵向相关的操作如下:
- CPU通过地址线将地址信息60h发出;
- CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据;
- 端口所在的芯片将60h端口中的数据通过数据线送入CPU
注意,在in和out指令中,只能使用ax或al来存放从端口中读入的数据火药发送到端口中的数据。访问8位端口时用al,访问16位端口时用ax。
4.2CMOS RAM芯片
PC机中,有一个CMOS RAM芯片,一般简称为CMOS。特性如下:
- 包含一个实时时钟和一个有128个存储单元的RAM存储器。
- 该芯片靠电池供电。所以,关机后其内部的实时钟仍可正常工作,RAM中信息不会丢失。
- 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息
- 该芯片内部有两个端口,端口地址70h和71h。CPU 通过这两个端口来读写CMOS RAM。
- 70h为地址端口,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据。或要写入到其中的数据。
4.3shl和shr指令
shl是逻辑左移指令,它的功能为:
- 将一个寄存器或内存单元中的数据向左移位;
- 将最后移出的一位写入CF中;
- 最低位用0补充。
指令:
mov al,01001000b
shl al,1 ;将al中的数据左移一位
执行后(al)=10010000b,CF=0
我们来看一下shl al,1的操作过程
1.左移
原数据: 01001000
左移后: 01001000
2.将最后移出的一位写入CF中
原数据: 01001000
左移后: 1001000 CF=0
3.最低位用0补充
原数据: 01001000
左移后: 10010000
如果移动的位数大于1时,必须将移动的位数放在cl中。逻辑左移一位,相当于执行X=X*2
shr是逻辑右移指令,它的功能为:
- 将一个寄存器或内存单元中的数据向右移位;
- 将最后移出的一位写入CF中;
- 最高位用0补充。
指令:
mov al,10000001b
shr al,1 ;将al中的数据右移一位
执行后(al)=01000000b,CF=1
如果移动的位数大于1时,必须将移动的位数放在cl中。逻辑右移一位,相当于执行X=X/2
4.4CMOS RAM中存储的时间信息
在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度为1个字节,存放单位为:
秒:0 分:2 时:4 日:7 月:8 年:9
这些数据以BCD码的方式存放。BCD码是以4位二进制数表示十进制数码的编码方法。一个字节可表示两个BCD码,高4位的BCD码表示十位,低4位的BCD码表示个位。
编程,在屏幕中间显示当前的月份。
分析,这个程序主要做以下两部分的工作
-
从CMOS RAM的8号单元读取当前月份的BCD码
要读取CMOS RAM的信息,首先要向地址端口70h写入要访问的单元的地址。
mov al,8 out 70h,al
然后从数据端口71h中取得指定单元中的数据:
int al,71h
-
将用BCD码表示的月份以十进制的形式显示在屏幕上
我们可以看出,BCD码值=十进制数码值,则BCD吗值+30h=十进制数对应的ASCII码
从CMOS RAM的8号单元读出的一个字节中,包含了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位,低4位的BCD码表示个位。
我们需要进行以下两步工作。
将从CMOS RAM的8号单元中读出的一个字节,分为两个表示BCD码值的数据
mov ah,al ;al中为从CMOS RAM的8号单元中读出的数据 mov cl,4 shr ah,cl ;ah中为月份的十位数码值 and al,00001111b ;al中为月份的个位数码值
显示(ah)+30h 和(al)+30h对应的ASCII码的字符
完整的程序如下:
assume cs:code code segment start: mov al,8 out 70h,al in al,71h mov ah,al mov cl,4 shr ah,cl and al,00001111b add ah,30h add al,30h mov bx,0b800h mov es,bx mov byte ptr es:[160*12+40*2],ah ;显示月份的十位数码 mov byte ptr es:[160*12+40*2+2],al ;显示月份的个位数码 mov ax,4c00h int 21h code ends end start
运行的结果如下:
5.写在最后
本篇博客主要介绍int指令和端口的读写。下篇博客将介绍一下外中断。