前言:本实验为王爽老师的《汇编语言》第三版中的实验10(p206)
实验环境:DOSBox 0.74-3
实验任务:
1、显示字符串
问题
显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。
子程序描述
名称:show_str
功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
参数:(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79)
(cl)=颜色,ds:si指向字符串的首地址
返回:无
就用举例:在屏幕的8行3列,用绿色显示data段中的字符串。
assume cs:code
data segment
db 'Welcome to masm!',0
data endscode 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,ds:data,ss:stack
data segment
db 'Welcome to masm!',0
data ends
stack segment
db 16 dup (0)
stack ends
code segment
start: mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
mov sp,10h
mov dh,8 ;x行
mov dl,3 ;y列
mov cl,11000010b ;红底闪烁绿字
call show_str
mov ax,4c00h
int 21h
show_str: mov ax,0b800h
mov es,ax ;es指向显示缓冲区
mov si,0 ;ds:si指向数据段字符串
;设置字符串出现的位置
mov al,0a0h ;al=160(8位)
mul dh ;dh也为8位,乘积保存在ax
sub ax,160 ;x行对应偏移地址:(x-1)*160
mov bh,0
mov bl,dl ;dl中列的值赋予bx,之后add操作需要寄存器位数匹配
add ax,bx
add ax,bx
sub ax,2;x行y列对应偏移地址:(x-1)*160 + (y-1)*2
mov di,ax
s: push cx ;子程序中会使用cx,先入栈保存初始值(出栈在ret之前)
mov ch,0
mov cl,ds:[si] ;使用cx保存字符,cx为0则字符串结束
jcxz s1 ;cx为0字符串结束,退出子程序
mov es:[di],cl ;cx不为零则将字符copy到es段
pop cx ;接下来需要使用cx初始值,故出栈
mov es:[di+1],cl
inc si
add di,2
jmp short s
s1: pop cx ;退出子程序时恢复cx初始值
ret
code ends
end start
运行结果:
2、解决除法溢出的问题
问题
前面讲过,div指令可以做除法。当进行8位除法的时候,用al存储结果的商,ah存储结果的余数;进行16位除法的时候,用ax存储结果的商,dx存储结果的余数。可是,现在有一个问题,如果结果的商大于ah或ax所能存储的最大值,那么将如何?
比如,下面的程序段:
mov bh,1
mov ax,1000
div bh
进行的是8位除法,结果的商为1000,而1000在ah中放不下。
又比如,下面的程序段:
mov ax,H
mov dx,1
mov bx,1
div bx
进行的是16位除法,结果的商为11000H,而11000H在ax中存放不下。
我们在用div指令做除法的时候,很可能发生上面的情况:结果的商过大,超出了寄存器所能存储的范围。当CPU执行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,FFFF FFFF]
N:除数,范围:[0,FFFF]
H:X高16位,范围:[0,FFFF]
L:X低16位,范围:[0,FFFF]
int():描述性运算符,取商,比如,rem(38/10)=3
rem():描述性运算符,取余数,比如,rem(38/10)=8
公式:X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
这个公式将可能产生溢出的除法运算:X/N,转变为多个不会产生溢出的除法运算。公式中,等号右边的所有除法运算都可以用div指令来做,肯定不会导致除法溢出。
完整代码:
assume cs:code,ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,10h
mov ax,4240h
mov dx,000fh
mov cx,0ah
call divdw
divdw: push ax
;入栈保存被除数ax中低16位初始值
;栈中元素...ax,ip
;先计算高16位
mov ax,dx
mov dx,0 ;高16位转换为32位(因为除数为16位-cx)
div cx ;高16位除以除数,商在ax,余数在dx
;此时ax即为最终结果的高16位
mov bx,ax ;bx = int(H/N)
pop ax ;dx = rem (H/N),ax = L
div cx ;商在ax,余数在dx
mov cx,dx ;最终结果的余数保存在cx
mov dx,bx ;int(H/N)为最终结果的高16位
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码为30H~39H)。
通过上面的分析可以看到,在概念世界中,有一个抽象的数据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
完整代码:
assume cs:code,ds:data,ss:stack
data segment
db 16 dup (0) ;数据段存储数字对应的字符串
data ends
stack segment
db 16 dup (0)
stack ends
code segment
start: mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
mov sp,10h
mov si,1 ;将数字对应的字符串保存在ds:si开始的内存单元(第一个内存单元内容为00h,作为字符串结束标志)
mov ax,12666 ;设置显示的数值
call dtoc
;返回之后si指向data段中字符串最后一个字符(输出时需要逆序输出)
mov dh,8
mov dl,3
mov cl,11000010b
call show_str
mov ax,4c00h
int 21h
dtoc: mov dx,0
mov bx,10 ;bx保存除数10(000a-16位)
div bx ;余数在dx中(余数小于除数10,dh一定为0)
;商还在ax中
add dl,30h ;dl = 十进制数码值 + 30h = 对应的ascii码值
mov ds:[si],dl ;data段中数据是逆序存放
mov cx,ax ;cx = 商
jcxz ok ;商为0则说明各位的数已全部求出
inc si
jmp short dtoc
ok: ret
show_str: mov ax,0b800h
mov es,ax ;es指向显示缓冲区
;设置字符串出现的位置
mov al,0a0h ;al=160(8位)
mul dh ;dh也为8位,乘积保存在ax
sub ax,160 ;x行对应偏移地址:(x-1)*160
mov bh,0
mov bl,dl ;dl中列的值赋予bx,之后add操作需要寄存器位数匹配
add ax,bx
add ax,bx
sub ax,2;x行y列对应偏移地址:(x-1)*160 + (y-1)*2
mov di,ax
s: push cx ;子程序中会使用cx,先入栈保存初始值/栈中数据...cx,ip
mov ch,0
mov cl,ds:[si] ;使用cx保存字符,cx为0则字符串结束
jcxz ok1 ;cx为0字符串结束,退出子程序
mov es:[di],cl ;cx不为零则将字符copy到es段
pop cx ;接下来需要使用cx初始值,故出栈
mov es:[di+1],cl
dec si
add di,2
jmp short s
ok1: pop cx ;退出子程序时恢复cx初始值
ret
code ends
end start
运行结果: