当前位置: 首页 > news >正文

《汇编语言:基于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 STRUCTX WORD ?			;偏移量00Y WORD ?			;偏移量02
COORD ENDS

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

1)定义结构。

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

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

10.1.1 定义结构

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

name STRUCTfield-declarations	;字段声明
name ENDS

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

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

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

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

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

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

Employee STRUCTIdNum 	BYTE "000000000"LastName BYTE 30 DUP(0)Years WORD 0SalaryHistory 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 STRUCTIdNum			BYTE "000000000"	;9LastName	BYTE 30 DUP(0)			;30ALIGN 		WORD				;加1字节Years 		WORD 0				;2ALIGN			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 STRUCTIdNum			BYTE "000000000"	;9LastName	BYTE 30 DUP(0)			;30ALIGN 		WORD				;加1字节Years 		WORD 0				;2ALIGN			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:DWORDEmployee STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30ALIGN 	WORD					;加1字节Years 	WORD 0					;2ALIGN		DWORD					;加2字节SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节.data
worker Employee <>.code 
main PROC;查看数据大小mov eax, TYPE Employeemov ebx, SIZEOF Employeemov ecx, SIZEOF workerINVOKE 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:DWORDEmployee STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30ALIGN 	WORD					;加1字节Years 	WORD 0					;2ALIGN		DWORD					;加2字节SalaryHistory DWORD 0,0,0,0		;16
Employee ENDS						;共60字节.data
worker Employee <>
worker2 Employee <>.code 
main PROC;查看数据大小mov eax, TYPE Employee							;60mov eax, TYPE Employee.SalaryHistory			;4mov eax, LENGTHOF Employee.SalaryHistory		;4mov eax, SIZEOF Employee.SalaryHistory			;16mov eax, TYPE Employee.Years					;2mov dx, worker.Yearsmov worker.SalaryHistory, 20000h				;第一个工资mov [worker.SalaryHistory+4], 30000h			;第二个工资mov edx, OFFSET worker.LastNamemov eax, OFFSET workermov ebx, OFFSET worker2INVOKE 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:DWORDEmployee STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30ALIGN 	WORD					;加1字节Years 	WORD 0					;2ALIGN		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, 2025mov esi, OFFSET workermov ax, (Employee PTR [esi]).Years;用变址操作数可以访问结构数组。mov esi, TYPE Employee					;索引=1mov department[esi].Years, 4	movzx eax, department[esi].YearsINVOKE 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:DWORDCOORD STRUCTX WORD ?			;偏移量00Y WORD ?			;偏移量02
COORD ENDSNumPoints = 3.data
ALIGN WORD			;2字节对齐
AllPoints COORD NumPoints DUP(<0, 0>).code 
main PROCmov edi, 0						;数组索引mov ecx, NumPoints				;循环计数器mov ax, 1						;起始X,Y的值
L1:	mov (COORD PTR AllPoints[edi]).X, axmov (COORD PTR AllPoints[edi]).Y, axadd edi, TYPE COORDinc ax loop L1;exit								INVOKE ExitProcess, 0
main ENDP 
END main

运行调试:

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

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

EmployeeBad STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30Years		WORD 0					;2SalaryHistory DWORD 0,0,0,1			;16
EmployeeBad ENDSEmployee STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30Years		WORD 0					;2ALIGN		DWORD					;+3SalaryHistory DWORD 0,0,0,2			;16
Employee ENDS

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

完整代码测试笔记

