1. 预备知识
-
子程序类似于高级语言中的函数,通常通过转移指令 call 和 ret/retf 来共同实现。
-
call 标号实现类似于 jmp 的段内转移。首先将 IP 入栈,然后转移至标号处,子程序完成后配合 ret 指令出栈 IP 并返回至子程序调用处。
-
call far ptr 标号实现类似于 jmp 的段间转移。首先将 CS 和 IP 入栈,然后转移至标号处,子程序完成后配合 retf 指令出栈 IP 和 CS 并返回至子程序调用处。
-
使用 call 和 ret 完成子程序及其调用的基本格式为:
call 标号
...
标号:
特定功能的程序段
ret
-
在编写子程序时,可能出现子程序内部使用的寄存器和外部冲突的情况。解决办法是在子程序开始前将所有用到的寄存器内容存入栈中(保护现场),子程序结束后出栈恢复各寄存器的内容(恢复现场)。
-
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. 总结
-
在汇编语言中,子程序类似于高级语言中的函数,通常通过转移指令 call 和 ret/retf 来共同实现。为了避免子程序中使用的寄存器和外部冲突,使用栈在子程序开始前保护现场,结束后恢复现场
-
mul 是汇编语言中的乘法指令。两个相乘的数,要么都是 8 位,要么都是 16 位
-
参考:汇编语言/王爽著.——北京:清华大学出版社,2003