汇编语言数据类型以及数据定义详解

汇编器识别一组基本的内部数据类型(intrinsic data type),按照数据大小(字节、字、双字等等)、是否有符号、是整数还是实数来描述其类型。这些类型有相当程度的重叠,例如,DWORD 类型(32 位,无符号整数)就可以和 SDWORD 类型(32 位,有符号整数)相互交换。

可能有人会说,程序员用 SDWORD 告诉读程序的人,这个值是有符号的,但是,对于汇编器来说这不是强制性的。汇编器只评估操作数的大小。因此,举例来说,程序员只能将 32 位整数指定为 DWORD、SDWORD 或者 REAL4 类型。

下表给出了全部内部数据类型的列表,有些表项中的 IEEE 符号指的是 IEEE 计算机学会出版的标准实数格式。

类型 用法
BYTE 8 位无符号整数,B 代表字节
SBYTE 8 位有符号整数,S 代表有符号
WORD 16 位无符号整数
SWORD 16 位有符号整数
DWORD 32 位无符号整数,D 代表双(字)
SDWORD 32 位有符号整数,SD 代表有符号双(字)
FWORD 48 位整数(保护模式中的远指针)
QWORD 64 位整数,Q 代表四(字)
TBYTE 80 位(10 字节)整数,T 代表 10 字节
REAL4 32 位(4 字节)IEEE 短实数
REAL8 64 位(8 字节)IEEE 长实数
REAL10 80 位(10 字节)IEEE 扩展实数

数据定义语句

数据定义语句(data definition statement)在内存中为变量留岀存储空间,并赋予一个可选的名字。数据定义语句根据内部数据类型(上表)定义变量。

数据定义语法如下所示:

[name] directive initializer [,initializer]...

下面是数据定义语句的一个例子:

count DWORD 12345

其中:

  • 名字:分配给变量的可选名字必须遵守标识符规范。
  • 伪指令:数据定义语句中的伪指令可以是 BYTE、WORD、DWORD、SBTYE、SWORD 或其他在上表中列出的类型。此外,它还可以是传统数据定义伪指令,如下表所示。

伪指令 用法 伪指令 用法
DB 8位整数 DQ 64 位整数或实数
DW 16 位整数 DT 定义 80 位(10 字节)整数
DD 32 位整数或实数     

数据定义中至少要有一个初始值,即使该值为 0。其他初始值,如果有的话,用逗号分隔。对整数数据类型而言,初始值(initializer)是整数常量或是与变量类型,如 BYTE 或 WORD 相匹配的整数表达式。

如果程序员希望不对变量进行初始化(随机分配数值),可以用符号 ? 作为初始值。所有初始值,不论其格式,都由汇编器转换为二进制数据。 初始值 0011 0010b、32h 和 50d 都具有相同的二进制数值。

向 AddTwo 程序添加一个变量

前面《整数加减法》一节中介绍了 AddTwo 程序,现在创建它的一个新版本,并称为 AddTwoSum。这个版本引入了变量 sum,它出现在完整的程序清单中:

;AddTowSum.asm

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD

.data
sum DWORD 0

.code
main PROC
    mov eax,5
    add eax,6
    mov sum,eax

    INVOKE ExitProcess,0
main ENDP
END main

可以在第 13 行设置断点,每次执行一行,在调试器中单步执行该程序。执行完第 15 行后,将鼠标悬停在变量 sum 上,查看其值。或者打开一个 Watch 窗口,打开过程如下:在 Debug 菜单中选择 Windows(在调试会话中),选择 Watch,并在四个可用选项(Watch1,Watch2,Watch3 或 Watch4)中选择一个。然后,用鼠标高亮显示 sum 变量,将其拖拉到 Watch 窗口中。下图展示了一个例子,其中用大箭头指出了执行第 15 行后,sum 的当前值。

调试会话中使用watch窗口

定义 BYTE 和 SBYTE 数据

BYTE(定义字节)和 SBYTE(定义有符号字节)为一个或多个无符号或有符号数值分配存储空间。每个初始值在存储时,都必须是 8 位的。例如:

