《汇编语言:基于X86处理器》第10章 结构和宏(1)

前面的章节讲的都是指令和逻辑,本章咱们介绍一个新的概念。在C、C++、Java等高级高阶语言中结构体和宏是很重要的编程模块,汇编语言也有结构体和宏的部分,接下来本章讲讲结构和宏。

10.1结构

结构(structure)是一组逻辑相关变量的模板或模式。结构中的变量被称为字段(fields)。程序语句可以把结构作为整体进行访问,也可以访问其中的单个字段。结构常常包含不同类型的字段。联合(union)也会把多个标识符组织在一起,但是这些标识符会在内存同一区域内相互重叠。联合将在10.1.7 节介绍。

结构提供了一种简便的方法来实现数据的聚集以及在过程之间的传递。假设一过程的输入参数包含了磁盘驱动的20个不同单位的数据,那么,调用这种过程很容易出错,因为程序员可能会搞混参数的顺序,或是搞错了参数的个数。相反则可以把所有的输人参数放到一个结构中,然后将这个结构的地址传递给过程。这样,使用的堆栈空间将最少(一个地址),而且被调用过程还可以修改结构的内容。

汇编语言中的结构与C和C++中的结构同样重要。只需要一点转换,就可以从 MS-Windows API 库中获得任何结构,并将其用于汇编语言。大多数调试器都能显示各个结构字段。

COORD 结构 Windows API 中定义的COORD 结构确定了屏幕的 X 和 Y 坐标。相对于结构起始地址,字段X的偏移量为0,字段Y的偏移量为2:

复制代码
COORD STRUCT
  X WORD ?			;偏移量00
  Y WORD ?			;偏移量02
COORD ENDS

使用结构包括三个连续的步骤:

1)定义结构。

2)声明结构类型的一个或多个变量,称为结构变量(structure variables)。

3)编写运行时指令访问结构字段。

10.1.1 定义结构

定义结构使用的是 STRUCT 和 ENDS 伪指令。在结构内,定义字段的语法与一般的变量定义是相同的。结构对其包含字段的数量几乎没有任何限制:

复制代码
name STRUCT
  field-declarations	;字段声明
name ENDS

字段初始值 若结构字段有初始值,那么在创建结构变量时就要进行赋值。字段初始值可以使用各种类型:

●无定义:运算符?使字段初始值为无定义。

●字符串文本:用引号括起的字符串。

●整数:整数常数和整数表达式。

●数组:DUP运算符可以初始化数组元素。

下面的Employee结构描述了雇员信息,其包含字段有 ID 号、姓氏、服务年限,以及薪酬历史信息数组。结构定义如下所示,定义必须在声明Employee变量之前:

复制代码
Employee STRUCT
  IdNum 	BYTE "000000000"
  LastName BYTE 30 DUP(0)
  Years WORD 0
  SalaryHistory DWORD 0,0,0,0
Employee ENDS

该结构内存保存形式的线性表示如下:

对齐结构字段

为了获得最好的内存I/O 性能,结构成员应按其数据类型进行地址对齐。否则,CPU 将会花更多时间访问成员。例如,一个双字成员应对齐到双字边界。表 10-1 列出了MicrosoftC 和 C++编译器,以及Win32 API 函数的对齐方式。汇编语言中的ALIGN伪指令会使其后的字段或变量按地址对齐;

ALIGN datatype

比如,下面的例子就把myVar 对齐到双字边界:

复制代码
.data 
ALIGN DWORD
myVar DWORD ?

现在正确地定义Employee结构,利用ALIGN将Years按字(WORD)边界对齐,SalaryHistory按双字(DWORD)边界对齐。注释为字段大小:

复制代码
Employee STRUCT
  IdNum			BYTE "000000000"	;9
  LastName	BYTE 30 DUP(0)			;30
  ALIGN 		WORD				;加1字节
  Years 		WORD 0				;2
  ALIGN			DWORD				;加2字节
  SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节

10.1.2 声明结构变量

结构变量可以被声明,并能选择为是否用特定值进行初始化。语法如下,其中structureType已经用STRUCT伪指令定义过了:

identifier structureType<initializer-list>

