(十六)《汇编语言(王爽)》 | 实验 10:编写子程序


1. 预备知识

  • call 标号 将当前 IP 的内容入栈,并且根据位移跳转到执行标号处;ret 将栈顶元素赋值为 IP 寄存器,二者都是段内转移。由此,call 和 ret 指令共同支持了汇编语言编程中的模块化设计,类似于高级语言中的函数机制。如计算 N 的 3 次方:
call cube		;调用子程序
...
cube:
	mov ax,bx
	mul bx
	mul bx
	ret			;返回
  • 用寄存器来存储参数和结果是最常用的方法,对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作相反:调用者将参数送入参数寄存器,从结果寄存器取返回值;子程序从参数寄存器取参数,将结果送入结果寄存器。
  • 在使用寄存器时我们希望:调用子程序时不必关系程序使用了哪些寄存器编写子程序时不必关心调用者使用了哪些寄存器不会发生寄存器冲突。解决办法是在程序的开始将所有用到的寄存器中的内容使用栈保存,然后在子程序返回前恢复:
子程序开始:
	子程序使用的寄存器入栈
	子程序的内容
	子程序使用的寄存器出栈
	ret
  • 汇编语言中的除法规则:如果除数为 8 位则被除数为 16 位,默认存放在 AX 中,且 AL 存放商、AH 存放余数;如果除数为 16 位则被除数为 32 位,且高 16 位存放在 DX 中、低 16 位存放在 AX 中,且 AX 存放商、DX 存放余数。

2. 实验任务 1:显示字符串

