DOS子程序汇编样例及详解

2022-08-03 18:45:55 浏览数 (1)

【目的】
  1. 理解汇编语言中的ASSUME 伪指令和标准的汇编程序
  2. 掌握Debug-P/G/T 的关系和区别
  3. 掌握将十六进制数转换为十进制数的方法和程序
  4. 学习和改进两位数加法的程序
【样例要求】
  1. 使用记事本编写.asm 源程序
  2. 对于按程序进行汇编及连接,产生.exe 文件。若出错,则进行debug。
  3. 使用visio 绘制程序的流程图
【具体内容】

知识总结:Debug中 -P/-G/-T命令的区别

1、P和T都是执行,像这个语句add ax,bx ,你不管用哪个,都是执行这一句,但如果是call next 这个next是一个程序段,那么就不一样了,用P,直接就把这段程序执行完了,用T则进入内部一句一句的执行.这个和C语言的那些调试一样,有的进入函数内部,有的就执行完函数。

2、具体如下:

T命令:执行以CS:IP开始的一个或几个指令,并显示出执行每条指令后所有寄存器的内容。也称单步跟踪命令(step in),t命令是单步执行,遇到子程序,也会进入里面一步步执行再返回。

P命令:执行循环、重复的字符串指令、软件中断或子例程。单步执行命令(step over),p命令,大多数情况与t一样,只有当遇到call调用子程序的时候,p命令直接执行完这个程序。

G命令:连续执行内存代码,可以在g后面指定内存地址。格式: G [=<地址>[,<断点>]]

上式等价于: (1) G (2) G=<地址> (3) G=<地址>,<断点>

功能: 执行内存中的指令序列 注: (1) 从CS:IP所指处开始执行 (2) 从指定地址开始执行(3) 从指定地址开始执行,到断点自动停止。

【一】将键盘上输入的十六进制数转换成十进制数,并在屏幕上显示。
(1)流程图:

(2)源代码:
代码语言:javascript复制
DATAS SEGMENT ;定义数据段代码
	STR1 DB 'Please enter a hexadecimal number',10,'$';定义提示字符串
	STR2 DB 10,'Please enter a right number',10,'$';定义错误提示字符串
DATAS ENDS

STACKS SEGMENT STACK
	DW 8 DUP(?) ;保留8个字变量的位置
STACKS ENDS

CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START: 
	MOV AX,DATAS
	MOV DS,AX ;将数据段的地址赋给DS
	MOV AX,STACKS
	MOV SS,AX ;将栈的段地址赋给SS
	MOV SP,20H ;指出栈顶
	LEA DX,STR1 ;利用LEA直接将STR1的内容赋值到DX中
	MOV AH,9H ;将9H赋值到AH中
	INT 21H ;输出提示字符串
	MOV BX,0 ;将0赋值到BX中
INPUT:
	MOV AH,1H
	INT 21H ;从键盘上输入一个字符,将其对应字符的ASCII码送入AL中,并在屏幕上显示该字符
	ADD DX,1 ;输入数字
	CMP AL,0DH
	JE HH ;若判断结果相等,即输入回车时则跳转至HH
JUDGE: 
	CMP AL,'f' ;比较输入的字符和f的ASCII码大小
	JA ERROR ;无符号大于则跳转至ERROR
	CMP AL,'a'
	JNB SIT1 ;无符号不小于则跳转至 SIT1
	CMP AL,'F' ;判断输入的字符是否是A~F
	JA ERROR ;无符号大于则跳转至ERROR
	CMP AL,'A'
	JNB SIT2 ;无符号不小于则跳转至SIT2
	CMP AL,'9' ;判断输入的字符是否是1~9
	JA ERROR ;无符号大于则跳转至ERROR
	CMP AL,'0'
	JNB SIT3 ;无符号不小于则跳转至SIT3
	JMP ERROR ;跳转至ERROR处
SIT1: 
	SUB AL,57H ;若位于a—f 之间,则AL-57H
	JMP TRA ;无条件跳转至TRA
SIT2: 
	SUB AL,37H ;若位于A-F 之间,则AL-37H
	JMP TRA ;无条件跳转至TRA
SIT3: 
	SUB AL,30H ;若为0—9,则AL-30H
	JMP TRA ;无条件跳转至TRA
TRA: 
	ADD DX,1
	MOV AH,0H ;将AH置零
	JE INPUT
	MOV CX,4H ;将循环次数设置为4
S: ROL BX,1 ;将bx左移四位
	LOOP S
	ADD BX,AX
	JMP INPUT ;跳转至输入阶段
