实验10 编写子程序
在这次实验中,我们将要编写3个子程序,通过它们来认识几个常见的问题和掌握解决这些问题的方法。同前面的所有实验一样,这个实验是必须独立完成的,在后面的课程中,将要用到这个实验中编写的3个子程序。
1.显示字符串
问题
显示字符串是现象工作中经常用到的功能,应该编写一个通用的子程序来实现这个功能。
我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。
子程序描述
名称:show_str
功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
参数:(dh)=行号(取值范围024),(dl)=列号(取值范围079),
(cl)=颜色,ds:si指向字符串的首地址
返回:无
应用举例:在屏幕的8行3列,用绿色显示data段中的字符串。
assume cs:code
data segment
db ‘Welcome to masm!’, 0
data ends
code segment
start: mov dh, 8
mov dl, 3
mov cl, 2
mov ax, data
mov ds, ax
mov si, 0
call show_str
mov ax, 4c00h
int 21h
show_str: ;
;
;
code ends
end start
提示
(1)子程序的入口参数是屏幕上的行号和列号,注意在子程序内部要将它们转化为显存中的地址,
首先要分析一下屏幕上的行列位置和显存地址的对应关系 ;
(2)注意保存子程序中用到的相关寄存器;
(3)这个子程序的内部处理和显存的结构密切相关,但是向外提供了与显存结构无关的接口。
通过调用这个子程序,进行字符串的显示时可以不必了解显存的结构,为编程提供了方便。在实验中,
注意体会这种设计思想。
assume cs:code
data segment
db 'Welcome to masm!',0
db 'Welcome to masm!',0
data ends
code segment
start:
mov dh,8
mov dl,3
mov cl,2
mov ax,data
mov ds,ax
mov si,0
call show_str
mov dh,10
mov dl,3
mov cl,5
mov ax,data
mov ds,ax
mov si,17
call show_str
mov ax,4c00h
int 21h
;功能 在屏幕 指定位置 用指定颜色 显示一个以0结尾的字符串
;参数 dh行号 dl列号 cl颜色 ds:si指向字符串的首地址
show_str:
push ax
push bx
push es
push si
mov ax,0b800h
mov es,ax
mov ax,2 ;获取偏移量 保存到bx中 bx = 2*dl + 160*dh
mul dl
mov bx,ax ;bx保存 dl*2的值
mov ax,160
mul dh
add bx,ax ;bx保存160*dh+ax的值
mov al,cl
mov cl,0
change:
mov ch,ds:[si]
jcxz ok
mov es:[bx],ch
mov es:[bx+1],al
add bx,2
inc si
jmp short change
ok:
pop si
pop es
pop bx
pop ax
ret
code ends
end start
2.解决险法溢出的问题
问题
前面讲过,div指令可以做除法。当进行8位除法的时候,用al存储结果的商,ah存储结果的余数;
进行16位险法的时候,用ax存储结果的商,dx存储结果的余数。可是,现在有一个问题,
如果结果的商大于al或ax所能存储的最大值,那么将如何?
比如,下面的程序段:
mov bh, 1
mov ax, 1000
div bh
进行的是8位除法,结果的商为1000,而1000在al中放不下。
又比如,下面的程序段:
mov ax, 1000H
mov dx, 1
mov bx, 1
div bx
进行的是16位除法,结果的商为11000H,而11000H在ax中存放不下。
我们在用div指令做除法的时候,很可能发生上面的情况:结果的商过大,超过了寄存器所能存储的范围。
当CPU执行div等除法指令的时候,如果发生这样的情况,将引发CPU的一个内部错误,这个错误称为:
除法溢出。我们可以通过特殊的程序来处理这个错误,但在这里我们不讨论这个错误的处理,这是后面的
课程中要涉及的内容。下面我们仅仅来看一下除法溢出发生时的一些现象,如图10.1所示。
图中展示了在Windows 2000中使用debug执行相关程序段的结果,div指令引发了CPU的除法溢出,
系统对其进行了相关的处理。
好了,我们已经清楚了问题的所在:用div指令做除法的时候可能产生除法溢出。
由于有这样的问题,在进行除法运算的时候要注意除数和被除数的值,比如1000000/10就不能用div指令来计算。
那么怎么办呢?我们用下面的子程序divdw解决。
子程序描述
名称:divdw
功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。
参数:(ax)=dword型数据的低16位
(dx)=dword型数据的高16位
(cx)=除数
返回:(dx)=结果的高16位,(ax)=结果的低16位
(cx)=余数
应用举例:计算1000000/10 (F4240H/0AH)
mov ax, 4240H
mov dx, 000FH
mov cx, 0AH
call divdw
结果:(dx)=0001H,(ax)=86A0H,(cx)=0
提示
给出一个公式:
X:被除数,范围:[0,FFFFFFFF]
N:除数,范围:[0,FFFF]
H:X高16位,范围:[0,FFFF]
L:X低16位,范围:[0,FFFF]
int():描述性运算符,取商,比如,int(38/10)=3
rem():描述性运行符,取余数,比如,rem(38/10)=8
公式:X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
这个公式将可能产生溢出的除法运算:X/N,转变为多个不会产生溢出的除法运算。
公式中,等号右边的所有除法运算都可以用div指令来做,肯定不会导致除法溢出。
(关于这个公式的推导,有兴趣的读者请参看附注5.)
assume cs:code
code segment
start:
mov ax,4240h ;L
mov dx,000fh ;H
mov cx,0ah ;N
call divdw
mov cx,4c00h
int 21h
;使用div时 做被除数 ax 低位 dx 高位,做结果时,ax 商 dx 余数
;int(H/N)*65536 + [rem(H/N)*65536+L]/N
;参数:(ax)=dword型数据的低16位 (dx)=dword型数据的高16位 (cx)=除数
;返回:(dx)=结果的高16位 (ax)=结果的低16位 (cx)=余数
divdw:
push bx
push ax ;保存被除数低位 L
mov ax,dx ;把被除数高位H赋给ax 相当于先计算H/N
mov dx,0 ;dx 清零
div cx ;H/N,结果 ax 商 ,dx 余数
mov bx,ax ;保存商 ax=int(H/N)
pop ax ;取出L 余数做高位rem(H/N)*65536相当于存放在dx中,而上一步中的dx就是余数 低位ax=L
div cx ;[rem(H/N)*65536+L]/N dx=rem(H/N) ax=L N=cx
mov cx,dx ;保存余数结果
mov dx,bx ;结果dx=int(H/N)高位商,ax低位商
pop bx
ret
code ends
end start
3.数值显示
问题
编程,将data段中的数据以十进制的形式显示出来。
data segment
dw 123, 12666, 1, 8, 3, 38
data ends
这些数据在内存中都是二进制信息,标记了数值的大小。要把它们显示到屏幕上,成为我们能够
读懂的信息,需要进行信息的转化。比如,数据12666,在机器中存储为二进制信息:
0011000101111010B(317AH),计算机可以理解它。而要在显示器上读到可以理解的数值12666,
我们看到的应该是一串字符:”12666“。由于显卡遵循的是ASCII编码,为了让我们能在显示器
上看到这串字符,它在机器中应以ASCII码的形式存储为:31H、32H、36H、36H、36H(字符
”0“”9“对应的ASCII码为30H39H)。
通过上面的分析可以看到,在概念世界中,有一个抽象的数据12666,它表示了一个数值的大小。
在现实世界中它可以有多种表示形式,可以在电子机器中以高低电平(二进制)的形式存储,
也可以在纸上,黑板上、屏幕上以人数的语言”12666“来书写。现在,我们面临的问题就是,
要将同一抽象的数据,从一种表示形式转化为另一种表示形式。
可见,要将数据用十进制形式显示到屏幕上,要进地两步工作:
(1)将用二进制信息存储的数据转变为十进制形式的字符串;
(2)显示十进制形式的字符串。
第二步我们在本次实险的第一个子程序中已经实现,在这里只要调用一下show_str即可。我们来㢑第一步,
因为将二进制信息转变为十进制形式的字符串也是经常要用到的功能,我们应该为它编写一个通用的子程序。
子程序描述
名称:dtoc
功能:将word型数据转变为表示十进制的字符串,字符串以0为结尾符。
参数:(ax)=word型数据
ds:si指向字符串的首地址
返回:无
应用举例:编程,将数据12666以十进制的形式在屏幕的8行3列,用绿色显示出来。在显示
时我们调用本次实验中的第一个子程序show_str。
assume cs:code
data segment
db 10 dup (0)
data ends
code segment
start: mov ax, 12666
mov bx, data
mov ds, bx
mov si, 0
call dtoc
mov dh, 8
mov dl, 3
mov cl, 2
call show_str
:
:
:
code ends
end start
提示
下面我们对这个问题进行一下简单地分析。
(1)要得到字符串”12666“,就是要得到一列表示该字符串的ASCII码:31H、32H、36H、36H、36H。
十进制数码字会对应的ASCII码=十进制数据码值+30H。
要得到表示十进制数的字符串,先求十进制数每位的值。
例:对于12666,先求得每位的值:1、2、6、6、6。再将这些数分别加上30H,便得到了表示12666的
ASCII码串:31H、32H、36H、36H、36H。
(2)那么,怎样得到每位的值呢?采用下面的方法:
可见,用10除12666,共除5次,记下每次的余数,就得到了每位的值。
(3)综合以上分析,可得出处理过程如下。
用12666除以10,循环5次,记下每次的余数:将每次的余数分别加30H,便得到了表示
十进制数的ASCII码串。如下:
assume cs:code
data segment
db 256 dup(0)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov ax,1
mov si,10
call dotc_word
mov dh,9
mov dl,3
mov cl,2
call show_str
mov ax,12636
mov si,25
call dotc_word
mov dh,10
mov dl,3
mov cl,2
call show_str
mov ax,4c00h
int 21h
;功能:将word型数据转变为表示十进制的字符串,字符串以0为结尾符。
;参数:(ax)=word型数据 ds:si指向字符串的首地址
dotc_word:
push dx
push cx
push bx
push si
push di
mov di,si ;保存字符串首地址,后面字符串逆序需要
dotc_word_div_loops:
mov dx,0 ;作被除数时 高位dx 为0
mov bx,10 ;10 作除数
div bx ; ax/10 结果:ax 商 dx 余数
add dx,30h ;数字转字符
mov ds:[si],dx
inc si
mov cx,ax
jcxz dotc_word_ok ;判断商为0时结束 即 cx == 0
jmp short dotc_word_div_loops
dotc_word_ok:
mov cl,0
mov ds:[si],cl ;字符串结尾为0 data中的数据为66621 需要逆序
mov dx,0
push si ;保存si 字符串最高位下标
sub si,di ;字符串长度=最高位下标-初始首地址
mov cx,si
sub cx,1 ;只有一个数字字符 直接退出不需要逆序
jcxz dotc_word_str_ok
mov ax,si
mov bx,2
div bx ;循环次数=字符串长度/2
mov cx,ax
pop si ;取出si 字符串最高位0下标
dec si ;字符串最高位下标
dotc_word_str_reverse:
mov al,ds:[di] ;交换
mov ah,ds:[si]
mov ds:[si],al
mov ds:[di],ah
inc di
dec si
dec cx
jcxz dotc_word_str_reverse_ok
jmp short dotc_word_str_reverse
dotc_word_str_ok:
pop si
dotc_word_str_reverse_ok:
pop di
pop si
pop bx
pop cx
pop dx
ret
;参数:(ax)=word 型数据 ds:si 指向字符串的首地址
dotc:
push dx
push cx
push bx
push si
push di
mov di,si ;保存字符串首地址,后面字符串逆序需要
div_loops:
mov dx,0 ;作被除数时 高位dx 为0
mov bx,10 ;10 作除数
div bx ; ax/10 结果:ax 商 dx 余数
add dx,30h ;数字转字符
mov ds:[si],dx
inc si
mov cx,ax
jcxz dotc_ok ;判断商为0时结束 即 cx == 0
jmp short div_loops
dotc_ok:
mov cl,0
mov ds:[si],cl ;字符串结尾为0 data中的数据为66621 需要逆序
mov dx,0
push si ;保存si 字符串最高位下标
sub si,di ;字符串长度=最高位下标-初始首地址
mov ax,si
mov bx,2
div bx ;循环次数=字符串长度/2
mov cx,ax
pop si ;取出si 字符串最高位0下标
dec si ;字符串最高位下标
dotc_str_reverse:
mov al,ds:[di] ;交换
mov ah,ds:[si]
mov ds:[si],al
mov ds:[di],ah
inc di
dec si
dec cx
jcxz str_reverse_ok
jmp short dotc_str_reverse
str_reverse_ok:
pop di
pop si
pop bx
pop cx
pop dx
ret
;功能 在屏幕 指定位置 用指定颜色 显示一个以0结尾的字符串
;参数 dh行号 dl列号 cl颜色 ds : si 指向字符串的首地址
show_str:
push ax
push bx
push es
push si
mov ax,0b800h
mov es,ax
mov ax,2 ;获取偏移量 保存到bx中 bx = 2*dl + 160*dh
mul dl
mov bx,ax ;bx保存 dl*2的值
mov ax,160
mul dh
add bx,ax ;bx保存160*dh+ax的值
mov al,cl
mov cl,0
show_str_loops:
mov ch,ds:[si]
jcxz show_str_ok
mov es:[bx],ch
mov es:[bx+1],al
add bx,2
inc si
jmp short show_str_loops
show_str_ok:
pop si
pop es
pop bx
pop ax
ret
code ends
end start
dotc处理dword型数据
将dword型数转变为十进制的数据有些已经大于65535,一个编写一个新的数据到字符串转化的子程序,完成dword型数据到字符串的转化
名称:dtoc
功能:将dword型数转变为表示十进制数的字符串,字符串以0为结尾符合
参数:(ax)= dword 型数据的低16位 (dx)= dword型数据的高16位,ds:si指向字符串的首地址
返回:无
assume cs:code
data segment
db 24 dup(0)
data ends
code segment
start:
mov ax,0ffffh
mov dx,0ffffh
mov bx,data
mov ds,bx
mov si,0
call dotc_dword
mov dh,8
mov dl,3
mov cl,2
call show_str
mov ax,0ffffh
mov dx,0ff1fh
mov si,10
call dotc_dword
mov dh,9
mov dl,3
mov cl,8
call show_str
mov ax,4c00h
int 21h
dotc_dword:
push dx
push cx
push bx
push si
push di
mov di,si ;保存字符串首地址,后面字符串逆序需要
dotc_dword_div_loops:
mov cx,10 ;除数 10
call divdw ;dx ax / 10 结果:商dx ax 余数 cx
add cx,30h ;余数cx数字转字符
mov ds:[si],cx
inc si
mov cx,ax ;低16位ax
jcxz dotc_dword_ok ;判断商为0时结束 即 cx == 0
jmp short dotc_dword_div_loops
dotc_dword_ok:
mov cl,0
mov ds:[si],cl ;字符串结尾为0 data中的数据 需要逆序
mov dx,0
push si ;保存si 字符串最高位0下标
sub si,di ;字符串长度=最高位下标-初始首地址
mov cx,si
sub cx,1 ;只有一个数字字符 直接退出不需要逆序
jcxz dotc_dword_str_ok
mov ax,si
mov bx,2
div bx ;循环次数=字符串长度/2
mov cx,ax ;循环次数
pop si ;取出si 字符串最高位0下标
dec si ;字符串最高位下标
dotc_dword_str_reverse:
mov al,ds:[di] ;交换
mov ah,ds:[si]
mov ds:[si],al
mov ds:[di],ah
inc di
dec si
dec cx
jcxz dotc_dword_str_reverse_ok
jmp short dotc_dword_str_reverse
dotc_dword_str_ok:
pop si
dotc_dword_str_reverse_ok:
pop di
pop si
pop bx
pop cx
pop dx
ret
;功能 在屏幕 指定位置 用指定颜色 显示一个以0结尾的字符串
;参数 dh行号 dl列号 cl颜色 ds:si指向字符串的首地址
show_str:
push ax
push bx
push es
push si
mov ax,0b800h
mov es,ax
mov ax,2 ;获取偏移量 保存到bx中 bx = 2*dl + 160*dh
mul dl
mov bx,ax ;bx保存 dl*2的值
mov ax,160
mul dh
add bx,ax ;bx保存160*dh+ax的值
mov al,cl
mov cl,0
change:
mov ch,ds:[si]
jcxz show_str_ok
mov es:[bx],ch
mov es:[bx+1],al
add bx,2
inc si
jmp short change
show_str_ok:
pop si
pop es
pop bx
pop ax
ret
;使用div时 做被除数 ax 低位 dx 高位,做结果时,ax 商 dx 余数
;int(H/N)*65536 + [rem(H/N)*65536+L]/N
;参数:(ax)=dword型数据的低16位 (dx)=dword型数据的高16位 (cx)=除数
;返回:(dx)=结果的高16位 (ax)=结果的低16位 (cx)=余数
divdw:
push bx
push ax ;保存被除数低位 L
mov ax,dx ;把被除数高位H赋给ax 相当于先计算H/N
mov dx,0 ;dx 清零
div cx ;H/N,结果 ax 商 ,dx 余数
mov bx,ax ;保存商 bx = ax int(H/N)
pop ax ;取出L 余数做高位rem(H/N)*65536存放在dx中,而上一步中的dx就是余数 低位ax=L
div cx ;[rem(H/N)*65536+L]/N
mov cx,dx ;保存余数结果
mov dx,bx ;结果高位商 ,ax低位商
pop bx
ret
code ends
end start