在指定的位置,用指定的颜色,显示一个用 0 结束的字符串。参数:(dh)=行号,(dl)=列号,(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		;段寄存器DS指向数据段data
	mov si,0	
	call show_str	;调用子程序
	
	mov 4c00h
	int 21h
	
show_str:
	...			;待完成部分
code ends 
end start
  • 由于需要显示有颜色的字符串,所以使用 80×25 彩色模式。每个字符占用 2 个字节,低位存储 ASCII 码、高位存储属性。一屏幕共占用 4000 字节,可存储 2000 个字符,共 25 行,每行 80 个字符。
  • 由于写入位置的行号为 DH 的内容、列号为 DL 的内容,且每行的字节数为 160,所以可以使用乘法定位写入位置的起始位置(列的索引类似)。这里,使用两个 8 位寄存器做乘法:
mov al,160	;8位寄存器乘法,一个存在AL中,另一个存在寄存器或内存单元(DH)中
mul dh		;定位行的偏移,乘法结果存放在AX中
mov bx,ax	;以防被下一个乘法覆盖,这里需要保存寄存器AX的内容,如BX
mov al,2
mul dl		;定位列的偏移,乘法结果存放在AX中
add bx,ax	;最终的偏移
  • 由于事先不知道 data 段中字符串的长度,题目给出字符串的用 0 结束。jcxz 标号的功能是当寄存器 CX 的内容为 0 时则跳转到标号执行。所以,我们可以使用寄存器 CX 来接收字符串中的内容,并使用 jcxz 指令来判断是否到达字符串结尾
  • 由于不知道循环次数,这里使用 jmp 指令配合 jcxz 来完成循环功能
help:
	mov cl,data:[]		;将data段字符串依次写入CL中
	jcxz 标号			;一个退出标号,退出当前help子程序
	...					;写入字符及其属性
	...					;相关偏移
	jmp short help		;转移实现类似循环的功能
  • 根据预备知识,为防止寄存器冲突,在程序的开始将所有用到的寄存器中的内容使用栈保存,然后在子程序返回前恢复。最终整体程序为:
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		;使ds:si指向字符串的字符
	mov di,0		;di用于索引写入时的位置
	call show_str
	
	mov ax,4c00h
	int 21h
	
show_str:
	push ax 
	push bx 
	push cx 
	push dx 
	push es 
	push si 
	push di 		;将子程序用到的寄存器内容入栈

	mov ax,0B800h	
	mov es,ax		;寄存器ES指向彩色模式段
	
	mov al,160		;8位寄存器乘法,一个存在AL中,另一个存在寄存器或内存单元中
	mul dh 			;定位行的偏移,乘法结果存放在AX中
	mov bx,ax		;以防被下一个乘法覆盖,这里需要保存寄存器AX的内容,如BX
	mov al,2
	mul dl 			;定位列的偏移,乘法结果存放在AX中
	add bx,ax		;最终的偏移
	
	mov al,cl		;将颜色属性存到AL中,因为后面的jcxz指令会用到CX
	
help:
	mov cl,ds:[si]		;取字符串的字符
	jcxz exit			;如果CX等于0则退出
	mov es:[bx+di],cl	;低位写入字符的ASCII码
	mov es:[bx+di+1],al	;高位写入字符的属性
	inc si				;偏移1字节取字符
	add di,2			;偏移2字节写字符
	jmp short help		;转移实现类似循环的功能
	
exit:
	pop di 
	pop si 
	pop es 
	pop dx 
	pop cx
	pop bx 
	pop ax				;将子程序用到的寄存器内容出栈
	
	ret					;子程序返回
code ends 
end start

程序运行结果:

请添加图片描述


3. 实验任务 2:解决除法溢出的问题

由此可见,被除数的位数比除数高,这就可能造成除法溢出的情况。如在除数为 8 位被除数为 16 位时,假设结果也为 16 位,则商 / 余数无法存放在 8 位寄存器(AH、AL)中。

在汇编语言中,如果商过大,超过了寄存器所能存储的范围,CPU 执行到 div 指令时会引发内部错误,即除法溢出。如,我们编写一个除法溢出的程序,然后用 debug 调试:

assume cs:code

code segment
start:
	mov ax,1000H	;被除数
	mov bl,1		;除数
	div bl			;这里除法结果为1000H,需要用16位寄存器存放,除法溢出
	
	mov ax,4c00h
	int 21h
code ends 
end start

请添加图片描述

由结果可知,此时系统对其做了相关处理。编写子程序 divdw 来解决除法溢出问题:

名称:divdw
功能:进行不会产生溢出的除法运算,被除数为 dword 型、除数为 word 型,结果为 dword 型
参数:(ax)=dword 型数据的低 16 位、(dx)=dword 型数据的高 16 位、(cx)=除数
返回:(dx)=结果的高 16 位、(ax)=结果的低 16 位、(cx)=余数

如,计算 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,转变为多个不会产生溢出的除法运算。公式中,等号右边的所有除法运算均不会产生溢出。先手动计算:

H: 000FH  L: 4240H    N: 000AH
H/N 的商为 1H、余数为 5H。所以,int(H/N)=1H、rem(H/N)=5H。
X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
   =1H*10000H+[5H*10000H+4240]/000AH
   =10000H+[54240H]/000AH
   =10000H+86A0H(没有余数)
   =186A0H

根据手动计算的过程可以得到:左边除法运算的商由两部分组成,即右边等式加号的左右两部分。第一部分的商乘以 65536 相当于将其存放在结果的高 16 位;同理,加号右边部分乘以 65536 相当于将余数放到高 16 位。

  • H/N 表示被除数 X 的高 16 位除法。因为除数 N 是 16 位,所以被除数由高 16 位 DX 和 低 16 位 AX 组成。这里,需将 DX 设置为零:
mov bx,ax	;由于后面除法会用到AX,将其内容暂存到寄存器BX中

mov ax,dx	;针对X高16位的除法,将被除数X的高16位存放在AX中
mov dx,0	;除法H/N中高16位设置为零
div cx		;执行除法H/N,此时结果的商存放在AX中、余数存放在DX中

此时,除法结果中的高位为最终结果的商的高 16 位。由于后面还有一个除法,会继续使用寄存器 AX,所以将其值存放到其他寄存器:

mov si,ax	;H/N运算结果的商
  • 由前面的叙述可知,在第二个除法中,由于除数为 16 位,所以被除数为 32 位。且,被除数的高 16 位由 H/N 除法运算的余数组成,低 16 位由 L 组成:
mov dx,dx	;这里表示将H/N的余数(DX)放到下一次除法的高16位处(DX)
mov ax,bx	;将被除数低16位的内容放到AX中
div cx		;;执行除法[rem(H/N)*65536+L]/N,此时结果的商存放在AX中、余数存放在DX中

题目要求,(dx)=结果的高 16 位、(ax)=结果的低 16 位、(cx)=余数。高 16 位前面存放寄存器 SI 中了,低 16 位存放在 AX 中,余数存放在 DX 中:

mov cx,dx	;余数
mov dx,si	;高16位
mov ax,ax	;低16位

综合以上,完成程序为:

assume cs:code

code segment
start:
	mov ax,4240h
	mov dx,00Fh
	mov cx,0Ah
	call divdw
	mov ax,4c00h
	int 21h
divdw:
	mov bx,ax	;由于后面除法会用到AX,将其内容暂存到寄存器BX中

	mov ax,dx	;针对X高16位的除法,将被除数X的高16位存放在AX中
	mov dx,0	;除法H/N中高16位设置为零
	div cx		;执行除法H/N,此时结果的商存放在AX中、余数存放在DX中
	
	mov si,ax	;H/N运算结果的商
	
	mov ax,bx	;将被除数低16位的内容放到AX中
	div cx		;;执行除法[rem(H/N)*65536+L]/N,此时结果的商存放在AX中、余数存放在DX中
	
	mov cx,dx	;余数
	mov dx,si	;高16位
	ret
code ends
end start

程序运行后各存放结果的寄存器:

请添加图片描述


4. 实验任务 3:数值显示

编程,将 data 段中的数据以十进制的形式显示出来。

data segment
	dw 123,12666,1,8,3,38
data ends

这些数据在内存中以二进制的形式存放,如 12666(317AH)。而如果我们要在显示器上看到 12666,我们看到的应该是一串字符,计算机理解的内容是其 ASCII 码值:31H、32H、36H、36H、36H。

因此,要将数据以十进制形式显示在屏幕上,需进行两步工作:将用二进制信息存储的数据转变为十进制形式的字符串,显示十进制形式的字符串。

第二步可以使用实验 1 中的子程序完成,现将第一步定义为如下子程序:

名称:dtoc
功能:将 word 型数据转变为表示十进制数的字符串,字符串以 0 为结尾符
参数:(ax)=word 型数据,ds:si 指向字符串的首地址
返回:无

首先实现,将数据 12666 以十进制的形式在屏幕的 8 行 3 列用绿色显式出来。 程序如下:

assume cs:code

data segment
	db dup 10(0)
data ends

code segment
start:
	mov ax,12666
	mov bx,data 
	mov ds,bx 		;寄存器DS指向data段
	
	call dtoc 		;调用子程序完成转换
	
	mov dh,8
	mov dl,3
	mov cl,2
	call show_str	;调用子程序完成显示
	
	mov ax,4c00h
	int 21h
code ends
end start
  • 要通过数字 12666 得到其字符串形式,首先要得到数字的每一位,然后将其转换成 16 进制的 ASCII 码值。第一步,可通过除 10 取余的操作从低到高依次得到数字的每一位;第二步,将得到的每一位加上 30H 即可得到对应的 ASCII 码值。

  • 如何判断除 10 操作是否继续进行? 如果商为 0,则不继续进行除 10 操作,而当前余数为数字的最高位。解决办法是使用寄存器 CX 来存储商,再借助 jcxz 指令来实现相关功能。这里,除数 10 使用 BX 存储,则被除数低 16 位默认放在 AX 中。除法部分逻辑为:

mov bx,10		;BL存放除数
mov ax,12666	;AX存放被除数
div bx			;默认16位除法的商存放在AX中、余数存放在DX中
add dx,30h		;处理余数
mov cx,ax		;使用寄存器CX来存储商
jcxz ok			;基于jcxz跳转指令判断商是否为0

其次,由于从低到高取数字的每一位,与最终显式数字的每一位的顺序相反,这里利用栈先进后出的特点处理余数。同时,使用寄存器 DI 来记录共处理的元素数量以确定后续出栈的元素个数。结合实验任务 1 中的 show_ptr 子程序,完整程序为:

assume cs:code

data segment
	db 10 dup (0)
data ends

code segment
start:
	mov ax,12666
	mov bx,data 
	mov ds,bx 		;寄存器DS指向data段
	mov si,0
	
	call dtoc 		;调用子程序完成转换
	
	mov dh,8
	mov dl,3
	mov cl,2
	call show_str	;调用子程序完成显示
	
	mov ax,4c00h
	int 21h
	
dtoc:
	push ax 
	push bx 
	push cx 
	push dx
	push si
	push di 		;将子程序用到的寄存器内容入栈
	
	mov di,0		;计数器清零
	mov bx,10		;BX存放余数
	
div_call:	
	mov dx,0		;寄存器DX清零
	
	div bx			;被除数在AX中,默认16位除法的商存放在AX中、余数存放在DX中
	
	add dx,30h		;余数加上30H转换为对应ASCII码值
	push dx			;入栈
	inc di			;计数
	mov cx,ax		;判断商是否为零
	jcxz ok
	
	jmp short div_call	;如果商不为零则继续执行除法运算
	
ok:
	mov cx,di		;DI记录了栈中数据个数,将其赋值给CX作为后续循环次数
	
assign:
	pop ax			;栈中数据依次出栈
	mov ds:[si],al	;写入数据段,每次写入一个字节
	inc si 			;偏移1个字节写入下个元素
	
	loop assign 	;循环赋值
	
	pop di 
	pop si 
	pop dx 
	pop cx 
	pop bx
	pop ax			;将子程序用到的寄存器内容出栈
	
	ret				;子程序返回
	
show_str:
	push ax 
	push bx 
	push cx 
	push dx 
	push es 
	push si 
	push di 		;将子程序用到的寄存器内容入栈

	mov ax,0B800h	
	mov es,ax		;寄存器ES指向彩色模式段
	
	mov al,160		;8位寄存器乘法,一个存在AL中,另一个存在寄存器或内存单元中
	mul dh 			;定位行的偏移,乘法结果存放在AX中
	mov bx,ax		;以防被下一个乘法覆盖,这里需要保存寄存器AX的内容,如BX
	mov al,2
	mul dl 			;定位列的偏移,乘法结果存放在AX中
	add bx,ax		;最终的偏移
	
	mov al,cl		;将颜色属性存到AL中,因为后面的jcxz指令会用到CX
	
help:
	mov cl,ds:[si]		;取字符串的字符
	jcxz exit			;如果CX等于0则退出
	mov es:[bx+di],cl	;低位写入字符的ASCII码
	mov es:[bx+di+1],al	;高位写入字符的属性
	inc si				;偏移1字节取字符
	add di,2			;偏移2字节写字符
	jmp short help		;转移实现类似循环的功能
	
exit:
	pop di 
	pop si 
	pop es 
	pop dx 
	pop cx
	pop bx 
	pop ax				;将子程序用到的寄存器内容出栈
	
	ret					;子程序返回
	
code ends
end start

程序运行结果如下:

请添加图片描述
现在,如果将待写入数据改为:

data segment
	dw 123,12666,1,8,3,38
data ends

结合上述程序,需要额外定义一个段来存放余数:

remainder segment
	db 100 dup (0)
remainder ends

由写入单个数字到写入多个数字需改进的地方

  • 最外层加一个循环,且循环次数为待写入数字的个数。
  • 根据上一个程序,需要额外使用一个数据段来存储余数。同时,由于 show_ptr 中使用 0 作为结束标识符,在写完一个数字的余数后要在结尾处写入一个 0。
  • 由于使用的子程序较多,子程序开始时保存所有用到的寄存器、结束时恢复寄存器

5. 总结

注意,在多子程序调用时,在子程序开始前保存所所用的寄存器、结束时恢复寄存器。


  • 56
    点赞
  • 207
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值