;Struct1.asm     10.1.3 引用结构变量
;3.对齐的结构成员的性能INCLUDE Irvine32.inc
EmployeeBad STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30Years		WORD 0					;2SalaryHistory DWORD 0,0,0,1			;16
EmployeeBad ENDSEmployee STRUCTIdNum		BYTE "000000000"		;9LastName	BYTE 30 DUP(0)			;30Years		WORD 0					;2ALIGN		DWORD					;+3SalaryHistory DWORD 0,0,0,2			;16
Employee ENDS.data
ALIGN DWORD
startTime DWORD ?						;对齐startTime
emp Employee <>					
empBad EmployeeBad <>			.code 
main PROCmov eax, TYPE Employee				;60个字节mov ebx, TYPE EmployeeBad			;57个字节mov edi, OFFSET emp.IdNummov esi, OFFSET emp.SalaryHistorycall GetMSeconds					;获取系统时间mov startTime, eax		mov ecx, 0FFFFFFFFh					;循环计数器
L1:	mov emp.Years, 5mov emp.SalaryHistory, 35000hloop L1call GetMSeconds					;获取开始时间sub eax, startTimecall WriteDec						;显示执行花费的时间call Crlfcall GetMSeconds					;获取系统时间mov startTime, eax		mov ecx, 0FFFFFFFFh					;循环计数器
L2:	mov empBad.Years, 5mov empBad.SalaryHistory, 35000hloop L2call GetMSeconds					;获取开始时间sub eax, startTimecall WriteDec						;显示执行花费的时间call CrlfINVOKE 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 STRUCTX WORD ?Y WORD ?
COORD ENDS
SYSTEMTIME STRUCTwYear 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_HANDLEmov consoleHandle, eax;设置光标位置并获取系统时间。INVOKE SetConsoleCursorPosition, consoleHandle, XYPosINVOKE GetLocalTime, ADDR sysTime;显示系统时间(小时:分钟:秒)movzx eax, sysTime.wHour			;小时call WriteDecmov edx, OFFSET colorStr			;":"call WriteStringmovzx eax, sysTime.wMinute			;分钟call WriteDeccall WriteStringmovzx eax, sysTime.wSecond			;秒call WriteDeccall Crlfcall 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 STRUCTUpperLeft 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:DWORDCOORD STRUCTX DWORD ?Y DWORD ?   
COORD ENDSRectangle STRUCTUpperLeft COORD <>LowerRight COORD <>
Rectangle ENDS.data
rect Rectangle <<10h, 10h>, <50h, 30h>>.code 
main PROCmov eax, rect.LowerRight.xmov ebx, rect.LowerRight.ysub 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 STRUCTpath 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 = 25DrunkardWalk STRUCTpath COORD WalkMax DUP(<0, 0>)pathsUsed WORD 0
DrunkardWalk ENDS
DisplayPosition PROTO currX:WORD, currY:WORD.data
aWalk DrunkardWalk <>.code 
main PROCmov esi, OFFSET aWalkcall TakeDrunkenWalkINVOKE ExitProcess, 0
main ENDP
;---------------------------------------------
;向随机方向行走(北、南、东、西)
;接收:ESI为Drunkardwalk结构的指针
;返回:结构初始化为随机数
;---------------------------------------------
TakeDrunkenWalk PROCLOCAL currX:WORD, currY:WORDpushad;用OFFSET运算符获取path--COORD对象数组--的地址,并将其复制到EDImov edi, esiadd edi, OFFSET DrunkardWalk.pathmov ecx, WalkMax							;循环计数器mov currX, StartX							;当前X的位置mov currY, StartY							;当前¥的位置
Again:;把当前位置插入数组mov ax, currXmov (COORD PTR [edi]).X, axmov ax, currYmov (COORD PTR [edi]).Y, axINVOKE DisplayPosition, currX, currYmov 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										;指向下一个COORDadd edi, TYPE COORDloop Again
Finish:mov (DrunkardWalk PTR [esi]).pathsUsed, WalkMaxpopadret
TakeDrunkenWalk ENDP
;-----------------------------------------------
;显示当前x和Y的位置。
;------------------------------------------------
DisplayPosition PROC currX:WORD, currY:WORD
.data
commaStr BYTE ",",0
.codepushadmovzx eax, currX							;当前X的位置call WriteDecmov edx, OFFSET commaStr					;“,”字符串call WriteStringmovzx eax, currY							;当前Y的位置call WriteDeccall Crlfpopadret
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, currXmov (COORD PTR [edi]).X, axmov ax, currYmov (COORD PTR [edi]).Y, ax

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