value1 BYTE  'A'    ;字符常量
value2 BYTE  0      ;最小无符号字节
value3 BYTE  255    ;最大无符号字节
value4 SBYTE -128   ;最小有符号字节
value5 SBYTE +127   ;最大有符号字节

问号(?)初始值使得变量未初始化,这意味着在运行时分配数值到该变量:

value6 BYTE ?

可选名字是一个标号,标识从变量包含段的开始到该变量的偏移量。比如,如果 value1  在数据段偏移量为 0000 处,并在内存中占一个字节,则 value2 就自动处于偏移量为 0001 处:

value1 BYTE 10h
value2 BYTE 20h

DB 伪指令也可以定义有符号或无符号的 8 位变量:

val1 DB 255    ;无符号字节
val2 DB -128  ;有符号字节

1) 多初始值

如果同一个数据定义中使用了多个初始值,那么它的标号只指出第一个初始值的偏移量。在下面的例子中,假设 list 的偏移量为 0000。那么,数值 10 的偏移量就为 0000, 20 的偏移量为 0001,30 的偏移量为 0002,40 的偏移量为 0003:

list BYTE 10,20,30,40

下图给出了字节序列 list,显示了每个字节及其偏移量。

序列的内存排列

并不是所有的数据定义都要用标号。比如,在 list 后面继续添加字节数组,就可以在下一行定义它们:

list BYTE 10,20,30,40
     BYTE 50,60,70,80
     BYTE 81,82,83,84

在单个数据定义中,其初始值可以使用不同的基数。字符和字符串常量也可以自由组合。在下面的例子中,list1 和 list2 有相同的内容:

list1 BYTE 10, 32, 41h, 00100010b
list2 BYTE 0Ah, 20h, 'A', 22h

2) 定义字符串

定义一个字符串,要用单引号或双引号将其括起来。最常见的字符串类型是用一个空字节(值为0)作为结束标记,称为以空字节结束的字符串,很多编程语言中都使用这种类型的字符串:

greeting1 BYTE "Good afternoon",0
greeting2 BYTE 'Good night',0

每个字符占一个字节的存储空间。对于字节数值必须用逗号分隔的规则而言,字符串是一个例外。如果没有这种例外,greeting1 就会被定义为:

greeting1 BYTE 'G', 'o', 'o', 'd'....etc.

这就显得很冗长。一个字符串可以分为多行,并且不用为每一行都添加标号:

greeting1 BYTE "Welcome to the Encryption Demo program "
          BYTE "created by Kip Irvine.",0dh, 0ah
          BYTE "If you wish to modify this program, please "
          BYTE "send me a copy.",0dh,0ah,0

十六进制代码 0Dh 和 0Ah 也被称为 CR/LF (回车换行符)或行结束字符。在编写标准输出时,它们将光标移动到当前行的下一行的左侧。

行连续字符(\)把两个源代码行连接成一条语句,它必须是一行的最后一个字符。下面的语句是等价的:

greeting1 BYTE "Welcome to the Encryption Demo program "

greeting1 \
BYTE "Welcome to the Encryption Demo program "

3) DUP 操作符

DUP 操作符使用一个整数表达式作为计数器,为多个数据项分配存储空间。在为字符串或数组分配存储空间时,这个操作符非常有用,它可以使用初始化或非初始化数据:

BYTE 20 DUP ( 0 )      ;20 个字节,值都为 0
BYTE 20 DUP ( ? )      ;20 个字节,非初始化
BYTE 4 DUP ( "STACK" ) ; 20 个字节:

定义 WORD 和 SWORD 数据

WORD(定义字)和 SWORD(定义有符号字)伪指令为一个或多个 16 位整数分配存储空间:

word1 WORD 65535    ;最大无符号数
word2 SWORD -32768  ;最小有符号数
word3 WORD ?        ;未初始化,无符号

也可以使用传统的 DW 伪指令:

val1 DW 65535   ;无符号
val2 DW -32768  ;有符号

