汇编语言实验 6:子程序

629 阅读13分钟

1. 预备知识

  1. 子程序类似于高级语言中的函数,通常通过转移指令 call 和 ret/retf 来共同实现。

  2. call 标号实现类似于 jmp 的段内转移。首先将 IP 入栈,然后转移至标号处,子程序完成后配合 ret 指令出栈 IP 并返回至子程序调用处。

  3. call far ptr 标号实现类似于 jmp 的段间转移。首先将 CS 和 IP 入栈,然后转移至标号处,子程序完成后配合 retf 指令出栈 IP 和 CS 并返回至子程序调用处。

  4. 使用 call 和 ret 完成子程序及其调用的基本格式为:

call 标号
...
标号:
    特定功能的程序段
    ret
  1. 在编写子程序时,可能出现子程序内部使用的寄存器和外部冲突的情况。解决办法是在子程序开始前将所有用到的寄存器内容存入栈中(保护现场),子程序结束后出栈恢复各寄存器的内容(恢复现场)。

  2. mul 是汇编语言中的乘法指令。两个相乘的数,要么都是 8 位,要么都是 16 位。如果是 8 位,一个默认放在 AL 中,另一个放在 8 位寄存器或内存单元中;如果是 16 位,一个默认放在 AX 中,另一个放在 16 位寄存器或内存单元中。对于结果,如果是 8 位乘法,结果默认放在 AX 中;如果是 16 位乘法,结果的高 16 位放在 DX 中、低 16 位放在 AX 中。

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
    mov si,0
    mov di,0		;ds:si指向字符串的首地址
    call show_str	;子程序调用
    mov ax,4c00h
    int 21h
code ends
end start

2.1 实验分析

参数给的是行号和列号,首先需要定位最终的偏移。每行 160 个字节,每个字符占两个字节:

mov al,160
mul dh      ;行偏移,乘法结果存放在AX中
mov bx,ax   ;保存行偏移
mov al,2
mul dl      ;列偏移,乘法结果存放在AX中
add bx,ax   ;最终偏移

找到最终偏移后,不断从数据段中取数据,将其存放在 CX 中,并配合 jcxz 指令退出。整体代码为:

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
    mov di,0		;ds:si指向字符串的首地址
    call show_str	;子程序调用
    mov ax,4c00h
    int 21h
show_str:
    mov ax,0B800h	
    mov es,ax		;寄存器ES指向彩色模式段
    mov al,160		;8位寄存器乘法
    mul dh 		;行偏移,乘法结果存放在AX中
    mov bx,ax		;保存行偏移
    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	;低位写入字符
    mov es:[bx+di+1],al	;高位写入字符属性
    inc si		;偏移1字节取字符
    add di,2		;偏移2字节写字符
    jmp short help	;转移至help处
exit:
    ret			;子程序返回
code ends 
end start

2.2 实验结果

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

前面介绍过,div 是汇编语言中的除法指令。当进行 8 位除法时,用 AL 存储结果的商;当进行 16 位除法时,用 AX 存储结果的商。如果结果的商大于 AL 或 AX 所能存储的最大值时,除法出现溢出。如以下程序:

assume cs:code
code segment
start:
    mov bh,1
    mov ax,1000
    div bh		;计算(AX)/(BH)=1000
    mov ax,4c00h
    int 21h
code ends 
end start

寄存器 AL 无法存储 div bh 的结果,除法出现溢出。此时,程序没有按照既定顺序执行:

3.1 实验分析

现通过子程序 divdw 解决除法溢出的问题:

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

如计算 0F4240h 除以 0Ah:

mov ax,4240h  ;低16位
mov dx,000fh  ;高16位
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 相当于将寄存器的内容左移 8 位,由公式右端第一部分可知,H/N 的商作为最终结果商的高 16 位;H/N 的余数作为第二部分除法的高 16 位、低 16 位由原被除数的低 16 位补充,且商为最终结果商的低 16 位、余数为最终结果的余数。整体代码为:

assume cs:code
code segment
start:
    mov ax,4240h	;被除数的低16位
    mov dx,00Fh		;被除数的高16位
    mov cx,0Ah		;除数
    call divdw		;子程序调用
    mov ax,4c00h
    int 21h
divdw:
    push ax	;后面除法会用到AX,入栈保存
    mov ax,dx	;X/N中被除数的低16位
    mov dx,0	;X/N中被除数的高16位
    div cx	;H/N,AX存放结果的商、DX存放结果的余数
    mov bx,ax	;保存H/N的商
    ;mov dx,dx	;[rem(H/N)*65536+L]/N中的高16位
    pop ax	;[rem(H/N)*65536+L]/N中的低16位
    div cx	;[rem(H/N)*65536+L]/N,AX存放结果的商、DX存放结果的余数
    mov cx,dx	;余数存放在CX中
    mov dx,bx	;商的高16位存放在DX中、低16位存放在AX中
    ret         ;子程序返回
code ends
end start

3.2 实验结果

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

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

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

这些数据在内存中二进制的形式存放,如 12666 对应于 317Ah。如果我们要在显示器上看到 12666,我们看到的应该是一串字符,计算机理解的内容是其 ASCII 码:31h、32h、36h、36h、36h。现使用子程序完成以上显示功能:

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

4.1 实验分析

首先,编程将数据 12666 以十进制的形式在屏幕的 8 行 3 列,用绿色显示出来。

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       ;ds:si指向字符串的首地址
    call dtoc      ;调用子程序完成转换
    mov dh,8
    mov dl,3
    mov cl,2
    call show_str  ;调用子程序完成显示
    ...
code ends
end start

要通过 12666 得到其字符串形式,首先要得到数字的每一位,可通过除 10 取余依次得到从低位到高位的数字,再加上 30h 即得到对应数字的 ASCII 码。