identifier 的命名规则与MASM 中其他变量的规则相同。initializer-list为可选项,但是如果选择使用,则该项就是一个用逗号分隔的汇编时常数列表,需要与特定结构字段的数据类型相匹配:

initializer [. initializer] ...

空括号<>使结构包含的是结构定义的默认字段值。此外,还可以在选定字段中插人新值。结构字段中的插入值顺序为从左到右,与结构声明中字段的顺序一致。这两种方法的示例如下,使用的结构是COORD 和 Employee:

复制代码
.data
point1 COORD <5, 10>				;X=5,Y=10
point1 COORD <20>					;X=20,Y=?
point1 COORD <>						;X=?,Y=?
worker Employee <>					;默认初始值

可以只覆盖选定字段的初始值。下面的声明只覆盖了 Employee 结构的 IdNum 字段,而其他字段仍为默认值:

person1 Employee<555223333">

还有一种形式是使用大括号{--}而不是尖括号:

person2 Employee("555223333")

若字符串字段初始值的长度少于字段的定义,则多出的位置用空格填充。空字节不会自动插到字符串字段的尾部。通过插人逗号作为位置标记可以跳过结构字段。例如,下面的语句就跳过了IdNum 字段,初始化了LastName 字段:

person3 Employee<, "dJones">

数组字段使用DUP运算符来初始化某些或全部数组元素。如果初始值比字段位数少,则多出的位置用零填充。下面的语句只初始化了前两个SalaryHistory的值,而其他的值则为0:

person4 Employee<, , , 2DUP(20000)>

结构数组 DUP运算符能够用于定义结构数组,如下所示,AllPoints中每个元素的X和Y字段都被初始化为0:

复制代码
NumPoints = 3
AllPoints COORD NumPoints DUP(<0, 0>)

对齐结构变量

为了最好的处理器性能,结构变量在内存中的位置要与其最大结构成员的边界对齐Employee 结构包含双字(DWORD)字段,因此,下面的定义使用了双字对齐:

复制代码
.data 
ALIGN DWORD
person Employee<>

10.1.3 引用结构变量

使用TYPE和SIZEOF运算符可以引用结构变量和结构名称。例如,现在回到之前的Employee 结构:

复制代码
Employee STRUCT
  IdNum			BYTE "000000000"	;9
  LastName	BYTE 30 DUP(0)			;30
  ALIGN 		WORD				;加1字节
  Years 		WORD 0				;2
  ALIGN			DWORD				;加2字节
  SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节

给定数据定义:

复制代码
.data
vorker Employee <>

则下列所有表达式返回的值都相同:

复制代码
TYPE Employee			;60
SIZEOF Employee			;60
SIZEOF worker			;60

TYPE 运算符(4.4节)返回的是标识符存储类型(BYTE、WORD、DWORD等)的字节数。LENGTHOF运算符返回的是数组元素的个数。SIZEOF运算特则为LENGTHOF与 TYPE的乘积。

完整代码测试笔记:

复制代码
;10.1.3.asm     10.1.3 引用结构变量
;使用TYPE和SIZEOF运算符可以引用结构变量和结构名称。 

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

Employee STRUCT
  IdNum		BYTE "000000000"		;9
  LastName	BYTE 30 DUP(0)			;30
  ALIGN 	WORD					;加1字节
  Years 	WORD 0					;2
  ALIGN		DWORD					;加2字节
  SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节

.data
worker Employee <>

.code 
main PROC
	;查看数据大小
	mov eax, TYPE Employee
	mov ebx, SIZEOF Employee
	mov ecx, SIZEOF worker
	INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

1.引用成员

引用已命名的结构成员时,需要用结构变量作为限定符。以Employee结构为例,在汇编时能生成下述常量表达式:

复制代码
TYPE Employee.SalaryHistory			;4
LENGTHOF Employee.SalaryHistory	    ;4
SIZEOF Employee.SalaryHistory		;16
TYPE Employee.Years					;2

以下为对worker(一个Employee)的运行时引用:

复制代码
.data
worker Employee<>
.code
mov dx, worker.Years
mov worker.SalaryHistory, 20000		;第一个工资
mov [worker.SalaryHistory+4], 30000	;第二个工资

