本章实验12,要求编写0号中断的处理程序,使得在除法溢出时,在屏幕中间显示字符串“divide error!”,然后返回到DOS。要实现的效果如下图所示:
在编写这个实验程序之前,我们先复习一下本章的重点内容。
1、中断向量表,指向的是中断程序的地址入口。
2、CUP根据中断类型码,确定中断向量表的表项号,然后根据表项号,找到中断程序的地址入口。
3、中断类型码,主要分为:
1)类型码除法错误:0
2)单步执行:1
3)执行into指令:4
4)执行int指令,格式为 int n,指令中的n为节字型立即数,是提供给CUP的中断
4、中断向量表,位于内存地址0处:从内存0000:0000到0000:3FF
5、中断向量表的表项占2个字:高地址字存放段地址,低地址字存放偏移地址。
6、CUP找到中断程序入口地址,然后将原来的CS和IP值保存
7、CPU设置CS和IP,这个过程称为中断过程。
上述内容如下图所示:
其中,CPU将原来的CS和IP保存这步操作,又分为以下几个步骤:
1、标志寄存器入栈(pushf)
2、设置标志寄志器第8位TF和第9位IF值为0
3、CS内容入栈(push cs)
4、IP内容入栈(push ip)
而中断处理程序的编写方法主要是:
1、保存用到的寄存器
2、处理中断
3、恢复用到的寄存器
4、用iret指令返回。iret指令相当于pop ip、pop cs、popf
需注意的是:
1、CPU在收到中断信息后,会由硬件自动执行中断过程,故在编程中不用涉及包括pushf、push cs、push ip等指令。
2、由于本章实验12只涉及除法溢出产生提示信息,并自动返回DOS界面,未涉及中断返回其他程序的问题,故不用iret指令。
3、编写本章实验12整个程序时,可将其中的中断程序代码传送到0000:0200至0000:02FF的内存区。
4、由于除法溢出中断类型码为0,故中断程序入口地址从0*4地址单元有效,段地址在0*4+2的字单元,偏移地址在0*4字单元
在做这个实验之前,王爽老师给出了一个示例程序12.3,代码如下:
=================================================
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
mov cx,offset do0end-offset do0
cld
rep movsb
设置中断向量表
mov ax,4c00h
int 21h
do0: jmp short do0start
db "overflow!"
do0start: mov ax,cs
mov ds,ax
mov si,202h
mov ax,0b800h
mov es,ax
mov di,12*160+36*2
mov cx,9
s: mov al,[si]
mov es:[di],al
mov ah,4
mov es:[di+1],ah
inc si
add di,2
loop s
mov ax,4c00h
int 21h
do0end: nop
code ends
end start
=================================================
而我们现在要做的,就是根据本章的知识点对上述代码进行修改,然后实现笔记最开始图示中的最终效果。现在,我们来对相关代码一段一段分析。
首先,看这段代码:
=================================================
start: mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
mov cx,offset do0end-offset do0
cld
rep movsb
=================================================
1、mov ax,cs
mov ds,ax
将代码段地址CS的值赋给AX,然后将AX的值传送到DS这个寄存器中,使默认的段地址DS的值成为CS的地址;
2、mov si,offset do0,通过offset,获得do0处的偏移地址,然后赋值给si;这样就将ds:si指向了源地址。
3、mov ax,0
mov es,ax
mov di,200h
将段地址0,赋值给AX,然后将AX值传送到ES这个寄存器中,并将偏移值200H,赋值给di。这一段代码的目的,是为了后面执行rep movsb时,将do0到do0end之间的中断程序代码复制到0000:0200做寻址的准备。
因为一般情况下,从0000:0200至0000:02FF的256个字节的空间所对应的中断向量表项都是空的,所以可以将do0到do0end之间的中断程序代码复制到0000:0200这个位置,并将0000:0200这个地址做入中断处理程序的入口地址。
4、mov cx,offset do0end-offset do0。这里涉及两个步骤,第一,通过计算offset do0end 减去 offset do0 获得do0的代码长度;第二,将代码长度赋值给CX,将CX设置为传输长度。
5、 cld
rep movsb
设置中断向量表
cld,是为了设置传输方向为正。
rep movsb,MOVSB 的英文是 move string byte,意思是搬移一个字节,它是把 DS:SI 所指地址的一个字节搬移到 ES:DI 所指的地址上,搬移后原来的内容不变,但是原来 ES:DI 所指的内容会被覆盖,而且在搬移之后 SI 和 DI 会自动向下一个要搬移的地址。
一般而言,通常程序一般并不会只搬一个字节,通常都会重复许多次,如果要重复的话,就得把重复次数 ( 也就是字串长度 ) 先记录在 CX 寄存器,并且在 MOVSB 之前加上 REP 指令,REP 是重复 (repeat) 的意思。
一般而言汇编语言源文件的每一行都只有一个指令,但 REP MOVSB 却可以在同一行写两个指令,当然分开写也是一样的。
在rep movsb指令之后,出现了一段中文“设置中断向量表”,这是一段用中文表示的伪码,真正的代码在原书249页:
=================================================
mov ax,0
mov es,ax
mov word ptr es:[0*4],200h
mov word ptr es:[0*4+2],0
=================================================
需要我们在编写实验12程序时加上这一段代码。这一段的作用,就是把中断程序的入口地址0000:0200,写入中断向量0号表项中。0号表项的地址为0:0,其中0:0字单元存放偏移地址0200,0:2字单元存放段地址0000。
再来看这段代码:
=================================================
do0: jmp short do0start
db "overflow!"
do0start: mov ax,cs
mov ds,ax
mov si,202h
mov ax,0b800h
mov es,ax
mov di,12*160+36*2
mov cx,9
s: mov al,[si]
mov es:[di],al
mov ah,4
mov es:[di+1],ah
inc si
add di,2
loop s
mov ax,4c00h
int 21h
do0end: nop
=================================================
1、do0: jmp short do0start
db "overflow!"
do0: jmp short do0start,通过JMP指令,跳转到除法溢出后显示“overflow!”字符串的代码段do0start;并用db定义了9个字节,对应 "overflow!"的字符串长度。
2、在do0start中:
mov ax,cs
mov ds,ax
mov si,202h
先将code代码段的段地址CS赋值给AX,再将AX传送到DS这个寄存器中,使默认的段地址DS的值成为CS的地址;
然后将偏移地址202h赋值给SI,因为jmp short do0start这条指令占了2个字节,所以 "overflow!"的偏移地址为202h。
3、mov ax,0b800h
mov es,ax
mov di,12*160+36*2
这段代码,是为了向80X25彩色字符模式(原书第188页)输出“overflow!”字符串,段地址为0b800h,用ES寄存器保存段地址0b800h;mov di,12*160+36*2,是将偏移地址12*160+36*2赋值给DI。
其中12代表第12行;160代表80个字(每个字2个字节),低位字节保存字符的ASCII码,高位字节保存字符的属性。
36代表第12行的36个字(每个字2个字节),低位字节保存字符的ASCII码,高位字节保存字符的属性。
这样就可以让“overflow!”字符串位于屏幕居中的位置并显示相应的颜色。
4、s: mov al,[si]
mov es:[di],al
mov ah,4
mov es:[di+1],ah
inc si
add di,2
loop s
这段循环体中,mov al,[si],默认的段寄存器DS结合SI,将“overflow!”字符串读取出来然后通过mov es:[di],al,将每一个字符输送到内存缓冲区的内存低位字节,然后通过 mov ah,4和mov es:[di+1],ah,将高位字节的属性设置为4。SI自加1,DI自加2。通过这段循环体,就完成了“overflow!”字符串向显示缓冲区传送。
有了对上述例程序12.3的理解之后,就可以将其修改为实验12的完整程序,代码如下:
=================================================
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
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 bl,1
div bl
;----------------除法溢出代码
mov ax,4c00h
int 21h
do0: jmp short do0start
db "divide error!"
do0start: mov ax,cs
mov ds,ax
mov si,202h
mov ax,0b800h
mov es,ax
mov di,12*160+36*2
mov cx,13
s: mov al,[si]
mov es:[di],al
mov ah,4
mov es:[di+1],ah
inc si
add di,2
loop s
mov ax,4c00h
int 21h
do0end: nop
code ends
end start
=================================================
在完整的程序里,把除法溢出代码也加了进去,并且把“overflow!”字符串改成了"divide error!",字符串的长度由9改成了13。
把实验12的完整程序代码编译成p1212.exe,在debug下调试运行,进一步加深对这些代码的理解:
先输入r指令观察,会发现当前的CS在076A这个位置,IP是0000,然后输入u 076A:0000进行观察。
可以看到,左边截图的源代码中,mov si,offset do0,在右边的debug显示中变成了 mov si,0034,这意味着中断处理程序代码的位置是从076A:0034开始。
我们来看看076A:0034处以后的指令是什么,输入u 076A:0034进行观察。
可以看到,左边截图的源代码中,从do0: jmp short do0start开始,对应了右边的debug显示区域中的代码内容。
但这时候,这些代码只是一些数据,在没有被复制到0000:0200处并执行完之前,还不能称之为中断处理程序(原书第243页)。
再来看看在debug中,当前0000:0200处是什么内容,输入d 0000:0200观察:
除了一大堆0,现在是没有其他数据的。接下来,在debug中用t指令,执行 从mov ax,cs到 rep movsb的代码。
当CX归0的时候,再输入d 0000:0200观察:
显然,从076A:0034开始的do0中断处理程序代码已全部复制到0000:0200以后的位置。再输入U 0000:0200观察:
显然中断程序已经在内存中0000:0200安置完成,当除法溢出产生时,CPU会执行这一内存区域的中断处理程序。