1.5 结构类型伪指令
前面,我们介绍了汇编语言中六个最基本的数据类型,这些数据类型能满足程序设计中绝大多数情况的需要,但也存在需要更复杂的数据类型的情况。
下面介绍汇编语言所提供的三种复合数据类型的说明形式。
1、重复说明符DUP
从前面的内容里,我们知道了定义少量内存变量的定义形式,但如果在程序中要说明50个、100个、200个甚至更多的、同类型的内存变量时,若采用前面所学的方法,对它们一一加以说明显然是不可行的。为此,汇编语言提供了变量的重复说明符DUP,其说明的一般形式如下:
count DUP (表达式, 表达式, …, 表达式)
解释:count是重复次数,(表达式, 表达式, …, 表达式)是被重复的部分,“表达式”可以是存储单元的初值,也可以是含义另一个DUP的式子。如果在表达式的括号中有多个表达式,那么,它们之间要用逗号','分开。
例如:
BUFFER DB 100 DUP(?)
STRING DB 120 DUP('ABCDE'), 0
DATA1 DW 50 DUP(10H, 20 DUP(1,2,3), 20H)
POINTS DD 12, 30 DUP(0)
从上面的例子可看出:用DUP说明内存变量相当于在高级语言中定义数组。
2、结构类型的定义
重复说明符DUP只能用于重复同一数据类型的变量说明,它不可以重复不同数据类型的变量说明。为了把一组不同类型的变量说明组合在一起,汇编语言提供了另一种复合数据类型说明符——结构类型说明符STRUC。
用STRUC和ENDS可以把一系列数据定义语句括起来作为一种新的、用户定义的结构类型。其一般说明格式如下:
结构名 STRUC [Alignment][, NONUNIQUE]
数据定义语句序列
结构名 ENDS
解释:结构名是一个合法的标识符,且具有唯一性。结构名代表整个结构类型,前后两个结构名必须一致。结构内被定义的变量为结构字段,变量名即为字段名。
一个结构中允许含有任意多个字段,各字段的类型和所占字节数也都可任意。如果字段有字段名,则字段名必须唯一。每个字段可独立存取。
l 对齐方式(Alignment):可用1、2或4来指定结构中字段的字节边界(Byte boundary),其缺省值为1。见4.3.2节中的有关叙述;
l NONUNIQUE:要求结构中的字段必须用全名才能访问,见本小节中的“结构类型字段的引用”。
例如:
COURSE STRUC
NO DD ? ;学号
CNAME DB 'Assember' ;名字
SCORE DW 0 ;得分
COURSE ENDS
上例中,COURSE是结构名,它含有三个字段:NO、CNAME和SCORE,这些字段的类型分别是DD、DB和DW。
我们不难看出:结构类型COURSE共占14个字节,其字段NO、CNAME和SCORE的偏移量分别为:0、4和12。
结构中的字段可以有字段名,也可以没有字段名。有字段名的字段可直接用该字段名来访问它,没有字段名的字段可以用该字段在结构中的偏移量来访问。
例如:
PEASON STRUC
NO DD ? ;偏移量为0
NAME DB 10 dup (?) ;偏移量为4
DB 1 ;偏移量为14
PEASOM ENDS
在结构PEASON中,有二个字段有字段名,一个字段没有字段名,但不管有无字段名,我们都可用其偏移量来访问它。
结构类型变量的定义
在定义某个结构类型后,程序员就可以说明该结构类型的内存变量。它的说明形式与前面介绍的简单数据类型的变量说明基本上一致。其定义格式如下:
[变量名] 结构名 <[字段值表]>
解释:
1)、 变量名即为该结构类型的变量名,它可省缺。如果省缺,则不能用符号名来访问该内存单元;
2)、 字段值表是给字段赋初值,中间用逗号','分开,其字段值的排列顺序及类型应与该结构说明时各字段相一致;
3)、 如果结构变量中某字段用其说明时的缺省值,那么,可用逗号来表示;如果所有字段都如此,则可省去字段值表,但必须保留一对尖括号"<"、">"。
例如:
COURSE1 COURSE <> ;使用缺省的初值
COURSE2 COURSE <1, 'Pascal', 60>
COURSE3 COURSE <2, , 84> ;使用缺省的课程名
PEASON1 PEASON <1000, '张 三', 34>
结构类型字段的引用
定义了结构类型的变量后,若要访问其结构中的某个字段,则可采用如下形式:
结构变量名.字段名
大家看到,是不是感觉有些像C语言啊?不错,该引用方式与高级语言的字段引用方式完全一致,我们还可用偏移量来访问其中的某个字段,但此方法不直观,变动性大,所以,一般情况下,不提倡使用此方法。
例如:
EXAM1 STRUC
F1 DW ?
F2 DB ?
EVEN ;偶对齐
F3 DW ?
EXAM1 ENDS
E1 EXAM1 <1234H,'A',8765H> ;定义结构EXAM1的一个变量E1
下面二种方法都可以把结构变量E1中字段的内容赋给寄存器AX,但如果在字段F3之前增加或减少了字段,那么,这些引用需要改变吗?
(1)、用字段名直接引用
MOV AX, E1.F3
(2)、用字段的偏移量间接引用(不提倡)
LEA SI, E1
MOV AX, [SI+4] ;其中4是字段F3的偏移量
1.6 联合类型伪指令
联合数据类型是一种特殊的数据类型。它可以实现:以一种数据类型存储数据,以另一种数据类型来读取数据。程序员可以根据不同的需要,以不同的数据类型来读取联合类型中的数据。也就是说,在一些情况下,以一种数据类型来读取联合类型中的数据,而在另一些情况下,又以另一种数据类型来读取其数据。
1、联合类型的说明
联合数据类型其说明格式如下:
[联合类型名] UNION [Alignment] [,NONUNIQUE]
数据定义语句序列
[联合类型名] ENDS
联合类型中的各字段相互覆盖,即同样的存储单元被多个不同的字段所对应,并且其每个字段的偏移量都为0。联合类型所占的字节数是其所有字段所占字节数的最大值。
l 对齐方式(Alignment):可用1、2或4来指定结构字节的边界,其缺省值为1。它还用可伪指令ALIGN或EVEN来重新定界,也可用命令行选项/Zp来定界;
l NONUNIQUE:要求联合类型中的字段必须用全名才能访问,引用联合类型字段的方法见下面的“联合类型字段的引用”。
例如:
DATATYPE UNION
BB DB ? ;定义一个字节类型的字段
WW DW ? ;定义一个字类型的字段
DD DD ? ;定义一个双字类型的字段
DATATYPE ENDS
联合类型DATATYPE的字段分布如图4.8所示。
在联合类型的最外层定义中,在伪指令UNION和ENDS的前面一定要书写该联合类型名,而在其嵌套定义的内层,伪指令UNION和ENDS之前一定不能写联合类型名。
例如:
UNION1 UNION
BB DB ?
WW DW ?
UNION ;联合类型的嵌套定义形式
W1 DW ?
B1 DB ?
ENDS
UNION1 ENDS
2、联合类型变量的定义
联合数据类型的变量只能用第一个字段的数据类型来进行初始化。
例如:
U1 DATATYPE <'J'> ;定义一个联合变量,并初始化其值
U2 DATATYPE <1234H> ;初始化错误,只能用字节数据来初始化
U3 UNION1 <1>
3、联合类型字段的引用
定义了联合类型的变量后,就可根据需要,以不同的数据类型或字段名来存取该联合类型中的数据。引用其字段的具体形式如下:
联合类型变量名.字段名
例如:
MOV U1.WW, 1234H ;给联合类型变量赋字数据
MOV AL, U1.BB ;AL=34H
MOV BX, U1.WW ;BX=1234H
MOV U1.BB, 'A' ;U1的值1241H,41H是'A'的ASCII码
1.7 记录类型伪指令
1、记录类型的说明
汇编语言的记录类型与高级语言的记录类型不同,它是为按位存取数据提供方便的。记录类型的说明要用到另一个保留字RECORD,其说明格式如下:
记录名 RECORD 字段 [, 字段, ……]
其中“字段”代表:字段名:宽度[=初值表达式]
解释:
l 记录名代表该记录类型;
l 记录类型可以由多个字段组成,每个字段之间要用逗号','分开;
l 字段的属性包括字段名、宽度和初值;
l 字段的“宽度”表示该字段所占的二进制位数,它必须是一个常数,并且所有字段的宽度之和不能大于16;如果记录的总宽度大于8,则系统为该记录类型分配二个字节,否则,只分配一个字节;
记录的最后一个字段排在所分配空间的最低位,然后对记录中的字段依次“从右向左”分配二进制位,左边没有分完的二进制位补0;
初值表达式给出的是该字段的缺省值。如果初值超过了该字段的表示范围,那么,在汇编时将产生错误提示信息;如果某字段没有初值表达式,则其初值为0。
例如:
COLOR RECORD BLINK:1, BACK:3=0, INTENSE:1=1, FORE:3
FLOAT RECORD DSIGN:1, DATA:8, ESIGN:1, EXP:4
记录类型COLOR有四个字段:BLINK、BACK、INTENSE和FORE,它们的宽度分别为:1、3、1和3,所以,该记录类型共有8位二进制,系统给分配它一个字节。
2、记录变量的定义
在程序中,必须先说明记录类型,然后才能定义该记录类型的变量。记录变量是把其二进制位分成一个或多个字段的字节或字变量。其定义格式与其它类型变量的定义方式类似,具体如下:
[变量名] 记录名 <[字段值表]>
解释:
l 变量名即为该记录类型的变量名,它可省缺。如果省缺,则不能用符号名来访问该内存单元;
l 字段值表是给字段赋初值,中间用逗号','分开,其字段值的排列顺序及大小应与该记录说明时各字段相一致;
l 如果记录变量的某字段用其说明时的缺省值,那么,可用逗号来表示;如果所有字段都如此,则可省去字段值表,但必须保留一对尖括号"<"、">"。
例如:
COLOR1 COLOR <>, <1, 7, 0, 5>, <1, , 0, 7>
FLOAT1 FLOAT <1, 23H, 0, 3>, <0, 89H, 1, 5>
3、记录的专用操作符
操作符WIDTH和MASK是作用于记录类型的两个专用保留字,利用它们可得到记录类型的不同属性:
l 操作符WIDTH
操作符WIDTH返回记录或其字段的二进制位数,即其宽度。其一般书写格式如下:
WIDTH 记录名 或 WIDTH 记录字段名
假设有前面定义的记录类型COLOR,那么,WIDTH COLOR的值为8,WIDTH BACK的值为3,WIDTH BLINK的值为1。
l 操作符MASK
操作符MASK返回一个8位或16位二进制数。在该二进制数中,被指定记录或字段使用的对应位的值为1,否则,其值为0。其一般书写格式如下:
MASK 记录名 或 MASK 记录字段名
假设有前面定义的记录类型FLOAT,那么,MASK EXP的值为000FH,MASK DATA的值为1FE0H,WIDTH DSIGN的值为2000H。
l 记录字段
记录字段名是一个特殊的操作符,它本身也是操作数,其返回值是该字段移到所在记录的最低位所需要的位数,即该字段最低位在记录中的位置。
在有了一些数据类型后,程序员还可定义这些数据类型的别名或指针类型。表达这种定义的伪指令是TYPEDEF,其定义形式如下:
新数据类型名 TYPEDEF [位距] [PTR] 数据类型
其中:“位距”是NEAR、FAR或PROC等。
例如:
CHAR TYPEDEF BYTE ;给BYTE定义另一个别名CHAR
PCHAR TYPEDEF PTR CHAR ;定义一个字符指针数据类型PCHAR
有了上述定义之后,下面的变量说明就是合法的。
CH1 CHAR 'ABCDEF' ;定义一个字符串常量
PCH1 PCHAR CH1 ;定义一个指向字符串常量CH1的变量
1.8 标号和属性相关伪指令
1、标号
标号是一种特殊的标识符,它代表代码段中的某个具体位置,它主要用于表明转移的目标位置。其说明形式如下:
标号: 汇编语言指令 ;注释
解释:标号必须是一个合法的标识符,在其后面紧跟一个冒号":",冒号与汇编语言指令之间要有分隔符。通常用若干个空格、TAB来作分隔符,一般用分隔符使有关内容对齐为宜。
2、内存变量和标号的属性
变量是一个符号地质,其值会根据其数据类型来对应从该地址以后的若干个存储单元中所存的数值。标号也是一个符号地址,它所对应的存储单元中存放的是指令代码。虽然它们在某些性质上有所不同,但它们都是一个符号地址,代表一个存储单元的地址,所以,它们都具有存储单元的属性。除此之外,它们还有各自特殊的属性。
下面介绍内存变量和标号的属性及其有关操作符。
段属性操作符
段属性操作符(SEG)返回该标识符所在段的段地址。我们一般只会取内存变量所在段的段地址,而很少取标号所在段的段地址。
假设有下面变量定义:
…
SCORE DW ?
NAME DB 10 DUP(10) ;数据段的变量定义
…
MOV AX, SEG SCORE ;代码段的指令
MOV BX, SEG NAME
由于SCORE和NAME在同一段中定义,所以,寄存器AX和BX的值是相等的。
偏移量属性操作符
偏移量属性操作符(OFFSET)返回该标识符离它所在段的段地址有多少字节。一般情况,程序员只会取内存变量的偏移量,而不太关心标号的偏移量。
假设有下面变量定义:
FIRST DD 12345678H, 0 ;数据段的变量定义
SCORE DW ?, 12H
NAME DB 10 DUP(10)
…
MOV AX, OFFSET SCORE ;代码段的指令
MOV BX, OFFSET NAME
…
假设FIRST是数据段的第一个被定义的变量名,它的偏移量为0,SCORE的偏移量为8,因为它要跳过二个双字,其它如此类推。
由于NAME在SCORE之后,且SCORE之后有二个字,占四个字节,所以,BX的值要比AX的值大4。
类型属性操作符
类型属性操作符(TYPE)是返回该变量所占字节数,或标号的“远”(FAR)、“近”(NEAR)类型。常用标识符的类型值如表所列:
标识符种类 字节变量 字变量 双字变量 近标号(NEAR) 远标号(FAR)
TYPE的值 1 2 4 -1 -2
例如:
PEASON STRUC
NO DD ?
NAME DB 10 dup (?)
DW 1
PEASOM ENDS
…
B1 DB 1, 2, 3
W1 DW 200 DUP(1,2,30 DUP(10,20)), 101H, -1
PEOPLE PEASON <>
按属性TYPE的含义,TYPE B1、TYPE W1和TYPE PEOPLE的值分别为:1,2和16。
长度属性操作符
长度属性操作符(LENGTH)是针对内存变量的操作符,它返回重复操作符DUP中的重复数。如果有嵌套的DUP,则只返回最外层的重复数;如果没有操作符DUP,则返回1。
如上例所示,根据属性LENGTH的含义,LENGTH B1、LENGTH W1和LENGTH PEOPLE的值分别为:1,200和1。
容量属性操作符
容量属性操作符(SIZE)也是针对内存变量的操作符。它的返回值按下列公式计算:
SIZE 变量 = (LENGTH 变量) × (TYPE 变量)
如上例所示,SIZE B1、SIZE W1和SIZE PEOPLE的值分别为:1,400和16。
强制属性操作符(重点)
在程序中,我们有时需要对同一个存储单元以不同的属性来访问,或对一些不确定的存储属性需要显式指定等,这时,我们就需要强制属性操作符PTR。该操作符的作用有点象C语言中的类型强制方法。
对于指令:MOV [BX], 1H,其目标操作数[BX]是寄存器间接寻址方式,它指向一个存储单元。在作传送操作时,是把“1H”扩展成8位作字节传送,还是扩展成16位作字传送呢?这就使该指令具有二义性,因为[BX]指向的存储单元可以字节或字的首地址。含有该指令的程序在汇编时,可能会产生警告或出错信息。
为了使指令中存储单元操作数具有明确的属性,我们可以使用强制属性操作符PTR。其一般格式为:
数据类型 PTR 地址表达式
其中:数据类型是前面所学的各种数据类型,常用的数据类型有:BYTE、WORD、DWORD、NEAR和FAR等。
为了明确指令中存储单元的属性,可把指令“MOV [BX], 1H”可改写成:
MOV byte ptr [BX], 1H 或 MOV word ptr [BX], 1H
在指令中用操作符PTR强制后,不管其后的地址表达式原数据类型是什么,在本指令中就以PTR前面的类型为准。该强制属性只在本指令有效,是一种临时性的属性,它不会改变原内存单元的定义属性。
例如:
W1 DW 1234H, 5678H
B1 DB 2
DB 5
D1 DD 23456789H
…
MOV AX, word ptr b1 ;把B1开始的二个字节拼接成一个字,执行后,(AX)=0502H
MOV BH, byte ptr w1 ;把字W1的低字节传送给BH,执行后,(BH)=34H
MOV CH, byte ptr w1+1 ;把字W1的高字节传送给CH,执行后,(CH)=12H
MOV word ptr d1, 12H ;把双字D1的低字修改成0012H,执行后,(D1)=23450012H
上面指令中的强制属性是临时属性,它不能改变这些变量在定义时的永久属性。
存储单元别名操作符
如果需要以另一种数据类型来访问某一存储单元时,可用强制属性操作符PTR来实现。但如果在程序中要经常以某种其它的数据类型来访问该存储单元的话,那么,就必须在每次访问时都要加上强制属性操作符PTR。这样做虽然可行,但在编写程序时就显得比较麻烦。
为了克服上述不便,汇编语言提供了另一种操作符THIS,它为同一存储单元取另一别名,该别名可具有其自身的数据属性,但其段地址和偏移量是不变的。
操作符THIS的一般格式为:
THIS 数据类型
其中:数据类型是前面所学的各种数据类型,常用的数据类型有:BYTE、WORD、DWORD、NEAR和FAR等。
例如:
WBUFFER EQU THIS WORD ;EQU是一个等价符号定义语句,在后面有介绍
BUFFER DB 20 DUP(?)
这样就给同一片存储单元,取了二个具有不同数据类型的变量名。于是,在指令中,引用不同的变量名,就使用其不同的数据属性:
l 如果引用变量名WBUFFER,则是按“字”属性来访问;
l 如果引用变量名BUFFER,则是按“字节”属性来访问。
如此一来,指令“MOV AX, word ptr BUFFER”和“MOV AX, WBUFFER”是等效的,所不同的是:当以“字”属性访问BUFFER存储区时,不必使用强制属性符PTR,而改用“字”属性变量WBUFFER即可。
1.9 表达式伪指令
表达式是程序设计课程里的一个重要的基本概念,它可由运算符、操作符、括号、常量和一些符号连在一起的式子。在汇编语言中,表达式分为:数值表达式和地址表达式。
1、进制伪指令RADIX
伪指令RADIX用来设置整数的缺省进制,宏汇编开始时所默认的整数进制为十进制。该伪指令的使用格式如下:
.RADIX exp
其中:伪指令前面要用点‘.’开始,exp的值必须是区间[2, 16]内的一个整数。
该伪指令说明其下面整数的默认进制为exp。如果某整数已显式地表明了其进制,则该默认进制对其不起作用。在源文件中,可以使用多个RADIX伪指令来分别说明其后整数的默认进制,但为了避免引起不必要误会,我们不提倡这样去做。
例如:
.radix 8
B1 DB 10, 11, 12 ;这三个数是八进制数
DB 10D ;这数是十进制数,因为它已用'D'明确说明而不使用缺省进制
…
.radix 10
MOV AX, 1234 ;1234是十进制数
MOV AX, 1234H ;1234H是十六进制数
思考一下:
.radix 16
DW 90D, 101B ;前者是十进制数,后者是二进制数吗?
2、数值表达式
数值表达式是在汇编过程中能够由汇编程序计算其值的表达式,其组成部分在汇编时就能完全确定。它通常是一些常量的运算组合。
(1)常量
常量是一个立即数,直接写在汇编语言语句中,在程序的执行过程中,它不可能发生变化。通常,我们用二进制、八进制、十进制或十六进制来书写常量。
例如:10101011B、324Q、1234D、1234H、0abcdH、'AB'等都是常量。
在程序中,我们还可用伪指令.RADIX来改变数据的基数,在后面再详细讲解。
(2)算术运算符
算术运算符包括符号:+(正)、-(负),运算符:+(加)、-(减)、*(乘)、/(除)和MOD(取模)。这些运算符和常量、括号可组成数值表达式。
如:120+(321-90) mod 3,322*5/32,0abcdH+5,-590等
(3)关系运算符
关系运算符包括符号:EQ(相等)、NE(不等)、LT(小于)、GT(大于)、LE(小于等于)和GE(大于等于)。这些关系运算符和常量、括号也可组成数值表达式。该表达式的计算结果规定如下:
若关系不成立,则该数值表达式的计算结果为0;否则,其结果为0FFFFH。
如:120H LT 100H+3,21H LE 21H等,它们的计算结果分别为:0和0FFFFH。
(4)逻辑运算符
逻辑运算符包括按位操作符和移位操作符。具体是:AND(逻辑与)、OR(逻辑或)、NOT(逻辑非)、XOR(异或)、SHL(左移位)和SHR(右移位)。这些逻辑运算符和常量、括号可组成数值表达式。
如:1 SHL 3,47H AND 0FH,NOT 56H等,它们的计算结果分别为:8,7和0A9H。
(5)表达式中的其它操作符
汇编语言中,还有其它可在数值表达式中使用的操作符。它们是:
l HIGH(高8位)、LOW(低8位)
l SEG(段地址)、OFFSET(偏移量)
l TYPE(标识符类型)、LENGTH(变量长度)、SIZE(变量容量)
l WIDTH(记录/记录字段宽度)、MASK(记录/记录字段的屏蔽位)等
在以上操作符中,只有HIGH和LOW没有介绍过,它们分别是选取表达式计算结果的高8位和低8位。其使用格式如下:
HIGH 表达式 LOW 表达式
如:HIGH (1234H+100H),LOW 1234H等,它们的选取结果分别为:13H和34H。
(6)运算符和操作符的优先级
在汇编语言中,有许多各种运算符和操作符,它们的优先级按从高到低的排列如下:
优先级:高LENGTH、SIZE、WIDTH、MASK、()、[]、.(用于结构字段)、<>(用于记录类型)
↓ PTR、SEG、OFFSET、TYPE、THIS、:(用于段超越前缀)
↓*、/、MOD、SHL、SHR
↓HIGH、LOW
↓+、-
↓EQ、NE、LT、LE、GT、GE
↓NOT
↓AND
↓OR、XOR
优先级:低 SHORT
这些符号及其优先级并不要强记它们,有些符号同时出现的可能性非常小。在以后的学习中对常用的几个加以运用也就记住了。
3、地址表达式
地址表达式是计算存储单元地址的表达式,它可由标号、变量名和由括号括起来的基址或变址寄存器组成。其计算结果表示一个存储单元的地址,而不是该存储单元的值。
例如:
B1 DB 10H, 11H, 12H
DB 'ABCD'
W1 DW 1234H, 5678H
表达式B1+1、B1+3和W1+2等都是地址表达式,显然这些地址表达式所对应的存储内容分别为:11H、'A'和5678H。
注意:地址表达式W1+1并不表示字变量W1之后一个字的存储单元,而是字变量W1之后一个字节的存储单元,它的存储单元值是:7812H。