使用OFFSET运算符 使用OFFSET运算符能获得结构变量中一个字段的地址:

mov edx, OFFSET worker.LastName

完整代码测试笔记

复制代码
;10.1.3_1.asm     10.1.3 引用结构变量
;1.引用成员 

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

Employee STRUCT
  IdNum		BYTE "000000000"		;9
  LastName	BYTE 30 DUP(0)			;30
  ALIGN 	WORD					;加1字节
  Years 	WORD 0					;2
  ALIGN		DWORD					;加2字节
  SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节

.data
worker Employee <>
worker2 Employee <>

.code 
main PROC
	;查看数据大小
	mov eax, TYPE Employee							;60
	mov eax, TYPE Employee.SalaryHistory			;4
	mov eax, LENGTHOF Employee.SalaryHistory		;4
	mov eax, SIZEOF Employee.SalaryHistory			;16
	mov eax, TYPE Employee.Years					;2
	mov dx, worker.Years
	mov worker.SalaryHistory, 20000h				;第一个工资
	mov [worker.SalaryHistory+4], 30000h			;第二个工资
	mov edx, OFFSET worker.LastName
	mov eax, OFFSET worker
	mov ebx, OFFSET worker2
	INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

2.间接和变址操作数

间接操作数用寄存器(如 ESI)对结构成员寻址。间接寻址具有灵活性,尤其是在向过程传递结构地址或者使用结构数组的情况下。引用间接操作数时需要PTR运算符:

复制代码
mov esi,offset worker
mov ax, (Employee PTR [esi]).Years

下面的语句不能汇编,原因是Years自身不能表明它所属的结构:

mov ax[esi].Years ;无效

变址操作数 用变址操作数可以访问结构数组。假设department是一个包含5个Employee 对象的数组。下述语句访问的是索引位置为1的雇员的 Years 字段:

复制代码
.data
department Employee 5 DUP(<>)
.code
mov esi, TYPE Employee					;索引=1
mov department[esi].Years, 4		

完整代码测试笔记

复制代码
;10.1.3_2.asm     10.1.3 引用结构变量
;2.间接和变址操作数

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

Employee STRUCT
  IdNum		BYTE "000000000"		;9
  LastName	BYTE 30 DUP(0)			;30
  ALIGN 	WORD					;加1字节
  Years 	WORD 0					;2
  ALIGN		DWORD					;加2字节
  SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节

.data
worker Employee <>
department Employee 5 DUP(<>)

.code 
main PROC
	;间接操作数用寄存器(如 ESI)对结构成员寻址
	mov worker.Years, 2025
	mov esi, OFFSET worker
	mov ax, (Employee PTR [esi]).Years
	;用变址操作数可以访问结构数组。
	mov esi, TYPE Employee					;索引=1
	mov department[esi].Years, 4	
	movzx eax, department[esi].Years
	INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

数组循环 带间接或变址寻址的循环可以用于处理结构数组。下面的程序(AllPoints.asm)为AllPoints数组分配坐标;

复制代码
;AllPoints.asm     10.1.3 引用结构变量
;数组循环  带间接或变址寻址的循环可以用于处理结构数组。
;下面的程序(AllPoints.asm)为AllPoints数组分配坐标;

;INCLUDE Irvine32.inc
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD

COORD STRUCT
  X WORD ?			;偏移量00
  Y WORD ?			;偏移量02
COORD ENDS

NumPoints = 3

.data
ALIGN WORD			;2字节对齐
AllPoints COORD NumPoints DUP(<0, 0>)

.code 
main PROC
	mov edi, 0						;数组索引
	mov ecx, NumPoints				;循环计数器
	mov ax, 1						;起始X,Y的值
L1:	mov (COORD PTR AllPoints[edi]).X, ax
	mov (COORD PTR AllPoints[edi]).Y, ax
	add edi, TYPE COORD
	inc ax 
	loop L1
	;exit								
	INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

3.对齐的结构成员的性能