如何判断除 10 操作是否继续进行?如果商为零则停止除 10 操作,当前余数为数字的最高位。使用寄存器 CX 来存储商,结合 jcxz 转移指令跳出除 10 操作。

由于取数字是逆序进行的,与最终的显示顺序相反,利用栈先进后出的特点处理每位数字,同时记录数字的位数以确定后续出栈的数字个数。整体代码为:

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	;调用子程序完成显示
    mov ax,4c00h
    int 21h
dtoc:
    push si 		;保护现场
    mov di,0		;计数器清零
    mov bx,10		;BX存放除数
div_call:	
    mov dx,0		;DX存放高16位,AX存放低16位
    div bx		;除法运算,AX存放商、DX存放余数
    add dx,30h		;将余数转换为字符
    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	;写入数据段data,每次写入一个字节
    inc si 		;偏移1个字节写入下个元素
    loop assign
    pop si		;恢复现场
    ret			;子程序返回
show_str:
    mov ax,0b800h	
    mov es,ax		;寄存器ES指向彩色模式段
    mov al,160
    mul dh 		;行偏移,乘法结果存放在AX中
    mov bx,ax		
    mov al,2
    mul dl 		;列偏移,乘法结果存放在AX中
    add bx,ax		;最终偏移
    mov al,cl		;将颜色属性存到AL中,因为后面的jcxz指令会用到CX
    mov di,0
help:
    mov cl,ds:[si]	;取字符串的字符
    jcxz exit		;如果CX等于0则退出
    mov es:[bx+di],cl
    mov es:[bx+di+1],al	;写入字符及其属性
    inc si		;偏移1字节取字符
    add di,2		;偏移2字节写字符
    jmp short help
exit:
    ret			;子程序返回
code ends
end start

程序运行结果如下:

现考虑显示 data 段的所有数据:

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

使用如下数据段来存放所有数字,每个数字间以 0 作为间隔标识符:

remainder segment
    db 100 dup (0)
remainder ends

为了简便,这里采取先将所有数字写入 remainder 段后,再从 reminder 段取数字的方式,每个数字之间以空格作为间隔标识符。整体代码为:

assume cs:code
data segment
    dw 123,12666,1,8,3,38
data ends
remainder segment
    db 100 dup (0)
remainder ends 
code segment
start:
    mov ax,data
    mov ds,ax	;段寄存器DS指向data段
    mov ax,remainder
    mov es,ax	;段寄存器ES指向remainder段
    mov cx,6	;待显示的数字个数
    mov si,0	;从data段取数字的索引
    mov di,0	;remainder段的索引
s:
    call dtoc 	;调用子程序完成转换
    add si,2
    loop s 	;循环将数字写入remainder段,各数字间以'0'为标识符
    mov dh,8
    mov dl,3    ;行号和列号,为了简便(CL和CX冲突),字体属性写死了
    mov cx,6	;待显示的数字个数
    mov si,0	;从remainder段取字符
    mov di,0	;写入时的偏移
s_show:
    call show_str
    loop s_show	 
    mov ax,4c00h
    int 21h
dtoc:
    mov ax,ds:[si]	;从data段取数字
    push cx 
    push si		;保护现场
    mov si,0		;计数器清零
    mov bx,10		;BX存放除数
div_call:	
    mov dx,0		;DX存放高16位,AX存放低16位
    div bx		;除法运算,AX存放商、DX存放余数
    add dx,30h		;将余数转换为字符
    push dx		;入栈
    inc si		;计数
    mov cx,ax		
    jcxz ok		;判断商是否为零
    jmp short div_call
ok:
    mov cx,si		;SI记录了栈中数据个数,将其赋值给CX用于后续循环
assign:
    pop ax		;栈中数据依次出栈
    mov es:[di],al	;写入数据段remainder,每次写入一个字节
    inc di		;偏移1字节写字符
    loop assign
    inc di		;当前数字写入完成后,跳过1个字节用于表示各字符间的间隔
    pop si		;恢复现场
    pop cx 
    ret			;子程序返回
show_str:
    push cx             ;保护现场
    mov ax,0b800h	
    mov ds,ax		;寄存器ES指向彩色模式段
    mov al,160
    mul dh 		;行偏移,乘法结果存放在AX中
    mov bx,ax		
    mov al,2
    mul dl 		;列偏移,乘法结果存放在AX中
    add bx,ax		;最终偏移
help:
    mov cl,es:[si]	;取字符串的字符
    jcxz exit		;如果CX等于0则退出
    mov ds:[bx+di],cl
    mov byte ptr ds:[bx+di+1],2h
    ;写入字符及其属性
    inc si		;偏移1字节取字符
    add di,2		;偏移2字节写字符
    jmp short help
exit:
    inc di 
    mov ds:[bx+di],cl	;显示空格
    inc di 
    inc si		;从remainder段取字符时跳过空格
    pop cx              ;恢复现场
    ret			;子程序返回
code ends
end start

4.2 实验结果

首先查看第一个循环实现的功能,将数字的各位依次写入 remainder 段内,各数字间以空格隔开。使用 g 命令跳转到第一个循环结束的位置,并查看 es:0000 内存单元的值:

由图可知,所有数字已被正确写入。程序运行的最终结果:

5. 总结

  1. 在汇编语言中,子程序类似于高级语言中的函数,通常通过转移指令 call 和 ret/retf 来共同实现。为了避免子程序中使用的寄存器和外部冲突,使用栈在子程序开始前保护现场,结束后恢复现场

  2. mul 是汇编语言中的乘法指令。两个相乘的数,要么都是 8 位,要么都是 16 位

  3. 参考:汇编语言/王爽著.——北京:清华大学出版社,2003