HH: 
	MOV AX,BX ;将bx的值赋给ax
	MOV BX,10 ;设置除数为16位,用于解决四位十六进制数字。
	MOV CX,0
CIR: 
	MOV DX,0 ;输入的是四位及以下十六进制数字,因此被除数高位置零
	ADD CX,1 ;为输出时循环结束做准备
	DIV BX ;AX中的数字除以10,ax存储商数,dx中存储余数
	PUSH DX ;之后将余数入栈
	CMP AX,0 ;直到商为0时结束循环
	JNE CIR
NEXT: 
	POP AX ;将余数出栈
	MOV DL,AL ;转入DL 准备输出
	ADD DL,30H ;余数位于1—9 之间,因此需要将AL 30
	MOV AH,2
	INT 21H ;输出该十进制数字
	LOOP NEXT ;根据cx中的值进行循环输出的操作
	JMP STOP ;跳转至STOP
ERROR: ;错误情况处理
	LEA DX,STR2 ;获取STR至DX中
	MOV AH,9H
	INT 21H ;输出该提示语句
	JMP INPUT ;跳转至输入
STOP: 
	MOV AH,4CH
	INT 21H
	CODES ENDS
	END START
(3)结果分析:

当输入十六进制数时,显示其对应的十进制数字。符合题意。

当输入错误字符时,程序输出错误信息,并重新回到输入状态。符合题意。

【二】判断该年是否为闰年
(1)流程图:

(2)源代码:
代码语言:javascript复制
data segment ;代码段开始
	infon db 0dh,0ah,'Please input a year:$' ;infon 用于字符串,0d 回车,oa 换行,然后显示'Please input a year:'
	Y db 0dh,0ah,'This is a leap year!$' ;Y 用于定义字符串,同上,回车换行后显示'This is a leap year!'
	N db 0dh,0ah,'This is not a leap year!$';N用于定义字符串,同上,回车换行显示'This is not a leap year!'
	w dw 0  ;声明空间存储输入年份解析后生成的年份数字
	buf db 8  ;定义缓冲区,准备接受8 个字符
		db ?  ;实际接受的字符数,初始化为空
		db 8 dup(?)  ;初始化,dup 是一条伪指令,用于重复初始化数据
data ends  ;代码段结束

stack segment ;栈段开始
	db 200 dup(0) ;定义一个200 字节的栈段,初始化的值0
stack ends ;栈段结束

code segment ;代码段开始
			assume ds:data,ss:stack,cs:code ;将代码段和cs 连接,data 和ds 连接,把stack 和ss 连接
	start: mov ax,data ;将data 的地址放到ax 中
		mov ds,ax ;将ax的内容存入ds 中,ds 存储data 的地址
		lea dx,infon ;在屏幕上显示提示信息
		mov ah,9
		int 21h ;提示输入一个字符串
		lea dx,buf ;将dx 与buf 的段地址链接
		mov ah,10
		int 21h ;提示键盘输入一个字符串
		mov cl,[buf 1] ;高位置零,保证cx 的数值与实际输入长度一致
		mov ch,0 ;让ch 等于0,保证cx 的值为[buf 1]对应字节的值
		lea di,buf 2 ;获取字符串首地址
		call datacate ;调用子程序datacate
		call ifyears ;调用子程序ifyears
		jc a1 ;当cf=1 时,跳转至A1 处执行
		lea dx,n ;获取n 的地址
		mov ah,9
		int 21h ;输出n 的提示信息,不是闰年
		jmp exit ;跳转至exit
a1:     lea dx,y ;获取y 的地址
		mov ah,9
		int 21h ;输出y 的信息,是闰年
exit:   mov ah,4ch 
		int 21h ; 因为AH 值为4C,代码段结束,返回DOS
datacate proc near ;说明datacate 子程序在主程序段内
		push cx ;将cx 压入栈中备份
		dec cx ;将cx 自减1,保证循环中使得si 指向最后一个字符(即回车符前的字符)
		lea si,buf 2 ;将si 与buf 2 的段地址链接(第三个字节存的才是从键盘输入的字符),获取buf 字符串的首地址
	tt1:inc si ;将si 1
		loop tt1 ;循环tt1 段代码
		pop cx ;将备份的cx 的值取出
		mov dh,30h ;用来将数字字符对应的ASCII 值转换为其代表的数字本身
		mov bl,10 ;让bl 的值等于10,在每进一位时使ax 乘10
		mov ax,1 ;让ax 的值等于1,其代表权值
	l1: push ax ;将ax 压入栈中备份
		push bx ;将bx 压入栈中备份
		push dx ;将dx 压入栈中备份
		sub byte ptr [si],dh ;将ASCII 码-30,转换成对应数字
		mov bl,byte ptr [si] ;获取该位的数字
		mov bh,0 ;BX 寄存器高位置零
		mul bx ;将cx 的值乘上bx中代表的权值,并存在ax 中
		add [w],ax ;把ax 的值加在结果上得到年份数字
	pop dx ;恢复dx 的值
	pop bx ;恢复bx 的值
		pop ax ;恢复ax 的值
		mul bl ;cx 的值乘10
		dec si ;si 中的内容自减1,让si 指向上个字符
		loop l1 ;循环l1 段,CX 控制循环次数
		ret ;子程序返回