之前已经断言,处理器访问正确对齐的结构成员时效率更高。那么,非对齐字段会对性能产生多大影响呢?现在使用本章介绍的Employee结构的两种不同版本,进行一个简单的测试。测试将对第一个版本进行重命名,以便两种版本能在同一个程序中使用:

复制代码
EmployeeBad STRUCT
	IdNum		BYTE "000000000"		;9
	LastName	BYTE 30 DUP(0)			;30
	Years		WORD 0					;2
	SalaryHistory DWORD 0,0,0,1			;16
EmployeeBad ENDS

Employee STRUCT
	IdNum		BYTE "000000000"		;9
	LastName	BYTE 30 DUP(0)			;30
	Years		WORD 0					;2
	ALIGN		DWORD					;+3
	SalaryHistory DWORD 0,0,0,2			;16
Employee ENDS

下面的代码首先获取系统时间,再执行循环以访问结构字段,最后计算执行花费的时间。变量emp可以声明为Employee对象或者EmployeeBad 对象:

完整代码测试笔记

复制代码
;Struct1.asm     10.1.3 引用结构变量
;3.对齐的结构成员的性能

INCLUDE Irvine32.inc
EmployeeBad STRUCT
	IdNum		BYTE "000000000"		;9
	LastName	BYTE 30 DUP(0)			;30
	Years		WORD 0					;2
	SalaryHistory DWORD 0,0,0,1			;16
EmployeeBad ENDS

Employee STRUCT
	IdNum		BYTE "000000000"		;9
	LastName	BYTE 30 DUP(0)			;30
	Years		WORD 0					;2
	ALIGN		DWORD					;+3
	SalaryHistory DWORD 0,0,0,2			;16
Employee ENDS

.data
ALIGN DWORD
startTime DWORD ?						;对齐startTime
emp Employee <>					
empBad EmployeeBad <>			

.code 
main PROC
	mov eax, TYPE Employee				;60个字节
	mov ebx, TYPE EmployeeBad			;57个字节
	mov edi, OFFSET emp.IdNum
	mov esi, OFFSET emp.SalaryHistory
	call GetMSeconds					;获取系统时间
	mov startTime, eax		
	mov ecx, 0FFFFFFFFh					;循环计数器
L1:	mov emp.Years, 5
	mov emp.SalaryHistory, 35000h
	loop L1
	call GetMSeconds					;获取开始时间
	sub eax, startTime
	call WriteDec						;显示执行花费的时间

	call Crlf

	call GetMSeconds					;获取系统时间
	mov startTime, eax		
	mov ecx, 0FFFFFFFFh					;循环计数器
L2:	mov empBad.Years, 5
	mov empBad.SalaryHistory, 35000h
	loop L2
	call GetMSeconds					;获取开始时间
	sub eax, startTime
	call WriteDec						;显示执行花费的时间
	call Crlf

	INVOKE ExitProcess, 0
main ENDP 
END main

运行结果:

Window10系统 Intel(R) Core(TM) i7-14700HX 的CPU测试

本书作者的测试:在这个简单的测试程序(Structl.asm)中,使用正确对齐的Employee结构的执行时间为6141 毫秒,而使用 EmployeeBad结构的执行时间为6203毫秒。两者相差不大(62毫秒),可能是因为处理器的内存cache将对齐问题最小化了。

10.1.4 示例:显示系统时间

MS-Windows提供了设置屏幕光标位置和获取系统时间的控制台函数。要使用这些函数,先为两个预先定义的结构--COORD和SYSTEMTIME--创建实例:

复制代码
COORD STRUCT
  X WORD ?
  Y WORD ?
COORD ENDS
SYSTEMTIME STRUCT
  wYear WORD ?
  wMonth WROD ?
  wDayOfWeek WORD ?
  wDay WORD ?
  wHour WORD ?
  wMinute WORD ?
  wSecond WORD ?
  wMilliseconds WORD ?
SYSTEMTIME ENDS

这两个结构都在 SmallWin.inc 中进行了定义,这个文件位于汇编器的INCLUDE 目录下,并且由 Irvine32.inc 引用。首先获取系统时间(调整本地时间),调用 MS-Windows 的GetLocalTime 函数,并向其传递SYSTEMTIME 结构的地址:

