【目的】
- 理解汇编语言中的ASSUME 伪指令和标准的汇编程序
- 掌握Debug-P/G/T 的关系和区别
- 掌握将十六进制数转换为十进制数的方法和程序
- 学习和改进两位数加法的程序
【样例要求】
- 使用记事本编写.asm 源程序
- 对于按程序进行汇编及连接,产生.exe 文件。若出错,则进行debug。
- 使用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工程文件 子程序汇编实验