Finish:mov (DrunkardWalk PTR [esi]).pathsUsed, WalkMax

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

10.1.7 声明和使用联合

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

unionname UNIONunion-fields
unionname ENDS

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

structname STRUCTstructure-fieldsUNION unionnameunion-fieldsENDS
structname ENDS

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

Integer UNIOND DWORD 0W WORD 0B BYTE 0
Integer ENDS

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

Integer UNIOND DWORD 1W WORD 5B BYTE 8
Integer ENDS

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

.data
myInt Integer <>

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

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

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

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

FileInfo STRUCTUNION FileIDD DWORD ?W WORD ?B BYTE ?ENDSFileName 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:DWORDInteger UNIOND DWORD 0W WORD 0B BYTE 0
Integer ENDSFileInfo STRUCTFileID 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 PROCmov esi, OFFSET myIntmov myInt.D, eaxmov ebx, TYPE myIntmov ebx, 0mov bl, myInt.Bmov bx, myInt.Wmov ebx, myInt.Dmov val3.B, almov val3.W, axmov 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 STRUCTEventType WORD ?ALIGN DWORDUNION EventKEY_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 STRUCTbKeyDown					DWORD ?wRepeatCount			WORD ?wVirtualKeyCode		WORD ?wVirtualScanCode	WORD ?UNION uCharUnicodeChar			WORD ?AsciiChar				BYTE ?ENDSdwControlKeyState DWORD ?
KEY_EVENT_RECORD ENDS

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

10.1.8 本节回顾

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

MyStruct STRUCTfield1 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)

http://www.lryc.cn/news/601774.html

相关文章:

  • 数据库连接操作详解:左连接、右连接、全连接与内连接
  • LeetCode 239:滑动窗口最大值
  • LeetCode第350题_两个数组的交集II
  • NVMe高速传输之摆脱XDMA设计17:队列管理控制设计(下)
  • 金字塔降低采样
  • 企业IT管理——突发病毒事件应急处理预案模板
  • 【Python系列】使用 memory_profiler 诊断 Flask 应用内存问题
  • 【NLP实践】三、LLM搭建中文知识库:提供RestfulAPI服务
  • 《计算机组成原理与汇编语言程序设计》实验报告四 Debug及指令测试
  • 基于黑马教程——微服务架构解析(一)
  • C/C++核心知识点详解
  • lombok插件@NoArgsConstructor、@AllArgsConstructor、@RequiredArgsConstructor的区别
  • 金融科技中的跨境支付、Open API、数字产品服务开发、变革管理
  • 2025C卷 - 华为OD机试七日集训第1期 - 按算法分类,由易到难,循序渐进,玩转OD
  • SpringSecurity实战:核心配置技巧
  • 由于主库切换归档路径导致的 Oracle DG 无法同步问题的解决过程
  • Python堆栈实现:从基础到高并发系统的核心技术
  • 模拟实现python的sklearn库中的Bunch类以及 load_iris 功能
  • 20250727让飞凌OK3576-C开发板在Rockchip的原厂Android14下通过耳机播音
  • 两个函数的卷积
  • Node.js特训专栏-配置与环境部署:20.PM2进程守护与负载均衡
  • 以使命为帆,结业是重新出发的号角
  • 电科金仓 KingbaseES 深度解码:技术突破・行业实践・沙龙邀约 -- 融合数据库的变革之力
  • 从0开始学linux韦东山教程Linux驱动入门实验班(6)
  • c# everthing.exe 通信
  • Android基础(一) 运行HelloWorld
  • 【java】 IntelliJ IDEA高效编程设置指南
  • 大模型算法面试笔记——常用优化器SGD,Momentum,Adagrad,RMSProp,Adam
  • Java 代理机制详解:从静态代理到动态代理,彻底掌握代理模式的原理与实战
  • 雪花算法原理深度解析