复制代码
.data
sysTime SYSTEMTIME <>
.code
INVOKE GetLocalTime, ADDR sysTime

接着,从 SYSTEMTIME 结构检索相应的数值:

复制代码
movzx eax, sysTime.wYear
call WriteDec

SmallWin.inc 文件位于本书的安装软件文件夹中,包含的结构定义和画数原型改编自针对C和C++程序员的Microsoft Windows头文件。它代表了一小部分可能被应用程序调用的函数。当Win32 程序产生屏幕输出时,它要调用MS-Windows GetStdHandle函数来检索标准控制台输出句柄(一个整数):

当Win32 程序产生屏幕输出时,它要调用MS-Windows GetStdHandle函数来检索标准控制台输出句柄(一个整数):

复制代码
.data
consoleHandle DWORD ?
.code
INVOKE GetStdHandle, STD_OUTPUT_HANDLE
mov consoleHandle, eax

(常数STD_OUTPUT_HANDLE 在SmallWin.inc 中定义。)

设置光标位置要调用 MS-Windows SetConsoleCursorPosition函数,并向其传递控制台输出句柄,以及包含 X、Y字符坐标的COORD结构变量:

复制代码
.data
XYPos COORD <10, 5>
.code
INVOKE SetConsoleCursorPosition, consoleHandle, XYPos

程序清单 下面的程序(ShowTime.asm)检索系统时间,并将其显示在指定的屏幕位置。该程序只在保护模式下运行:

复制代码
;ShowTime.asm     10.1.4 示例:显示系统时间
;下面的程序(ShowTime.asm)检索系统时间,并将其显示在指定的屏幕位置。
;该程序只在保护模式下运行:

INCLUDE Irvine32.inc

.data
sysTime SYSTEMTIME <>
XYPos COORD <10, 5>
consoleHandle DWORD ?
colorStr BYTE ":", 0

.code 
main PROC
	;获取 win32控制台的标准输出句柄。
	INVOKE GetStdHandle, STD_OUTPUT_HANDLE
	mov consoleHandle, eax
	;设置光标位置并获取系统时间。
	INVOKE SetConsoleCursorPosition, consoleHandle, XYPos
	INVOKE GetLocalTime, ADDR sysTime
	;显示系统时间(小时:分钟:秒)
	movzx eax, sysTime.wHour			;小时
	call WriteDec
	mov edx, OFFSET colorStr			;":"
	call WriteString
	movzx eax, sysTime.wMinute			;分钟
	call WriteDec
	call WriteString
	movzx eax, sysTime.wSecond			;秒
	call WriteDec
	call Crlf
	call WaitMsg						;"Press any key..."
	INVOKE ExitProcess, 0
main ENDP 
END main

SmallWin.inc(自动包含在Irvine32.inc中)中的上述程序采用如下定义:

复制代码
STD_OUTPUT_HANDLE EQU -11
SYSTEMTIME STRUCT ...
COORD STRUCT...
GetStdHandle PROTO,
  nStdHandle:DWORD
GetLocalTime PROTO,
  lpSystemTime:PTR SYSTEMTIME
SetConsoleCursorPosition PROTO,
  nStdHandle:DWORD,
  coords:COORD

下面是示例程序输出,执行时间为下午 12:16:

10.1.5 结构包含结构

结构还可以包含其他结构的实例。例如,Rectangle 可以用其左上角和右下角来定义,而它们都是COORD结构:

复制代码
Rectangle STRUCT
  UpperLeft COORD <>
  LowerRight COORD <>
Rectangle ENDS

Rectangle变量可以被声明为不覆盖或者覆盖单个 COORD 字段。各种表达形式如下所示;

复制代码
rect1 Rectangle <>
rect2 Rectangle {}
rect3 Rectangle {{10, 10}, {50,20}}
rect4 Rectangle < <10, 10>, <50,20> >

下面是对其一个结构字段的直接引用:

mov rect1.UpperLeft.x,10

也可以用间接操作数访问结构字段。下例用 ESI指向结构,并把 10 送入该结构左上角的 Y 坐标:

复制代码
mov esi, OFFSET rect1
mov (Retangle PTR[esi]).UpperLeft.Y, 10