16 位字数组通过列举元素或使用 DUP 操作符来创建字数组。下面的数组包含了一组数值:

myList WORD 1,2,3,4,5

下图是一个数组在内存中的示意图,假设 myList 起始位置偏移量为0000。由于每个数值占两个字节,因此其地址递增量为 2。

16位字数组的内存排列

DUP 操作符提供了一种方便的方法来声明数组:

array WORD 5 DUP (?) ; 5 个数值,未初始化

定义 DWORD 和 SDWORD 数据

DWORD(定义双字)和 SDWORD(定义有符号双字)伪指令为一个或多个 32 位整数分配存储空间:

val1 DWORD 12345678h    ;无符号
val2 SDWORD -2147483648 ;有符号
val3 DWORD 20 DUP (?)   ;无符号数组

传统的 DD 伪指令也可以用来定义双字数据:

val1 DD 12345678h ;无符号
val2 DD -2147483648 ;有符号

DWORD 还可以用于声明一种变量,这种变量包含的是另一个变量的 32 位偏移量。如下所示,pVal 包含的就是 val3 的偏移量:

pVal DWORD val3

32 位双字数组

现在定义一个双字数组,并显式初始化它的每 一个值:

myList DWORD 1,2,3,4,5

下图给岀了这个数组在内存中的示意图,假设 myList 起始位置偏移量为 0000,偏移量增量为 4。

32位双字数组的内存排列

定义 QWORD 数据

QWORD(定义四字)伪指令为 64 位(8 字节)数值分配存储空间:

quad1 QWORD 1234567812345678h

传统的 DQ 伪指令也可以用来定义四字数据:

quad1 DQ 1234567812345678h

定义压缩 BCD(TBYTE)数据

Intel 把一个压缩的二进制编码的十进制(BCD, Binary Coded Decimal)整数存放在一个 10 字节的包中。每个字节(除了最高字节之外)包含两个十进制数字。在低 9 个存储字节中,每半个字节都存放了一个十进制数字。最高字节中,最高位表示该数的符号位。如果最高字节为 80h,该数就是负数;如果最高字节为 00h,该数就是正数。整数的范围是 -999 999 999 999 999 999 到 +999 999 999 999 999 999。

示例下表列出了正、负十进制数 1234 的十六进制存储字节,排列顺序从最低有效字节到最高有效字节:

十进制数值 存储字节
+1234 34 12 00 00 00 00 00 00 00 00
-1234 34 12 00 00 00 00 00 00 00 80

MASM 使用 TBYTE 伪指令来定义压缩 BCD 变量。常数初始值必须是十六进制的,因为,汇编器不会自动将十进制初始值转换为 BCD 码。下面的两个例子展示了十进制 数 -1234 有效和无效的表达方式:

intVal TBYTE 800000000000001234h ;有效
intVal TBYTE -1234               ;无效

第二个例子无效的原因是 MASM 将常数编码为二进制整数,而不是压缩 BCD 整数。

如果想要把一个实数编码为压缩 BCD 码,可以先用 FLD 指令将该实数加载到浮点寄存器堆栈,再用 FBSTP 指令将其转换为压缩 BCD 码,该指令会把数值舍入到最接近的整数:

.data
posVal REAL8 1.5
bcdVal TBYTE ?

.code
fid posVal ;加载到浮点堆栈
fbstp bcdVal ;向上舍入到 2,压缩 BCD 码值

如果 posVal 等于 1.5,结果 BCD 值就是 2。

定义浮点类型

REAL4 定义 4 字节单精度浮点变量。REAL8 定义 8 字节双精度数值,REAL10 定义 10 字节扩展精度数值。每个伪指令都需要一个或多个实常数初始值:

rVal1 REAL4 -1.2
rVal2 REAL8 3.2E-260
rVal3 REAL10 4.6E+4096
ShortArray REAL4 20 DUP(0.0)

下表描述了标准实类型的最少有效数字个数和近似范围:

