实验三:子程序应用(数制转换)程序设计
一、实验目的
- 掌握程序设计中的子程序结构;
- 熟练使用过程伪指令、子程序调用和返回等汇编语言的指令编写子程序;
- 掌握数制转换方法;
- 掌握利用DOS系统功能调用进行字符输入及字符输出(显示)的方法。
二、实验内容
1、编写十进制到十六进制转换程序。要求从键盘取得一个十进制数,然后把该数以十六进制形式在屏幕上显示出来
2、已知从BUF开始存放了10个16进制字数据,编程求出这10个数中的最大数,(将最大数存入MAX字节单元),并将其以10进制数的形式在屏幕上显示出来。(提示:以上两题都要求采用子程序的方法)
3、从键盘上输入一行字符,如果这行字符比前一次输入的一行字符长度长,则保存该行字符,然后继续输入另一行字符;如果它比前一次输入的行短,则不保存这行字符。按下‘$’输入结束,最后将最长的一行字符显示出来。(选作)
(提示:定义数据段,包括两个变量STRING和BUFFER,格式如下:
STRING DB 0 ;存放字符的个数
DB 80 DUP (?), 0DH,0AH,‘$’;存放前一次输入的字符串,兼作显示缓冲区
BUFFER DB 80 ;输入字符串的缓冲区,最多输入80个字符
DB ?
DB 80 DUP (20H)
然后利用0AH号DOS系统功能调用收入字符,用09H号系统功能调用输出。)
三、设计思想
3.1 实验内容一
实验内容一要求我们将输入的十进制数转换为十六进制并显示在屏幕上。其中这个十位进制数,可能是多位,且需要用到输入和输出中断指令。
对于老师在课堂上演示的代码和思路,此处就不再累述。
对于自己,我先在数据段定义中,给出交互的语句STR1、STR2、STR3分别用于提示用户进行输入、得出输出结果、数据越界警告和换行。在代码段的主函数中,首先进行初始化,并给DS赋值DATA段地址(套路的基本操作),而后就是三条调用:调用输入子程序、调用转换输出子程序、调用换行,即可完成实验内容。
第一个call指令调用输入子程序INPUT,我先通过INT 21H中断指令输出交互提示,然后通过MOV AH,01H和INT 21H输入字符(这里输入的为十进制数),再判断输入的值是否为回车(回车键代表结束输入)、判断是否小于0或大于9(均结束输入),如果都不是这些情况就需要将输入的字符转换为字,并将其与BX寄存器交换(这里就是将输入的数放到高位,低位等待下一个数),升到高位之后需要乘10,以便JMP跳转,等待下一个字输入并加到整个数据中。
第二个call指令调用CHANGE子程序(完成进制转换以及输出的功能)。在这里除了屏幕显示输出交互提示外,需要给CL赋值为4,CH也赋值为4(方便之后的十进制数转换为十六进制)。而后在CHANG_TO_H标识符下,对于已经存在BX寄存器中的数据进行循环四次左移(二进制,每次移位CL=4次),每次移动四位是为了二进制四位保存,移动四位相当于乘上24,将其数值转换。这样基本完成了从十进制切换到二进制转换为十六进制。而后需要屏蔽高位,并加上30H转为ASCII码,如果是10以内就直接输出,如果大于等于10,则需要额外加7转为A~F。至此完成了对十六进制数的输出。
最后一个CRLF子程序输出换行只是为了界面简洁交互方便,并无太大作用。
具体流程图见下图1所示:
图1 实验内容一流程图
3.2 实验内容二
实验内容二要求我们对已存的10个十六进制数进行大小判断,并输出最大值到MAX字节单元,并将其转换为十进制数输出。
在数据段中,我给BUF字单元定义10个十六进制数,将MAX DB 赋值为?,并给出两个交互的字符串str1和str2,分别用于提示最大的十六进制数和对应的十进制数。在代码段中,我首先初始化AX寄存器,并给DS段寄存器赋值DATA地址,而后就调用三个CALL指令,分别调用MAXIMUM计算最大值,BEFORE子程序输出源数据,LATER子程序输出将十六进制转换为十进制后的数据。
在MAXIMUM子程序中,我先给BX寄存器赋值0(偏移量),CX计数器赋值为9。循环9次就可以找到一个最大值,将BUF单元中的第一个值赋值给AX,而后进入比较。由于BUF是子单元,所以每次偏移量需要加2,在AX与BUF[BX]的比较中,如果原来在AX的数据更大,就直接跳转重新循环;如果AX的值小于下一个BUF值,则更新AX后再进入循环,最后将最大值赋值给MAX存储单元。
在BEFORE子程序中,同实验内容一思路一致,循环移2次,每次移动4位,同理转换为ASC码后输出,最后输出的字符是十六进制。
在LATER子程序中,转换为10进制需要辗转相除,故嵌套了DIVD子程序,每次除十取余得到对应的十进制数。
具体流程图见下图2所示:(由于转换为16进制的流程与图1类似,故图2中不给出BEFORE子程序的流程)。
图2 实验内容二流程图
3.3 实验内容三
实验内容三要求我们在输入结束提示符前,一直输入字符串,并在这些输入的字符串中找到最长的字符并输出。
首先,我在数据段中定义STRING和BUFFER分别用来存放字符的个数(STRING兼做显示缓冲区)而BUFFER为输入字符串缓冲区,以及RESULT交互提示符。然后,在代码段中初始化后,调用READ_STR输入字符串的子程序和OUTPUT_LONGSTR输出最长的字符串子程序。
在READ_STR中需要先“吃回车”以及调用换行,方便交互输入提示,而后获取字符串输入缓冲区的地址(BUFFER),然后调用21H指令输入字符串,并取出第一个字符判断是否为“$”若为dollar则直接结束输入,否则通过进入字符串长度判断,首先取出字符并与之前保存的暂时的最长字符串比较,若短于之前的,则直接重新继续输入,否则更新长度字符冲,通过SI和DI指针进行REP MOVSB直接进行字符串保存。
在OUTPUT_LONGSTR,直接输出保存在STRING中的最长的字符串。
具体流程图见下图3所示:
图3 实验内容三流程图
四、程序代码
4.1 实验一内容
4.1.1 老师课上讲解
;编写十进制到十六进制转换程序,要求从键盘取得一个十进制数,然后把概述以十六进制形式在屏幕上显示出来
data segment
str1 db 0DH,0AH,'Please input a number(D):','$'
str2 db 0DH,0AH,'The result(H) is:','$'
str3 db "The number is too big to fill 16 bits.$"
data ends
code segment
assume cs:code,ds:data
;主程序
main proc
mov ax,data
mov ds,ax
call input ;调用输入子程序
je toobig ;十进制数过大
push ax ;实参入栈
call change ;调用转换输出子程序
jmp exit ;退出程序
toobig:
mov dx,offset str3
mov ah,9
int 21h
exit:
mov ax,4c00h
int 21h
main endp
;输入子程序:输入一个无符号十进制整数(<65535),返回值=ax
;如果十进制整数不能用16位二进制表示,返回值=-1(FFFFH)
input proc
;输出用户交互提示字符串
lea dx,str1
mov ah,9
int 21h
;寄存器保存
push bx
push cx
push si
push dx
mov bx,10 ;bx=乘数
mov cx,0 ;cx=将要输入的十进制整数的初始值
nextDigit:
;输入字符
mov ah,1
int 21h ;al=输入的字符
cmp al,'0'
jb finish
cmp al,'9'
ja finish
;数字字符转为数字
sub al,'0'
mov ah,0
mov si,ax ;si=当前输入的数字
;原整数*10+当前数字
mov ax,cx ;ax=原整数
mul bx ;乘积=bx:ax,如果乘积不能用16位表示,即dx!=0,cf=of=1
jc overflow ;如果cf=1,则跳转
add ax,si ;加当前数字
mov cx,ax ;cx=添加当前一位十进制数字后的十进制数
jmp nextDigit
overflow:
mov ax,-1
jmp return
finish:
mov ax,cx
return:
;寄存器恢复
pop dx
pop si
pop cx
pop bx
ret
input endp
;转换输出子程序:以十六进制输出一个无符号整数到显示器,用栈传递要输出的数,参数=[bp+4]
change proc
;输出用户交互提示字符串
lea dx,str2
mov ah,9
int 21h
push bp
mov bp,sp ;寄存器保存
push cx
mov cx,4 ;循环4次,输出十六进制数字字符串
nextchange:
push cx ;保存cx(下面的指令会修改cx)
mov cl,4
rol word ptr[bp+4],cl ;参数(传来的要显示的数)循环左移4位,高4位移到低4位
mov cx,[bp+4]
and cl,0fh ;让cl高4位清0,低4位保持不变,cl=当前要输出的一组4位二进制数
;转为16进制数字字符
cmp cl,9
jbe decimalNum
;当前数字大于9,要转为'A'-'F'
add cl,'A'-10
jmp output
decimalNum:
add cl,'0'
;输出当前字符
output:
mov dl,cl
mov ah,2
int 21h
pop cx ;恢复cx
loop nextchange
mov dl,'H'
mov ah,2
int 21h
pop cx
pop bp
ret
change endp
code ends
end main
4.1.2 自己课后练习
DATA SEGMENT
STR1 DB 0DH,0AH,'Please input a number(D):','$'
STR2 DB 0DH,0AH,'The result(H) is:','$'
STR3 DB 0DH,0AH,'$'
DATA ENDS
CODE SEGMENT
MAIN PROC FAR
ASSUME CS:CODE,DS:DATA
START:
PUSH DS
MOV AX,0
PUSH AX
MOV AX,DATA
MOV DS,AX
CALL INPUT ;调用输入子程序
CALL CHANGE ;调用转换并输出
CALL CRLF ;换行
RET
MAIN ENDP
INPUT PROC NEAR
MOV BX,0 ;B初始化为0
LEA DX,STR1
MOV AH,09H
INT 21H ;中断指令 输出用户交互提示字符串
SCANIN:
MOV AH,01H
INT 21H ;输入字符
CMP AL,0DH ;判读是否为回车键
JZ EXIT
SUB AL,30H ;记录是否小于0
JB EXIT
CMP AL,09H ;判断是否大于9
JA EXIT
CBW ;将其转换为字
XCHG AX,BX
MOV CX,0AH
MUL CX ;将前面的数*10
XCHG BX,AX
ADC BX,AX
JMP SCANIN ;再次输入
EXIT:
RET
INPUT ENDP
CHANGE PROC NEAR
LEA DX,STR2
MOV AH,09H
INT 21H ;在屏幕显示输出提示符
MOV CL,4 ;每次移位四位
MOV CH,4 ;循环四次
CHANG_TO_H:
ROL BX,CL ;循环左移
MOV AX,BX
AND AL,0FH ;屏蔽
ADD AL,30H ;转换为asc
CMP AL,39H
JBE OUTPUT ;10以内直接输出
ADD AL,07H
OUTPUT:
MOV DL,AL
MOV AH,02H
INT 21H
DEC CH
JNZ CHANG_TO_H
RET
CHANGE ENDP
CRLF PROC NEAR
LEA DX,STR3
MOV AH,09H
INT 21H
RET
CRLF ENDP
CODE ENDS
END START
4.2 实验内容二
DATA SEGMENT
BUF DW 05H,0FH,15H,1FH,9AH,25H,2AH,32H,3AH,3FH
MAX DB ?
str1 DB 0DH,0AH,'MAX(DEC):','$'
str2 DB 0DH,0AH,'MAX(HEX):','$'
DATA ENDS
CODE SEGMENT
MAIN PROC FAR
ASSUME CS:CODE,DS:DATA
START:
PUSH DS
XOR AX,AX
PUSH AX
MOV AX,DATA
MOV DS,AX
CALL MAXIMUM ;调用计算最大值子程序
CALL BEFORE ;调用输出原数据子程序
CALL LATER ;调用输出十进制数据子程序
RET
MAIN ENDP
MAXIMUM PROC NEAR
MOV BX,0 ;偏移量
MOV CX,9 ;循环比较9次
MOV AX,BUF[BX]
COMP:
ADD BX,2 ;偏移量+2
CMP AX,BUF[BX] ;比较两个数
JA NEXT ;原数据更大则下一个
MOV AX,BUF[BX] ;更换最大值
NEXT:
LOOP COMP
MOV BX,AX
MOV MAX,AL ;最大值存入MAX存储单元
RET
MAXIMUM ENDP
BEFORE PROC NEAR
LEA DX,str2
MOV AH,09H
INT 21H ;输出提示字符串
MOV BL,MAX
MOV CL,4 ;移位4位
MOV CH,2 ;循环2次
REPEAT:
ROL BL,CL ;循环左移4位
MOV AL,BL
AND AL,0FH
ADD AL,30H ;转换ASCII
CMP AL,39H
JBE LAST ;不大于9则直接输出
ADD AL,07H ;大于9再加07H
LAST:
MOV DL,AL
MOV AH,02H
INT 21H ;输出
DEC CH ;计数-1
JNZ REPEAT
RET
BEFORE ENDP
LATER PROC NEAR
LEA DX,str1
MOV AH,09H
INT 21H ;输出提示字符
MOV CX,100d
CALL DIVD ;调用DIVD子程序
MOV CX,10d
CALL DIVD
MOV CX,1d
CALL DIVD
RET
LATER ENDP
DIVD PROC NEAR
MOV AX,BX
MOV DX,0
DIV CX ;除以CX的值
MOV BX,DX ;余数存入BX
MOV DL,AL ;商存入DL
ADD DL,30H ;转换为ASCII
MOV AH,02H
INT 21H ;输出
RET
DIVD ENDP
CODE ENDS
END START
4.3 实验内容三
DATA SEGMENT
STRING DB 0 ;存放字符的个数
DB 80 DUP (?), 0DH,0AH,'$' ;存放前一次输入的字符串,兼作显示缓冲区
BUFFER DB 80 ;输入字符串的缓冲区,最多输入80个字符
DB ?
DB 80 DUP (20H)
RESULT DB 0DH,0AH,'LONGEST STRING:','$'
DATA ENDS
CODE SEGMENT
MAIN PROC FAR
ASSUME CS:CODE,DS:DATA,ES:DATA ;调用附加段使用SI和DI
START:
PUSH DS
XOR AX,AX
PUSH AX
MOV AX,DATA
MOV DS,AX
MOV ES,AX
CALL READ_STR ;调用输入字符串的子程序
CALL OUTPUT_LONGSTR ;调用输出最长的字符串
RET
MAIN ENDP
READ_STR PROC NEAR
REPEAT:
MOV AH,02H
MOV DL,0DH
INT 21H ;调用回车 类似C语言中吃回车
MOV AH,02H
MOV DL,0AH
INT 21H ;调用换行 再换行交互
LEA DX,BUFFER ;获取字符串输入缓冲区的地址
MOV AH,0AH
INT 21H ;键盘键入缓冲区 一次最多输入80个字符
MOV AL,BUFFER[2] ;取出第一个字符
CMP AL,'$' ;与$比较
JNZ COMP
JMP EXIT ;是就结束本次任务
COMP:
MOV AL,BUFFER[1] ;取出实际字符数
CMP AL,STRING[0] ;与最长字符数比较
JB REPEAT ;比前一次短直接返回继续输入
MOV STRING[0],AL ;长度若长则更新字串
LEA SI,BUFFER[2]
LEA DI,STRING[1] ;获取相关的地址信息,指针移动为后面REP指令取数据
MOV CH,0
MOV CL,BUFFER[1]
CLD
REP MOVSB ;输入的最长字符串存入STRING
JMP REPEAT ;再次输入
EXIT:
RET
READ_STR ENDP
OUTPUT_LONGSTR PROC NEAR
LEA DX,RESULT
MOV AH,09H
INT 21H ;输出提示字符串
LEA DX,STRING[1]
MOV AH,09H
INT 21H ;输出最长字符串
RET
OUTPUT_LONGSTR ENDP
CODE ENDS
END START
五、结果分析
5.1 调试情况
在实验的三个程序调试时,都遇到了或多或少的问题。通过emu8086的单步执行,观察执行中每一步的相关寄存器内容变化,发现错误的地方。最终程序运行顺利,与预期设想一致,且能够完成输入与输出。
5.2 对程序设计的总结及分析
由于汇编程序不同其他高级程序如C、C++、java等一样方便符合人的自然语言描述,我们对汇编程序的设计应当先建立详细的流程图,明确自己需要做什么,而后熟悉要用到的指令集,并掌握它,最后根据流程图,利用指令集编写出符合实验要求的汇编程序。
在原先的实验调试中,我们都是通过debug中的-t、-d、-r等指令来观察不同时刻下的寄存器及标志位的变化,来判断代码的出错情况。而本次实验中,我们用到了新的编译工具:emu8086。它的使用方便快捷,而且功能众多。不仅可以直接运行知晓结果是否正确,而且可以像高级程序编译工具那样设置断点、进行单步执行操作等。
本次的三个实验内容中,前两个实验内容侧重于数值的进制转换,如10进制到16进制或者16进制到10进制,因此实验内容存在一定难度。我分别用到了ROL循环移位指令和辗转相除法进行求解,思想同高级语言但是对于汇编语言指令的运用的要求更高。
5.3 输出结果及分析
5.3.1 实验内容一
老师讲的代码运行检验:
将10进制数的10转换为16进制数下的000A,符合实验内容的要求,结果正确。
自己课后代码检验:
将10进制数的123转换为16进制数下的007B,符合实验内容的要求,结果正确。
5.3.2 实验内容二
在代码中,十个十六进制数分别是05H,0FH,15H,1FH,9AH,25H,2AH,32H,3AH,3FH。易知,这10个测试数据中9AH是最大的,故16进制输出理应是9A,将其转换为10进制应当为154,结果正确。
5.3.3 实验内容三
我输入四个字符串:“Hello”,“I am RuanQionglu”,“Nice to meet you!”,“HaHa,you are right!!!”,并通过$符号中止输入,得到最长的字符串为第四个字符串,结果正确。
5.4 心得与体会
本次为汇编语言第三次实验,也是汇编语言的最后一次课。
通过本次实验我掌握了程序设计中的子程序结构,在内容1中编写了INPUT输入子程序、CHANGE转换进制输出子程序、CRLF换行子程序;在内容2中编写了MAXIMUM计算最大值子程序、BEFORE输出原先十六进制子程序、LATER输出十进制子程序,同时在LATER子程序中又嵌套了DIVD的辗转相除法子程序;在内容3中编写了READ_STR输入字符串子程序、OUTPUT_LONGSTR输出最长字符串子程序。综上共9个子程序,能够熟练编写且调用。
同时我熟练使用过程伪指令、子程序调用和返回等汇编指令,对于设计子程序来说,其实内容就如同编写普通的汇编语言(类似底层的C语言),但是如何将主程序与子程序连接就需要用这些伪指令,比如RET或者一些内部短转移等,能够掌握。
另外,我也掌握了数值转换的方法,在十六进制转换为十进制中,比较麻烦,我需要另外编写DIVD子程序,通过之前的辗转相除法获取相关的转换后的值,而实验三中用到最多的十进制转换为十六进制比较简单,可以把十进制看成二进制,通过ROL指令每四位进行循环,通过控制循环的次数进行二进制数转换为十六进制,原理虽然简单,但是在汇编语言中要通过ROL指令以及CX计数器的控制,还是存在一定难度,需要对指令有相当深的印象以及理解。
掌握利用DOS系统功能调用进行字符串输入以及字符输出显示的方法。这里INT 21指令在上一次的实验二中我就已经使用过许多21H中断指令(我仍记得当时内容硬性要求输出的很少,很多都是自己为了交互方便而设计的),这里如同输出最长的字符串,我们不能放在寄存器中通过ASCII码一个个观察,不仅麻烦而且交互性差,故这里三个实验内容都需要进行21H的输入和输出,比如读入十进制输出十六进制又或者输入字符串,输出最长的字符串。另外也有常量字符串(交互信息)的输出,让用户知道这里需要做什么。其实21H指令的功能非常庞大,它有许多子功能号,可以通过汇编语言书本或者网络查找相关的子功能号,进行查询并使用。
本次汇编语言,虽然有一定难度,但是最终还是完成了。