OFFSET运算符能返回单个结构字段的指针,包括嵌套字段:

复制代码
mov edi, OFFSET rect2.LowerRight
mov (COORD PTR[edi]).X, 50
mov edi, OFFSET rect2.LowerRight.X
mov WORD PTR [edi], 50

完整代码测试笔记

复制代码
;10.1.5.asm     10.1.5 结构包含结构
;计算矩形的面积

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

COORD STRUCT
	X DWORD ?
	Y DWORD ?   
COORD ENDS

Rectangle STRUCT
  UpperLeft COORD <>
  LowerRight COORD <>
Rectangle ENDS

.data
rect Rectangle <<10h, 10h>, <50h, 30h>>

.code 
main PROC
	mov eax, rect.LowerRight.x
	mov ebx, rect.LowerRight.y
	sub eax, rect.UpperLeft.x		;计算长度
	sub ebx, rect.UpperLeft.y		;计算宽度
	mov edx, 0						;高位清零
	mul ebx							;结果:低32位存在eax中,高32位存在edx中
	INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

40h*20h=800h

10.1.6 示例:醉汉行走

现在来看一个使用结构的小程序将会有所帮助。下面完成一个"醉汉行走"练习,用程序模拟一个不太清醒的教授从计算机科学假期聚会回家的路线。利用随机数生成器,选择该教授每一步行走的方向。假设教授处于一个虚构的网格中心,其中的每个方格代表的是北、南、东、西方向上的一步。现在按照随机路径通过网格(图10-1)。

本程序将使用COORD结构追踪这个人行走路径上的每一步,它们被保存在一个COORD 对象数组中。

复制代码
WalkMax = 50
DrunkardWalk STRUCT
  path COORD WalkMax DUP(<0,0>)
  pathsUsed WORD 0
DrunkardWalk ENDS

Walkmax 是一个常数,决定在模拟中教授能够行走的总步数。pathsUsed 字段表示在程序循环结束后,一共行走了多少步。教授每走一步,其位置就被记录在 COORD 对象中,并插入 path 数组下一个可用的位置。程序将在屏幕上显示这些坐标。以下是完整的程序清单,需在32位模式下运行:

复制代码
;Walk.asm     10.1.3 引用结构变量
;醉汉行走程序。教授的起点坐标为(25,25),并在周围徘徊

INCLUDE Irvine32.inc
WalkMax = 50
StartX = 25
StartY = 25

DrunkardWalk STRUCT
	path COORD WalkMax DUP(<0, 0>)
	pathsUsed WORD 0
DrunkardWalk ENDS
DisplayPosition PROTO currX:WORD, currY:WORD

.data
aWalk DrunkardWalk <>

.code 
main PROC
	mov esi, OFFSET aWalk
	call TakeDrunkenWalk
	INVOKE ExitProcess, 0
main ENDP
;---------------------------------------------
;向随机方向行走(北、南、东、西)
;接收:ESI为Drunkardwalk结构的指针
;返回:结构初始化为随机数
;---------------------------------------------
TakeDrunkenWalk PROC
	LOCAL currX:WORD, currY:WORD
	pushad
	;用OFFSET运算符获取path--COORD对象数组--的地址,并将其复制到EDI
	mov edi, esi
	add edi, OFFSET DrunkardWalk.path
	mov ecx, WalkMax							;循环计数器
	mov currX, StartX							;当前X的位置
	mov currY, StartY							;当前¥的位置
Again:
	;把当前位置插入数组
	mov ax, currX
	mov (COORD PTR [edi]).X, ax
	mov ax, currY
	mov (COORD PTR [edi]).Y, ax
	INVOKE DisplayPosition, currX, currY
	mov eax, 4									;选择一个方向(0-3)
	call RandomRange
	.IF eax == 0								;北
		dec currY
	.ELSEIF eax == 1							;南
		inc currY
	.ELSEIF eax == 2							;西
		dec currX
	.ELSE										;东(EAX=3)
		inc currX
	.ENDIF										;指向下一个COORD
	add edi, TYPE COORD
	loop Again
Finish:
	mov (DrunkardWalk PTR [esi]).pathsUsed, WalkMax
	popad
	ret
