资源描述
第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
DW 9876h
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 stack segment (其中:xxxx是错误号)
在源程序中,可用以下方法来定义堆栈段。
方法1:
STACK1
SEGMENT
DB 256 DUP(?)
;256是堆栈的长度,可根据需要进行改变
TOP
LABEL WORD
STACK1
ENDS
以上堆栈段的定义如图6.1所示。由于堆栈是按地址从大到小的存储单元顺序来存放内容的,所以,在堆栈存储单元说明语句之后,再说明一个栈顶别名,这样,对栈顶寄存器SP的赋值就很方便。
在源程序的代码段中,还要添加如下程序段,才能把段STACK1当作堆栈段来使用。
图6.1 堆栈段定义示意图
…
ASSUME
SS:STACK1
;可在代码段的段指定语句中一起说明
CLI
;禁止响应可屏蔽中断
MOV
AX, STACK1
MOV
SS, AX
MOV
SP, offset TOP
;给堆栈段的栈顶寄存器SP赋初值
STI
;恢复响应可屏蔽中断
…
方法2:
STACK1
SEGMENT STACK
;定义一个堆栈段,其段名为STACK1
DB 256 DUP(?)
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
;能XCHG word1, 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
;指令AND CHAR1, 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)
{case 0:
b = 32;
break;
case 1:
case 2:
b = a + 43;
break;
case 3:
b = 2*a;
break;
case 4:
b--;
break;
case 5:
case 6:
case 7:
printf(“Function 5_6_7”);
break;
}
解:
DATA1
SEGMENT
…
A
DW ?
B
DW ?
Table
DW case0. case12, case12, case3
DW case4, case567, case567, case567
MSG
DB 'Function 5_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
用地址表实现多路转移的关键在于:转移入口的地址表和转移情况可整数化。如果这二个要求有一个不满足,或很难构造,则无法使用该方法。
为了改善汇编语言源程序的结构,减少显式转移语句所带来混乱,在宏汇编MASM 6.11系统中,增加了表达分支结构的伪指令。该伪指令的书写格式与高级语言的书写方式相类似,汇编程序在汇编时会自动增加转移指令和相应的标号。理解并掌握该知识,对将来学习《编译原理》课程也有一定的帮助。
分支伪指令的具体格式如下:
格式1:
.IF condition ;以英文“句号”开头
指令序列 ;条件"condition"成立时所执行的指令序列
.ENDIF
格式2:
.IF condition
指令序列1
.ELSE
指令序列2 ;条件"condition"不成立时所执行的指令序列
.ENDIF
格式3:
.IF condition1
指令序列1
.ELSEIF condition2
指令序列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
汇编程序会把它转换为指令“OR ax, ax”,而不是“CMP ax, 0”,因为前者比后者更好,而不是简单直接地转换为后者。
如果用伪指令来书写分支结构,那么,例6.5的代码段部分就可写成如下程序段:
…
MOV
AL, CHAR1
.IF AL>='a' && AL<='z'
;语句象C语言语句吗?
SUB CHAR1, 20H
.ENDIF
…
也可把例6.6的代码段部分就可写成如下程序段:
…
MOV AX, X
.IF AX < 0
ADD AX, 10
;计算第一种情况的结果
.ELSEIF AX <= 10
MOV BX, 30D
IMUL BX
;计算第二种情况的结果
.ELSE
SUB AX, 9
;计算第三种情况的结果
.ENDIF
MOV Y, 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, CX
;用CX来保存数组元素个数
LEA
SI, data
;用指针SI来访问整个数组
again:
MOV
BX, word ptr [SI]
CMP
BX, 0
JL
over
ADD
AX, BX
ADC
DX, 0
;把当前数组元素之值加到(DX,AX)中
INC
CX
;数组元素个数加1
ADD
SI, 2
JMP
again
over:
JCXZ
exit
;防止零作除数,即数组是空数组
DIV
CX
MOV
Average, AX
exit:
MOV
AX, 4C00H
INT
21H
CODE1
ENDS
END
START
二、用伪指令实现的循环结构
在宏汇编MASM 6.11系统中,还增加了表达循环结构的伪指令,以便更清晰地表达WHILE循环、REPEAT-UNTIL循环。另外,还增加两个辅助循环的伪指令。这些伪指令的书写格式和含义与高级语言中相应语句的书写格式和含义相一致,所以,这些伪指令是很容易掌握的,也是非常有用的。
循环伪指令的格式和含义如下:
1、WHILE型循环伪指令
.WHILE condition
循环体的指令序列 ;条件"condition”成立时所执行的指令序列
.ENDW
其中:.ENDW与前面的.WHILE相匹配,它标志着其循环体到此结束。
如果条件表达式“condition”在循环开始时,就为“假”(false),那么,该循环体一次也不会被执行。
2、REPEAT型循环伪指令
.REPEAT
循环体的指令序列
.UNTIL condition
.REPEAT
循环体的指令序列
.UNTILCXZ [condition]
REPEAT型循环在执行完循环体后,才判定逻辑表达式condition的值。若该表达式的值为真,则终止该循环,并将执行伪指令.UNTIL[CXZ]后面的指令,否则,将向上跳转到伪指令.REPEAT之后的指令,为继续执行其循环体作准备。
如果.UNTILCXZ后面没有写逻辑表达式,那么,由.REPEAT-.UNTILCXZ所构成的循环与用LOOP指令所过程的循环是一致的,它们都是以“CX=0”为循环终止条件。
如果.UNTILCXZ后面书写了逻辑表达式,那么,该逻辑表达式的形式只能是:“EXP1==EXP2”或“EXP1!=EXP2”。所以,这时由“.REPEAT-.UNTILCXZ condition”所构成的循环就与用LOOPNE/LOOPE指令所过程的循环是一致的,它们都是以“condition || CX=0”为循环终止条件。
和高级语言的REPEAT型的循环一样,.REPEAT-.UNTIL[CXZ]的循环体也会至少被执行一次。
.WHILE-.ENDW和.REPEAT-.UNTIL[CXZ]的循环体内还可再含有循环伪指令,这样就构成了循环结构的嵌套。
汇编程序在生产指令代码时会进行代码优化,以便尽可能得到最优化的指令序列。
3、辅助循环伪指令
(1)、终止循环伪指令
.BREAK
.BREAK .IF condition
该伪指令用来终止包含它的最内层循环。前者是无条件终止循环,后者是仅当逻辑表达式condition为真时,才终止循环。
.WHILE 1
.REPEAT
…
.BREAK .IF condition
…
…
.BREAK .IF condition
…
ENDW
.UNTIL 0
对于以上二个循环,如果没有指令来终止循环的话,它们都将进入死循环状态,但如果在该层循环体内,存在伪指令“.BREAK .IF condition”的话,那么,当逻辑表达式condition为真时,该循环就会被终止了。
(2)、循环继续伪指令
.CONTINUE
.CONTINUE .IF condition
该伪指令用于直接跳转到包含它的最内层循环的计算循环条件表达式的代码处。前者是无条件转移到计算循环条件表达式的代码处,后者是仅当条件表达式condition为真时,才进行这样的跳转。
辅助循环伪指令.BREAK和.CONTINUE只能在伪指令.WHILE-.ENDW和.REPEAT-.UNTIL的循环体内使用。
例6.12 显示9个数字字母'1'~'9',26个大写字母,和显示任意输入的数字字符,并用按“回车”键来结束本程序的运行。
解:
DATA1
SEGMENT
MSG1
DB 13, 10, "Iteration: "
NUM1
DB '1', "$"
MSG2
DB 13, 10, "Alphabet: $"
NUM2
DB 'A', " $"
MSG3
DB 13, 10, "Type digits, then press ENTER: $"
DATA1
ENDS
CODE1
SEGMENT
ASSUME CS:CODE1, DS:DATA1
START:
MOV
AX, DATA1
MOV
DS, AX
MOV
CX, 9
MOV
AH, 09H
MOV
DX, OFFSET MSG1
.REPEAT
INT 21H
INC NUM1
;显示Iteration: 1,2,~,9
.UNTILCXZ
MOV
DX, OFFSET MSG2
INT
21H
;显示字符串"Alphabet:"
MOV
AH, 09H
MOV
DX, OFFSET NUM2
.REPEAT
INT 21H
INC NUM2
;显示当前字母
;当前字母向后移
.UNTIL NUM2 > 'Z'
;显示整个大写字母表
MOV
AH, 09H
MOV
DX, OFFSET MSG3
INT
21H
.WHILE 1
;循环条件为永真的循环
MOV AH, 07H
INT 21H
;不带回显地从键盘读一个字符
.BREAK .IF AL == 13
;如果输入“回车”键,则终止循环
.CONTINUE .IF (AL<'0') || (AL>'9')
;如果字符不是数字字符,则继续循环
MOV DL, AL
MOV AH, 02H
INT 21H
;显示所输入的数字字母
.ENDW
MOV
AX, 4C00H
INT
21H
CODE1
ENDS
END
START
6.3 段的基本属性
在通常情况下,一个复杂的应用程序会由若干个模块组成,一个模块又会含有多个段。而不同模块的段之间、同一模块的段之间往往存在某种联系,这种联系就要体现在段属性的说明上。
段定义的一般格式如下:
段名
SEGMENT [对齐类型] [组合类型] [类别]
…
段名
ENDS
段属性“对齐类型”、“组合类型”和“类别”要按此顺序说明,但这些可选项可根据需要选择书写。如果源程序中不指定某个属性,那么,汇编程序将使用该属性的缺省值。
程序中的段名可以是唯一的,也可以与其它段同名。在同一模块中,如果有二个段同名,则后者被认为是前段的后续,这样,它们就属同一段。
当同一模块出现二个同名段时,则后者的可选项属性要么与前者相同,要么不写其属性而选用前者的段属性。
例6.13 同段名的作用
DATA1
SEGMENT ;第一个数据段
MSG
DB "Hello, "
DATA1
ENDS
CODE1
SEGMENT ;第一个代码段
ASSUME CS:CODE1, DS:DATA1
START:
MOV
AX, DATA1
MOV
DS, AX
MOV
DX, offset MSG
MOV
AH, 9
INT
21H
CODE1
ENDS
DATA1
SEGMENT ;第二个数据段
DB "World.$"
DATA1
ENDS
CODE1
SEGMENT ;第二个代码段
MOV
AX, 4C00H
INT
21H
CODE1
ENDS
END
START
END
在上面的例子中,第二个数据段是第一个数据段的后续,汇编程序把它们是合二为一,上述的代码段也如此。
下面,详细说明段属性的含义及其作用。
6.3.1 对齐类型(ALIGN)
对齐类型表示当前段对起始地址的要求,连接程序(LINK.EXE)按表6.1的地址格式来定位段的起始地址。在进行段定位时,会根据其定位类型进行定位的,所以,各段之间就有可能出现一些空闲字节,即可能浪费几个字节单元。
段对齐类型PARA是一个适用于所有段类型的对齐类型,它也是缺省的对齐类型。对齐类型BYTE和WORD通常用于数据段的定位,对齐类型DWORD通常用于80386及其以后CPU代码段的定位。
表6.1 段对齐类型与段起始地址之间的对应关系
对齐类型
起始地址(二进制)
功能说明
最多的空闲字节数
BYTE
xxxx xxxx xxxx xxxx xxxx
下一个字节地址
0
WORD
xxxx xxxx xxxx xxxx xxx0
下一个字地址
1
DWORD
xxxx xxxx xxxx xxxx xx00
下一个双字地址
3
PARA
xxxx xxxx xxxx xxxx 0000
下一个节地址
15
PAGE
xxxx xxxx xxxx 0000 0000
下一个页地址
127
6.3.2 组合类型(COMBINE)
组合类型是告诉连接程序如何把不同模块中段名相同的段合并在一起。具体的组合类型如下:
NONE
表示当前段在逻辑上独立于其它模块,并有其自己的基地址。NONE是缺省的组合类型。
PUBLIC
表示当前段与其它模块中同段名的PUBLIC类型段组合成一个段。组合的先后次序取决于LINK程序中目标模块排列的次序。在组合时,后续段的起始地址要按其对齐类型进行定位,所以,同名段之间可能有间隔。
COMMON
表示当前段与其它模块中同名段重叠,也就是说,它们的起始地址相同。最终段的长度是同名段的最大长度。由于段覆盖,所以,前一同名段中的初始化数据被后续段的初始数据覆盖掉。
STACK
组合类型STACK表示当前段是堆栈栈,其组合情况与PUBLIC相同。
AT 数值表达式
该数值表达式是当前段所指定的绝对起始地址的段地址。
6.3.3 类别(CLASS)
类别是一个由程序员指定的用单引号括起来的字符串。如果一个段没有给出类别,那么,这个段的类别就为空。类别是用于段的分类,连接程序利用该类别来调整同名、同类别的段,并使它们相邻。典型的类别是"Data"和"Code"。如果指定某段的类别是"Code",那么,该段最好是代码段,这样,有的调试程序(如:CodeView)就可以顺序工作。
例如:
DATA1
SEGMENT WORD PUBLIC "Data"
…
DATA1
ENDS
上述段定义说明了该段的起始地址是下一个字地址、组合类型为PUBLIC、段类别是"Data"。
6.3.4 段组(GROUP)
段组伪指令GROUP是用于把源程序模块中若干个段结合成一个组,并对该段组定义一个段组名。段组伪指令的格式如下:
段组名 GROUP 段名[, 段名, ……]
其中:段名之间要用逗号间隔,段名也可以用表达式“SEG 变量”或“SEG 标号”。
下面举例说明段组伪指令的使用方法和作用。
例6.12 段组的作用
方法1:用一个段寄存器对应二个数据段
DATA1
SEGMENT
;第一个数据段
b1
DB 10h
DATA1
ENDS
DATA2
SEGMENT
;第二个数据段
b2
DB 23h
DATA2
ENDS
CODE1
SEGMENT
ASSUME CS:CODE1, DS:DATA1
;(1)
START:
MOV
AX, DATA1
MOV
DS, AX
;(2)把数据段DATA1的段值赋给段寄存器DS
…
MOV
BL, b1
;(3)引用DS来访问DATA1中的变量b1
…
ASSUME DS:DATA2
;(4)
MOV
AX, DATA2
MOV
DS, AX
;(5)把数据段DATA2的段值赋给段
展开阅读全文