datacate endp ;子程序结束

ifyears proc near ;说明datacate 子程序在主程序段内
		push bx ;将bx 压入栈中备份
		push cx ;将cx 压入栈中备份
		push dx ;将dx 压入栈中备份
		mov ax,[w] ;将年份放到ax 中
		mov cx,ax ;让年份转入CX 备份
		mov dx,0 ;将DX 置零
		mov bx,100 ;将bx 赋值100作为除数
		div bx ;将年份除以100
		cmp dx,0 ;将余数dx 的值与0 作比较
		jnz lab1 ;若结果不为0,跳转到lab1
		mov ax,cx ;将cx 的值放入ax 中
	mov bx,400 ;让除数的值等于400
		div bx ;将年份除以400
		cmp dx,0 ;将余数dx 的值与0 作比较
	jz lab2 ;若结果为0,则执行lab2
	clc ;将标记位c清零
	jmp lab3 ;跳转到lab3
lab1: mov ax,cx ;lab1 段代码:将cx 的值放入ax 中
	mov dx,0 ;dx置零
	mov bx,4 ;将bx 的内容赋值为4
	div bx ;将年份除以4
	cmp dx,0 ;将余数dx 的值与0 作比较
	jz lab2 ;若结果为0,则执行lab2
	clc ;将标记位c清零
		jmp lab3 ;跳转到lab3
lab2: stc ;标志位设置为1
lab3: pop dx ;恢复dx的值
	pop cx ;恢复cx的值
	pop bx ;恢复bx的值
	ret ;子程序返回
  ifyears endp ;子程序结束
code ends ;代码段结束
	end start ;程序结束
(3)结果分析:

这里选择有代表性的2022、2020、2000、1900作为样例进行测试。

2022不能被4整除,非闰年。符合题意。

2020能被4整除,闰年。符合题意。

1900 能被100 整除,但不能被400 整除,非闰年。符合题意。

1900 能被100 整除,也能被400 整除,闰年。符合题意。

【三】汇编实例学习和改进:两位数加法
1. 3 5 程序

(1)流程图:

(2)源代码:

代码语言:javascript复制
DATAS SEGMENT;数据段开始
	five db 5 ;定义five,值为5的字节变量
DATAS ENDS;数据段结束

STACKS SEGMENT;栈段开始
	db 128 dup(?);定义栈为128 个双字节的不做初始化的空间
STACKS ENDS;栈段结束

CODES SEGMENT;代码段开始
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START:;主程序开始
	MOV AX,DATAS;将段地址装入段寄存器
	MOV DS,AX;将DS 与DATAS 相连接
	MOV AL,FIVE;令AL 的值为FIVE,即5
	ADD AL,3;将寄存器中的值取出,加上3后放回
	ADD AL,30H;将AL存的值 30H,得到ASCII 码
	MOV DL,AL;将待输出字符的ASCII码传到DL中去
	MOV AH,2
	INT 21H;输出DL
	MOV AH,4CH
	INT 21H;返回DOS系统
CODES ENDS;代码段结束
END START;程序结束

(3)代码、过程、相应结果的说明和分析:

结果符合预期。

2. 两变量加法程序

(1)流程图:

(2)源代码(粘贴源代码):

代码语言:javascript复制
DATAS SEGMENT ;DATAS 代码段开始
	num1 db 0 ;定义num1
	num2 db 0 ;定义num2
	add1 db ' $' ;定义add1,内容为“ ”
	equ1 db '=$' ;定义equ1,内容为“=”
DATAS ENDS ;DATAS 代码段结束

STACKS SEGMENT ;栈段开始
	db 128 dup(?) ;定义一个128 字节的栈段
STACKS ENDS ;栈段结束