TakeDrunkenWalk ENDP
;-----------------------------------------------
;显示当前x和Y的位置。
;------------------------------------------------
DisplayPosition PROC currX:WORD, currY:WORD
.data
commaStr BYTE ",",0
.code
	pushad
	movzx eax, currX							;当前X的位置
	call WriteDec
	mov edx, OFFSET commaStr					;","字符串
	call WriteString
	movzx eax, currY							;当前Y的位置
	call WriteDec
	call Crlf
	popad
	ret
DisplayPosition ENDP
END main

运行调试:

现在进一步查看TakeDrunkenWalk过程。过程接收指向DrunkardWalk结构的指针(ESI),利用OFFSET运算符计算path数组的偏移量,并将其复制到EDI:

复制代码
mov edi, esi 
add edi, OFFSET DrunardWalk.path

教授初始位置的X和Y值(StartX和StartY)都被设置为25,位于50x50虚拟网格的中点。循环计数器也进行了初始化:

复制代码
mov ecx, WalkMax		;循环计数器
mov currX, StartX		;当前x的位置
mov currY, StartY		;当前¥的位置

循环开始时,对path 数组的第一项进行初始化:

复制代码
Again:
  ;将当前位置插入数组。
  mov ax, currX
  mov (COORD PTR [edi]).X, ax
  mov ax, currY
  mov (COORD PTR [edi]).Y, ax

路径结束时,在pathsUsed字段插入一个计数值,表示总共走了多少步:

复制代码
Finish:
  mov (DrunkardWalk PTR [esi]).pathsUsed, WalkMax

在当前的程序中,pathsUsed 总是等于 WalkMax。不过,若在行走过程中发现障碍,如湖泊或建筑物,情况就会发生变化,循环将会在达到 WalkMax之前结束。

10.1.7 声明和使用联合

结构中的每个字段都有相对于结构第一个字节的偏移量,而联合(union)中所有的字段则都起始于同一个偏移量。一个联合的存储大小即为其最大字段的长度。如果不是结构的组成部分,那么需要用 UNION 和ENDS 伪指令来定义联合:

复制代码
unionname UNION
  union-fields
unionname ENDS

如果联合嵌套在结构内,其语法会有一点不同:

复制代码
structname STRUCT
  structure-fields
  UNION unionname
    union-fields
  ENDS
structname ENDS

除了其每个字段都只有一个初始值之外,联合字段声明的规则与结构的规则相同。例如,Integer 联合对同一个数据声明了3种不同的大小属性,并将所有的字段都初始化为0:

复制代码
Integer UNION
  D DWORD 0
  W WORD 0
  B BYTE 0
Integer ENDS

一致性 如果使用初始值,那么它们必须为相同的数值。假设 Integer 声明了 3 个不同的初始值:

复制代码
Integer UNION
  D DWORD 1
  W WORD 5
  B BYTE 8
Integer ENDS

同时还假设声明了一个Integer变量myInt使用默认初始值:

复制代码
.data
myInt Integer <>

结果发现,myInt.D、myInt.W和myInt.B都等于1。字段W和B中声明的初始值会被汇编器忽略。

结构包含联合 在结构声明中使用联合的名称,就可以使联合嵌套在这个结构中。方法如同下面在FileInfo结构中声明FilelD字段一样:

复制代码
FileInfo STRUCT
  FileID Integer <>
  FileName BYTE 64 DUP(?)
FileInfo ENDS

还可以直接在结构中定义联合,方法如同下面定义FilelD 字段一样:

复制代码
FileInfo STRUCT
  UNION FileID
    D DWORD ?
    W WORD ?
    B BYTE ?
  ENDS
  FileName BYTE 64 DUP(?)
FileInfo ENDS

声明和使用联合变量联合变量的声明和初始化方法与结构变量相同,只除了一个重要的差异:不允许初始值多于一个。下面是Integer 类型变量的例子:

复制代码
val1 Integer<12345678h>
val2 Integer<100h>
val3 Integer<>

在可执行指令中使用联合变量时,必须给出字段的一个名称。下面的例子把寄存器的值赋给了Integer联合字段。注意其可以使用不同操作数大小的灵活性

