汇编语言之程序的基本结构.docx
《汇编语言之程序的基本结构.docx》由会员分享,可在线阅读,更多相关《汇编语言之程序的基本结构.docx(42页珍藏版)》请在冰豆网上搜索。
汇编语言之程序的基本结构
汇编语言之程序的基本结构
————————————————————————————————作者:
————————————————————————————————日期:
第6章 程序的基本结构
在前面几章,我们分别介绍了用汇编语言进行程序设计所需要的几个最基本的知识:
内存单元的寻址方式,变量定义和各种汇编指令格式。
在掌握了这些基本内容之后,就需要学习如何把它们组成一个完整的汇编语言程序。
6.1源程序的基本组成
汇编语言源程序的组成部分有:
模块、段、子程序和宏等。
一个模块对应一个目标文件,当开发较大型的应用程序时,该程序可能由若干个目标文件或库结合而成的。
有关模块和子程序的知识和宏在第7章介绍,有关宏的知识将在第9章中叙述。
6.1.1 段的定义
微机系统的内存是分段管理的,为了与之相对应,汇编语言源程序也分若干个段来构成。
8086CPU有四个段寄存器,在该系统环境下运行的程序在某个时刻最多可访问四个段,而80386及其以后的CPU都含有六个段寄存器,于是,在这些系统环境下开发的运行程序在某个时刻最多可访问六个段。
不论程序在某个时刻最多能访问多少个段,在编程序时,程序员都可以定义比该段数更多的段。
在通常情况下,一个段的长度不能超过64K,在80386及其以后系统的保护方式下,段基地址是32位,段的最大长度可达4G。
段的长度是指该段所占的字节数:
、如果段是数据段,则其长度是其所有变量所占字节数的总和;ﻫ、如果段是代码段,则其长度是其所有指令所占字节数的总和。
在定义段时,每个段都有一个段名。
在取段名时,要取一个具有一定含义的段名。
段定义的一般格式如下:
段名
SEGMENT
[对齐类型] [组合类型] [类别]
…
;段内的具体内容
…
段名
ENDS
其中:
“段名”必须是一个合法的标识符,前后二个段名要相同。
可选项“对齐类型”、“组合类型”和“类别”的说明作用请见6.3节中的叙述。
一个数据段的定义例子:
DATA1
SEGMENT
word1
DW
1, 9078H, ?
byte1
DB
21,'World'
DD
12345678H
DATA1
ENDS
一个代码段的例子:
CODE1
SEGMENT
MOV
AX,DATA1
;把数据段DATA1的段值送AX
MOV
DS,AX
;把AX的值送给DS,即:
DS存储数据段的段值
…
MOV
AX,4C00H
INT
21H
;调用DOS功能,结束程序的运行
CODE1
ENDS
6.1.2段寄存器的说明语句
在汇编语言源程序中可以定义多个段,每个段都要与一个段寄存器建立一种对应关系。
建立这种对应关系的说明语句格式如下:
ASSUME 段寄存器名:
段名[, 段寄存器名:
段名,……]
其中:
段寄存器是CS、DS、ES、SS、FS和GS,段名是在段定义语句说明时的段名。
在一条ASSUME语句中可建立多组段寄存器与段之间的关系,每种对应关系要用逗号分隔。
例如,
ASSUME CS:
CODE1,DS:
DATA1
上面的语句说明了:
CS对应于代码段CODE1,DS对应于数据段DATA1。
在ASSUME语句中,还可以用关键字NOTHING来说明某个段寄存器不与任何段相对应。
下面语句说明了段寄存器ES不与某段相对应。
ASSUME ES:
NOTHING
在通常情况下,代码段的第一条语句就是用ASSUME语句来说明段寄存器与段之间的对应关系。
在代码段的其它位置,还可以用另一个ASSUME语句来改变前面ASSUME语句所说明的对应关系,这样,代码段中的指令就用最近的ASSUME语句所建立的对应关系来确定指令中的有关信息。
例6.1汇编语言段及其段说明语句的作用。
DATA1
SEGMENT
;定义数据段DATA1
word1
DW 5678h
byte1
DB "ABCDEFG"
DATA1
ENDS
DATA2
SEGMENT
;定义数据段DATA2
word2
DW 1234h
word3
DW9876h
DATA2
ENDS
DATA3
SEGMENT
;定义数据段DATA3
byte2
DB?
DATA3
ENDS
CODE1
SEGMENT
;编写代码段CODE1
ASSUME
CS:
CODE1,DS:
DATA1, ES:
DATA2
;
(1)
MOV
AX, DATA1
;
(2)
MOV
DS,AX
;(3)
MOV
AX,DATA2
;(4)
MOV
ES,AX
;(5)
…
MOV
AX,word1
;访问段DATA1中的字变量word1
MOV
word2, AX
;访问段DATA2中的字变量word2
…
ASSUME
DS:
DATA3, ES:
NOTHING
;(6)
MOV
AX,DATA3
MOV
DS,AX
MOV
BL, byte2
;访问段DATA3中的字节变量byte2
…
MOV
AX,4C00H
;(7)
INT
21H
;(8)
CODE1
ENDS
语句
(1)和(6)分别说明了段和段寄存器之间的对应关系,其中语句(6)重新说明语句
(1)所指定的对应关系。
二组语句
(2)和(3)、(4)和(5)实现对段寄存器DS和ES赋初值。
ASSUME说明语句只起说明作用,它不会对段寄存器赋值,所以,必须对有关段寄存器赋值。
在以后的其它源程序中也都是用此方法来实现对数据段寄存器赋值的。
语句(7)和(8)是调用中断21H的4CH号功能来结束本程序的执行,程序的返回代码由寄存器AL来确定。
结束本程序执行的指令是所有主模块必须书写的语句。
注意:
代码段寄存器不能由程序员在源程序中对其赋值,其值是由操作系统在装入它进入系统运行时自动赋值的。
6.1.3 堆栈段的说明
堆栈段是一个特殊的段,在程序中可以定义它,也可以不定义。
除了要生成COM型执行文件的源程序外,一个完整的源程序一般最好定义堆栈段。
如果在程序中不定义堆栈段,那么,操作系统在装入该执行程序时将自动为其指定一个64K字节的堆栈段。
在程序没有定义堆栈段的情况下,在由连接程序生成执行文件时,将会产生一条如下的警告信息,但程序员可以不理会它,所生成的执行文件是可以正常运行的。
warning xxxx:
no stacksegment (其中:
xxxx是错误号)
在源程序中,可用以下方法来定义堆栈段。
方法1:
STACK1
SEGMENT
DB256DUP(?
)
;256是堆栈的长度,可根据需要进行改变
TOP
LABEL WORD
STACK1
ENDS
以上堆栈段的定义如图6.1所示。
由于堆栈是按地址从大到小的存储单元顺序来存放内容的,所以,在堆栈存储单元说明语句之后,再说明一个栈顶别名,这样,对栈顶寄存器SP的赋值就很方便。
在源程序的代码段中,还要添加如下程序段,才能把段STACK1当作堆栈段来使用。
图6.1 堆栈段定义示意图
…
ASSUME
SS:
STACK1
;可在代码段的段指定语句中一起说明
CLI
;禁止响应可屏蔽中断
MOV
AX, STACK1
MOV
SS,AX
MOV
SP,offsetTOP
;给堆栈段的栈顶寄存器SP赋初值
STI
;恢复响应可屏蔽中断
…
方法2:
STACK1
SEGMENT STACK
;定义一个堆栈段,其段名为STACK1
DB256DUP(?
)
STACK1
ENDS
上述段定义说明了该段是堆栈段,系统会自动把段寄存器SS和栈顶寄存器SP与该堆栈段之间建立相应的关系,并设置其初值,而不用在代码段对它们进行赋值。
除了以上二种方法外,还有一种更简洁的方法来定义堆栈段,有关内容请见第6.4.2节中的叙述。
6.1.4源程序的结构
下面的程序是一个完整的源程序,其功能是在屏幕上显示字符串“Hello,World.”。
读者可参考此结构编写自己的简单程序。
例6.2在屏幕上显示字符串“HELLO,WORLD.”
解:
可运行下面的控件,用鼠标左键单击程序中的某一行,可阅读其含义;单击“内存”可切换内存内容的显示方式。
伪指令END表示源程序到此为止,汇编程序对该语句之后的任何内容都不作处理,所以,通常情况下,伪指令END是源程序的最后一条语句。
伪指令END后面可附带一个在程序中已定义的标号,由该标号指明程序的启动位置。
如果源程序是一个独立的程序或主模块,那么,伪指令END后面一定要附带一个标号;如果源程序仅是一个普通模块,那么,其END后面就一定不能附带标号。
6.2程序的基本结构
在学习高级语言程序设计时,我们知道了程序的三大主要结构:
顺序结构、分支结构和循环结构。
在汇编语言的源程序也同样有此三大结构,所不同的是它们的表现形式不同。
用高级语言编写程序时,由于不使用“转移语句”而使这三种结构清晰明了。
但在汇编语言的源程序中,很难不使用“转移语句”(除非是一些只有简单功能的程序),有时甚至会有各种各样的“转移语句”。
由于存在这些转移语句,就使得:
汇编语言源程序的基本结构显得不太明确。
如果源程序的编写者思维混乱,编写出来的源程序在结构上就会显得杂乱无章,反之,如果编写者条理清晰,安排的操作井然有序,那么,编写出来的程序在结构上就会一目了然。
总之,不论是高级语言的源程序,还是汇编语言的源程序,其程序的三大基本结构也还是万变不离其宗的。
6.2.1顺序结构
顺序结构是最简单的程序结构,程序的执行顺序就是指令的编写顺序,所以,安排指令的先后次序就显得至关重要。
另外,在编程序时,还要妥善保存已得到的处理结果,为后面的进一步处理直接提供前面的处理结果,从而避免不必要的重复操作。
例6.3编写程序段,完成下面公式的计算(其中:
变量X和Y是32位有符号数,变量A,B和Z是16位有符号数)。
A←(X-Y+24)/Z的商,B←(X-Y+24)/Z的余数
解:
DATA1
SEGMENT
X
DD?
Y
DD ?
Z
DW?
A
DW ?
B
DW ?
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AX,X
MOV
DX,X+2
;用(DX:
AX)来保存32位变量X的数值
SUB
AX,Y
SBB
DX,Y+2
;(DX:
AX)-(Y+2:
Y)
ADD
AX,24D
ADC
DX,0
;(DX:
AX)+24
IDIV
Z
MOV
A,AX
MOV
B, DX
…
CODE1
ENDS
在编程序时,常常需要交换二变量之值。
假设需要交换值的变量名为:
var1和var2,临时增加的变量名为temp。
常用的算法如下:
temp= var1
var1=var2ﻫvar2= temp
例6.4 假设有二个字变量word1和word2,编写程序段实现交换其值的功能。
解:
方法1:
用汇编语言指令简单“直译”上面的交换数据方法
DATA1
SEGMENT
…
word1
DW?
word2
DW ?
temp
DW?
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AX,word1
MOV
temp,AX
;上二语句实现语句“temp=word1”
MOV
AX,word2
MOV
word1,AX
;上二语句实现语句“word1=word2”
MOV
AX, temp
MOV
word2, AX
;上二语句实现语句“word2=temp”
…
CODE1
ENDS
这种方法虽然也能完成功能,但显然其不能充分利用汇编语言的特点,程序效率很低。
方法2:
用汇编语言指令的特点来直接编译
DATA1
SEGMENT
…
word1
DW?
word2
DW ?
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AX,word1
XCHG
AX,word2
MOV
word1,AX
;能XCHGword1,word2来代替这三条指令吗?
…
CODE1
ENDS
该方法充分利用了汇编语言的特点,不仅省去了中间变量temp的定义,而且程序的效率也提高了。
6.2.2分支结构
分支结构是一种非常重要的程序结构,也是实现程序功能选择所必要的程序结构。
由于汇编语言需要书写转移指令来实现分支结构,而转移指令肯定会破坏程序的结构,所以,编写清晰的分支结构是掌握该结构的重点,也是用汇编语言编程的基本功。
在程序中,当需要进行逻辑分支时,可用每次分二支的方法来达到程序最终分多支的要求,也可是用地址表的方法来达到分多支的目的。
一、显示转移指令实现的分支结构
在高级语句中,分支结构一般用IF语句来实现,在汇编语言中,课用无条件转移指令或条件转移指令实现的分支结构。
如图6.2给出了二种常用的分支结构。
在编写分支程序时,要尽可能避免编写“头重脚轻”的结构,即:
当前分支条件成立时,将执行一系列指令,而条件不成立时,所执行的指令很少。
这样就使后一个分支离分支点较远,有时甚至会遗忘编写后一分支程序。
这种分支方式不仅不利于程序的阅读,而且也不便将来的维护。
所以,在编写分支结构时,一般先处理简单的分支,再处理较复杂的分支。
对多分支的情况,也可遵循“由易到难”的原则。
因为简单的分支只需要较少的指令就能处理完,一旦处理完这种情况后,在后面的编程过程中就可集中考虑如何处理复杂的分支。
(a) if…endif结构
(b)if…else…endif结构
图6.2分支结构的二种结构
例6.5已知字节变量CHAR1,编写一程序段,把它由小写字母变成大写字母。
解:
DATA1
SEGMENT
…
CHAR1
DB?
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AL,CHAR1
CMP
AL,‘a’
JB
next
CMP
AL,‘z’
JA
next
SUB
CHAR1, 20H
;指令ANDCHAR1,0DFH也可以
next:
…
…
CODE1
ENDS
例6.6 编写一程序段,计算下列函数值。
其中:
变量X和Y是有符号字变量。
解:
DATA1
SEGMENT
…
X
DW ?
Y
DW ?
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AX,X
CMP
AX,0
JGE
case23
ADD
AX,10
;第一种情况的计算结果
JMP
result
case23:
CMP
AX,10D
JG
case3
MOV
BX,30D
IMUL
BX
;第二种情况的计算结果
JMP
result
case3:
SUB
AX,9
;第三种情况的计算结果
result:
MOV
Y,AX
;把计算结果保存到变量Y中
…
CODE1
ENDS
例6.7把下列C语言的语句改写成等价的汇编语言程序段(不考虑运算过程中的溢出)。
If (a+b>0 &&c%2 ==0)a=62;ﻫelse a=21;
其中:
变量a,b和c都是有符号的整型(int)变量。
解:
DATA1
SEGMENT
…
A
DW?
B
DW?
C
DW?
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AX,A
ADD
AX,B
JLE
_ELSE
;ADD指令会改变算术标志位
TEST
C,1
;C%2==0,也就是:
看C的最低位是否为0
JNZ
_ELSE
MOV
A,62D
JMP
NEXT
_ELSE:
MOV
A, 21D
NEXT:
…
CODE1
ENDS
例6.8用地址转移表实现下列C语言的switch语句,其中:
变量A和B是有符号的整型(int)变量。
switch
(a%8)
{case0:
b=32;
break;
case1:
case2:
b=a+43;
break;
case3:
b=2*a;
break;
case4:
b--;
break;
case5:
case6:
case7:
printf(“Function5_6_7”);
break;
}
解:
DATA1
SEGMENT
…
A
DW ?
B
DW ?
Table
DW case0.case12,case12,case3
DW case4,case567,case567,case567
MSG
DB 'Function5_6_7$'
…
DATA1
ENDS
CODE1
SEGMENT
…
MOV
AX,A
MOV
BX,AX
AND
BX,7
;得到BX的低三位,实现a%8的计算
SHL
BX,1
;由于地址表是字类型,其下标要乘2
JMP
Table[BX]
;利用地址表实现多路转移
case0:
MOV
B,32D
JMP
next
case12:
ADD
AX,43D
MOV
B,AX
JMP
next
case3:
SHL
AX,1
MOV
B,AX
JMP
next
case4:
DEC
B
JMP
next
case567:
LEA
DX,MSG
MOV
AH,9
INT
21H
JMP
next
next:
…
CODE1
ENDS
用地址表实现多路转移的关键在于:
转移入口的地址表和转移情况可整数化。
如果这二个要求有一个不满足,或很难构造,则无法使用该方法。
为了改善汇编语言源程序的结构,减少显式转移语句所带来混乱,在宏汇编MASM6.11系统中,增加了表达分支结构的伪指令。
该伪指令的书写格式与高级语言的书写方式相类似,汇编程序在汇编时会自动增加转移指令和相应的标号。
理解并掌握该知识,对将来学习《编译原理》课程也有一定的帮助。
分支伪指令的具体格式如下:
格式1:
.IF condition ;以英文“句号”开头
指令序列 ;条件"condition"成立时所执行的指令序列
.ENDIF
格式2:
.IFcondition
指令序列1
.ELSE
指令序列2 ;条件"condition"不成立时所执行的指令序列
.ENDIF
格式3:
.IFcondition1
指令序列1ﻫ.ELSEIFcondition2ﻫ指令序列2 ;条件"condition2"成立时所执行的指令序列ﻫ.ENDIF
其中:
条件表达式“condition”的书写方式与C语言中条件表达式的书写方式相似,也可用括号来组成复杂的条件表达式。
条件表达式中可用的操作符有:
==(等于)、!
=(不等)、>(大于)、>=(大于等于)、<(小于)、<=(小于等于)、&(位操作与)、!
(逻辑非)、&&(逻辑与)、||(逻辑或)等。
若在条件表达式中检测标志位的信息,则可以使用的符号名有:
CARRY?
(相当于CF==1)、OVERFLOW?
(OF==1)、PARITY?
(PF==1)、SIGN?
(SF==1)、ZERO?
(ZF==1)等。
例如:
.IF CARRY?
&&AX!
=BX ;检测CF==1且AX!
=BX是否成立
;汇编语言指令序列ﻫ.ENDIF
在指令序列中,还可再含有其它的.IF伪指令,即:
允许嵌套。
伪指令.ELSEIF引导出另一个二叉分支,但它不能作伪指令块的第一个伪指令。
汇编程序在对“条件表达式”进行代码转换时将进行代码优化处理,以便尽可能生成最好的指令代码。
如:
.IF ax== 0
汇编程序会把它转换为指令“ORax,ax”,而不是“CMP ax, 0”,因为前者比后者更好,而不是简单直接地转换为后者。
如果用伪指令来书写分支结构,那么,例6.5的代码段部分就可写成如下程序段:
…
MOV
AL, CHAR1
.IFAL>='a'&&AL<='z'
;语句象C语言语句吗?
SUBCHAR1, 20H
.ENDIF
…
也可把例6.6的代码段部分就可写成如下程序段:
…
MOV AX,X
.IF AX< 0
ADD AX,10
;计算第一种情况的结果
.ELSEIFAX<=10
MOVBX,30D
IMULBX
;计算第二种情况的结果
.ELSE
SUBAX,9
;计算第三种情况的结果
.ENDIF
MOVY,AXﻫ…
;把计算结果保存到变量Y中
例6.9 根据当前计算机的时间和日期,显示上午(AM)或下午(PM),以及所在的季节。
解:
显示解答
6.2.3 循环结构
循环结构是一个重要的程序结构,它具有重复执行某段程序的功能。
通常,循环结构包括以下四个组成部分:
1、循环初始化部分——初始化循环控制变量、循环体所用到变量;ﻫ2、循环体部分——循环结构的主体;ﻫ3、循环调整部分——循环控制变量的修改、或循环终止条件的检查;
4、循环控制部分——程序执行的控制转移。
以上四部分可以在程序中用各种不同的形式体现出来,有时也并非清析地表达出来。
常用的循环结构如图6.3所示。
(a)、Do—While结构
(b)、While结构
图6.3常用的循环结构示意图
一、用循环指令构成循环结构
在编写循环结构的程序片段时,我们可以多种方法来循环结构。
如:
循环次数是已知的,可用LOOP指令来构造循环;当循环次数是未知或不定的,则可用条件转移或无条件转移来构成循环结构。
例6.10分类统计字数组data中正数、负数和零的个数,并分别存入内存字变量Positive、Negative和Zero中,数组元素个数保存在其第一个字中。
解:
显示解答
例6.11 计算数组score的平均整数,并存入内存字变量Average中,数组以-1为结束标志。
解:
DATA1
SEGMENT
data
DW 90,95,54,65, 36, 78,66,0,99,50,-1
Average
DW 0
DATA1
ENDS
CODE1
SEGMENT
ASSUME CS:
CODE1, DS:
DATA1
START:
MOV
AX,DATA1
MOV
DS,AX
XOR
AX,AX
XOR
DX,DX
;用(DX,AX)来保存数组元素之和
XOR
CX, C