CODES SEGMENT ;代码段开始
ASSUME CS:CODES,DS:DATAS,SS:STACKS;把CODES 代码段和CS 链接起来,DATAS 和DS 连接起来,把STACKS 和SS 连接起来
START: ;主程序开始
	MOV AX,DATAS ;将DATAS 的地址放到AX 中
	MOV DS,AX ;将DS 与DATAS 连接
	MOV AH,1
	INT 21H ;从键盘上输入第一个字符
	SUB AL,30H ;AL 的值-30H,转换为对应的数字
	MOV num1,AL ;将AL 的值放入num1中
	LEA DX,add1 ;将DX 与add1 的段地址链接
	MOV AH,9
	INT 21H ;输出该内容
	MOV AH,1
	INT 21H ;从键盘上输入字符
	SUB AL,30H ;AL 的值-30H,转换为对应的数字
	MOV num2,AL ;将AL 的值放入num1中
	LEA DX,equ1 ;将DX 与equ1的段地址链接
	MOV AH,9
	INT 21H ;输出该内容
	MOV AL,num1 ;把num1内容放入AL 中
	ADD AL,num2 ;把AL内容加上num2
	ADD AX,30H ;将AX 的值 30H转换为对应的ASCII 码
	MOV DL,AL ;将AL 的值存入DL 中
	MOV AH,2
	INT 21H ;输出该值
	MOV AH,4CH 
	INT 21H ;AH 值为4C,返回DOS
CODES ENDS ;代码段结束
END START ;主程序结束

(3)代码、过程、相应结果的说明和分析:

这里先采用上个实验中的3和5作为测试样例,得到的结果如下:

但是由于在写程序只用了AL存储结果,因此该程序无法输出多于一位的结果数,这里采用6和7作为测试样例,结果符合预期。程序溢出,产生了bug,需要进一步的改进。

3. 两位数加法程序

(1)流程图:

(2)源代码:

代码语言:javascript复制
DATAS SEGMENT
	infon1 db 0dh,0ah,'Please enter the first number:$';定义提示语句
	infon2 db 0dh,0ah,'Please enter the second number:$'
	infon3 db 0dh,0ah,'The sum is:$'
buf1 db 8 ;定义第一个缓冲区,存储第一个数字
db ?
db 8 dup(?)
buf2 db 8 ;;定义第二个缓冲区,存储第二个数字
db ?
db 8 dup(?)
DATAS ENDS

STACKS SEGMENT
	DB 128 dup(?);定义栈段代码
STACKS ENDS

CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START:
	MOV AX,DATAS
	MOV DS,AX
	lea dx,infon1
	MOV AH,9
	INT 21H
	lea dx,buf1
	mov ah,0ah
	int 21h ;输入第一个两位数
	MOV BL, [buf1 2];将十位存入bl
	SUB BL,'0';减去0对应的ASCII码,即转换为数字
	MOV BH, [buf1 3];将个位存入bh
	SUB BH,'0';减去0对应的ASCII码,即转换为数字
	lea dx,infon2
	mov ah,9
	int 21h
	lea dx,buf2
	mov ah,0ah
	int 21h ;输入第二个两位数
	
	lea dx,infon3 ;输出结果提示语
	mov ah,9
	int 21h
	MOV CL,[buf2 2] ;CL存入第一个数字的十位
	SUB CL,'0' ;减去0对应的ASCII码,即转换为数字
	MOV CH,[buf2 3] ;CH存入第一个数字的个位
	SUB CH,'0' ;减去0对应的ASCII码,即转换为数字
	add bl,cl ;将两个数十位相加
	add bh,ch ;将两个数个位相加
	cmp bh,10 ;个位与10 比较,考虑进位的问题
	jge units ;若小于10 则跳到units
units:;个位进位
	sub bh,10 ;个位减10,得到个位数字
	add bl,1 ;十位进1
	jmp tens ;继续判断十位数字加法
tens:;十位判断
	cmp bl,10 ;将十位数字与10 作比较
	jge carry ;若大于10 则跳到十位进位代码段carry
	jmp output ;否则跳至输出
carry:;十位进位代码段
	sub bl,10 ;十位有进位,将和的十位数字减10
	mov dl,31h;进位为1
	mov ah,02h
	int 21h
	jge output;跳至输出
output:
	mov dl,bl
	add dl,30h
	mov ah,02h
	int 21h ;把十位的值赋值到dl 并且输出该数字
	mov dl,bh
	add dl,30h
	mov ah,02h
	int 21h ;把个位的值赋值到dl 并且输出该数字
	mov ah,4ch
	int 21h;返回DOS