复制代码
mov va13.B,al
mov va13.w, ax
mov va13.D, eax

完整代码测试笔记

复制代码
;10.1.7.asm      10.1.7 声明和使用联合

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

Integer UNION
  D DWORD 0
  W WORD 0
  B BYTE 0
Integer ENDS

FileInfo STRUCT
  FileID Integer <>
  FileName BYTE 64 DUP(?)
FileInfo ENDS

.data
myInt Integer <>
val1 Integer<12345678h>
val2 Integer<100h>
val3 Integer<>
myFileInfo FileInfo <>
fName BYTE "Assembly language: based on X86 processor.pdf",0

.code 
main PROC
	mov esi, OFFSET myInt
	mov myInt.D, eax
	mov ebx, TYPE myInt
	mov ebx, 0
	mov bl, myInt.B
	mov bx, myInt.W
	mov ebx, myInt.D

	mov val3.B, al
	mov val3.W, ax
	mov val3.D, eax							
	mov myFileInfo.FileID.D, 10203040h		;给ID赋值
	;给文件名赋值
	cld										;清除方向标志位
	mov esi, OFFSET fName					;ESI指向源串
	mov edi, OFFSET myFileInfo.FileName		;EDI执行目的串
	mov ecx, LENGTHOF fName					;计数器
	rep movsb								;文件名赋值
		
	INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

赋值后:

联合还可以包含结构。有些MS-Windows控制台输入函数会使用如下INPUTRECORD结构,它包含了一个名为Event的联合。这个联合对几个预定义的结构类型进行选择。EventType字段表示联合中出现的是哪种record。每一种结构都有不同的布局和大小,但是一次只能使用一种:

复制代码
INPUT_RECORD STRUCT
  EventType WORD ?
  ALIGN DWORD
  UNION Event
    KEY_EVENT_RECORD <>
    MOUSE_EVENT_RECORD <>
    WINDOW_BUFFER_SIZE_RECORD <>
    MENU_EVENT_RECORD <>
    FOCUS_EVENT_RECORD <>
  ENDS
INPUT_RECORD ENDS

Win32 API 在命名结构时,常常使用单词RECORD。KEY_EVENT_RECORD 结构的定义如下所示:

复制代码
KEY_EVENT_RECORD STRUCT
  bKeyDown					DWORD ?
  wRepeatCount			WORD ?
  wVirtualKeyCode		WORD ?
  wVirtualScanCode	WORD ?
  UNION uChar
    UnicodeChar			WORD ?
    AsciiChar				BYTE ?
  ENDS
  dwControlKeyState DWORD ?
KEY_EVENT_RECORD ENDS

SmallWin.inc文件中可以找到INPUT_RECORD 其余的 STRUCT 定义。

10.1.8 本节回顾

问题 1-9 参考如下结构:

复制代码
MyStruct STRUCT
  field1 WORD ?
  field2 DWORD 20 DUP(?)
MyStruct ENDS

1.用默认值声明变量MyStruct。

答:temp MyStruct <>

2.声明变量 MyStruct,将第一个字段初始化为0。

答:temp MyStruct <0>

3.声明变量 MyStruct,将第二个字段初始化为全零数组。

答:temp MyStruct <, 20 DUP(0)>

4.一数组包含 20个 MyStruct 对象,将该数组声明为变量。

答:array MyStruct 20 DUP(<>)

5.对上一题的MyStruct 数组,把第一个数组元素的field1送人AX。

答:mov ax, array.field1

6.对上一题的MyStruct数组,用ESI索引第3个数组元素,并将AX送入field1。提示:使用 PTR 运算符。

答:

复制代码
mov esi, OFFSET array
add esi, 3*(TYPE MyStruct)
mov (MyStruct PTR[esi]).field1, ax

7.表达式TYPE MyStruct 的返回值是多少?

答:2+20*4 = 82

8.表达式SIZEOF MyStruct的返回值是多少?

答:82

9.编写一个表达式,返回 MyStruct 中 field2 的字节数。

答:TYPE MyStruct.field2(或:SIZEOF MyStruct.field2)