数据类型 有效数字 近似范围
短实数 6 1.18x 10-38 to 3.40 x 1038
长实数 15 2.23 x 10-308 to 1.79 x 10308
扩展精度实数 19 3.37 x 10-4932 to 1.18 x 104932

DD、DQ 和 DT 伪指令也可以定义实数:

rVal1 DD -1.2      ;短实数
rVal2 DQ 3.2E-260  ;长实数
rVal3 DT 4.6E+4096 ;扩展精度实数

MASM 汇编器包含了诸如 wal4 和 real8 的数据类型,这些类型表明数值是实数。更准确地说,这些数值是浮点数,其精度和范围都是有限的。从数学的角度来看,实数的精度和大小是无限的。

变量加法程序

到目前为止,本节的示例程序实现了存储在寄存器中的整数加法。现在已经对如何定义数据有了一些了解,那么可以对同样的程序进行修改,使之实现三个整数变量相加,并将和数存放到第四个变量中。

;AddTowSum.asm

.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD

.data
firstval DWORD 20002000h
secondval DWORD 11111111h
thirdval DWORD 22222222h
sum DWORD 0

.code
main PROC
    mov eax,firstval
    add eax,secondval
    add eax,thirdval
    mov sum,eax

    INVOKE ExitProcess,0
main ENDP
END main

注意,已经用非零数值对三个变量进行了初始化(9〜11 行)。16〜18 行进行变量相加。x86 指令集不允许将一个变量直接与另一个变量相加,但是允许一个变量与一个寄存器相加。这就是为什么 16〜17 行用 EAX 作累加器的原因:

mov eax,firstval
add eax,secondval

第 17 行之后,EAX 中包含了 firstval 和 secondval 之和。接着,第 18 行把 thirdval 加到 EAX 中的和数上:

add eax,thirdval

最后,在第 19 行,和数被复制到名称为 sum 的变量中:

mov sum,eax

作为练习,鼓励大家在调试会话中运行本程序,并在每条指令执行后检查每个寄存器。最终和数应为十六进制的 53335333。

在调试会话过程中,如果想要变量显示为十六进制,则按下述步骤操作:鼠标在变量或寄存器上悬停 1 秒,直到一个灰色矩形框出现在鼠标下。右键点击该矩形框,在弹出菜单中选择 Hexadecimal Display。

小端顺序

x86 处理器在内存中按小端(little-endian)顺序(低到高)存放和检索数据。最低有效字节存放在分配给该数据的第一个内存地址中,剩余字节存放在随后的连续内存位置中。考虑一个双字 12345678h。如果将其存放在偏移量为 0000 的位置,则 78h 存放在第一个字节,56h 存放在第二个字节,余下的字节存放地址偏移量为 0002 和 0003,如下图所示。

12345678h小端表示

其他有些计算机系统采用的是大端顺序(高到低)。 下图展示了 12345678h 从偏移量 0000 开始的大端顺序存放。

12345678h的大端表示

声明未初始化数据

.DATA ? 伪指令声明未初始化数据。当定义大量未初始化数据时,.DATA ? 伪指令减少了编译程序的大小。例如,下述代码是有效声明:

.data
smallArray DWORD 10 DUP (0) ;40 个字节
.data?
bigArray DWORD 5000 DUP ( ? ) ;20 000 个字节,未初始化

而另一方面,下述代码生成的编译程序将会多岀 20 000 个字节:

.data
smallArray DWORD 10 DUP ( 0 )  ; 40 个字节
bigArray DWORD 5000 DUP ( ? )  ; 20 000 个字节

代码与数据混合汇编器允许在程序中进行代码和数据的来回切换。比如,想要声明一个变量,使其只能在程序的局部区域中使用。下述示例在两个代码语句之间插入了一个名为 temp 的变量:

.code
mov eax,ebx
.data
temp DWORD ?
.code
mov temp,eax

尽管 temp 声明的出现打断了可执行指令流,MASM 还是会把 temp 放在数据段中,并与保持编译的代码段分隔开。然而同时,混用 .code 和 .data 伪指令会使得程序变得难以阅读。

匿名

发表评论

匿名网友