CODES ENDS;代码段结束
END START;主程序结束

(3)代码、过程、相应结果的说明和分析:

这里以98和88作为测试样例,结果符合预期。但是仍存在问题,就是由于编写程序中寄存器存储接收数字的逻辑,未能实现两位数加一位数的功能,若相加只能让一位数通过高位补零的方式完成,因此整个程序还有不断改进的空间。

【心得】

call指令和ret指令:CALL 指令调用一个过程,指挥处理器从新的内存地址开始执行。过程使用 RET(从过程返回)指令将处理器转回到该过程被调用的程序点上。 从物理上来说,CALL 指令将其返回地址压入堆栈,再把被调用过程的地址复制到指令指针寄存器。当过程准备返回时,它的 RET 指令从堆栈把返回地址弹回到指令指针寄存器。

LEA指令可以用来将一个内存地址直接赋给目的操作数,例如:lea eax,[ebx 8]就是将ebx 8这个值直接赋给eax,而不是把ebx 8处的内存地址里的数据赋给eax。

mul 指令: 两个相乘的数, 要么都是 8 位, 要么都是 16 位. 如果是 8 位, 一个默认放在 AL 中, 另一个放在 8 位 reg 或内存字节单元中; 如果是 16 位, 一个默认再 AX 中, 另一个放在 16 位 reg 或内存子单元中。结果:如果是 8 位乘法, 结果默认放在 AX 中; 如果是 16 位乘法, 结果高位默认在 DX 中存放, 低位在 AX 中存放。

div 指令:一般格式为:div reg或div 内存单元,reg和内存单元存放的是除数,除数可分为8位和16为2种。被除数:默认放在AX或DX和AX,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数 为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

​ 整体回顾此次实验,最开始直接浏览题时即使有思路,但是不知道如何去用汇编实现这个思路,各种地址,取址,移位等操作,有时输出的结果和自己预想中的不一样,出现乱码,此时还会一条一条地debug程序,整个过程毫无疑问是非常考验耐心的,且设计程序时有了一定的思路在实现中遇到很多考虑不周的地方,导致再分析修改的思路,前前后后发现又回到了起点。

​ 不过后来我又仔细地阅读了两遍老师写的实验PDF,我采用先阅读理解出老师给出的实验二判断闰年的那个代码,然后查了一些不熟悉的指令用法,阅读源码给我带来的收获颇丰,之前大概知道lea,push,pop等指令的用法,但具体却用的不灵活,能实现的功能有限,理解老师的源码时会有种恍然大悟的感觉:原来是这么用的。再模仿着实验二模块化的思想,顺着老师PDF的一步一步引导慢慢实现了其他实验。同时在这个过程中也查阅了不熟悉的指令用法与具体使用,整体上感觉比我前面几次实验收获更大。

​ 在第一个实验中,最开始我想的是比较常规的做法,即先将十六进制转换为二进制,再将二进制转换为十进制输出,但后来在具体实验过程中发现过于复杂冗余,且消耗的内存资源较多,实现起来并不方便。之后就回到直接除以十取余的转换方法。在判断输入字符时,需要多次跳转,因此借助老师实验二中模块化的思想,也照着采用模块化的定义方法,实现了最终的功能。

​ 在有了前两个实验的基础上,写第三个实验不像最初看题时的感觉了,在3 5实验的基础上,我做了一点改进,写了一个初步的两变量加法程序,但是由于在写程序只用了AL存储结果,因此该程序无法输出多于一位的结果数,采用6和7作为测试样例,程序溢出,产生了bug,需要进一步的改进。在最终的两位数加法程序中,采用了多个寄存器,分开个位和十位数字,并求和,再分开判断个位和十位是否需要进位,写到最后我发现汇编和之前学过的C语言写程序很相似,只不过汇编通过取址等操作以及寄存器实现。在实验运行出现预计的结果时,内心是异常喜悦的。

​ 现在回过头来看我写的第一个实验报告,有种莫名的感叹,虽然仅过去三周,但是让我对汇编的理解产生了翻天覆地的变化,我深切地感受到,学习一门语言不只是要逐个学习每一条指令就算掌握了这门语言,更重要地是通过实际地在应用中使用它,往往会有更好地理解,虽然现在只是学习了一点汇编的基础知识,但是对汇编的理解却有了很大的变化。

初学汇编,可能存在错误之处,还请各位不吝赐教。

受于文本原因,本文相关实验工程无法展示出来,现已将资源上传,可自行下载。

山东大学微处理器原理实验3工程文件 子程序汇编实验

0 人点赞