80386汇编-结构和宏
9unk Lv5

简介

结构(srtucture)是逻辑上相互联的一组变量的模板,结构中的单个变量称为域(field),程序的语句可以把结构作为一个实体进行访问,也可以对结构中的单个域进行访问。结构通常包含不同类型的域。联合(union)同样是把多个标识符组合在一起,不过这些标识符公用同一块内存区域。
结构为归集数据以及在过程间进行传递提供方便。假设一个过程的输入参数是由 20 个与磁盘驱动器相关的数据构成的,调用过程时要想正确地传递所需参数是非常困难的,相反,应该把所有的相关数据存放在一个数据结构中,然后向过程传递结构的地址,这只需使用极少的堆栈空间(一个地址),被调用过程同时还能够修改结构的内容。
1

定义结构

结构使用 STRUCT 和ENDS 伪指令定义。在结构内部,使用与定义普通变量一样的格式来定义域。基本格式如下:

1
2
3
名字 STRUCT
域的声明
名字 ENDS

域的初始化

如果结构的域有初始值,在定义结构变量时这些初始值就成了结构变量域的默认值。结构中可以使用多种类型的初始值:

  • 未定义:使用 “?” 表示域的额内容未定义
  • 字符串:用引号包围的字符串
  • 整数:整数常量或整数表达式
  • 数组:当域是一个数组时,可使用DUP操作符初始化数组
    2

结构中域的初始化

为取得最佳的I/O性能,结构的成员根据其数据类型进行对齐,否则CPU在访问结构的成员时就要花费更多的时间。例如,双字成员应该对齐在双字边界上。下表列出了Microsoft C/C++ 编译器对不同的数据类型对齐方式是的处理。在汇编语言中,ALIGN 伪指令设置下一个域或变量的地址对齐方式:

1
ALIGN datatype

在下列中,myVar 对齐在双字地址边界上:

1
2
3
.data
ALIGN DWORD
myVar DWORD ?

3

下面定义 Employee 结构:使用 ALIGN 伪指令使得成员 Years 对齐在字边界上、成员 SalaryHistory 对齐在双字边界上,各个域的大小在注释中给出:

1
2
3
4
5
6
7
8
Employee STRUCT
Idump BYTE "000000000"
Lastname BYTE 30 DUP(0)
ALIGN WORD
Years WORD 0
ALIGN DWORD
SalaryHistory DWORD 0,0,0,0
Employee ENDS

声明结构变量

4
5
6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
INCLUDE Irvine32.inc
COORD STRUCT
X WORD ?
Y WORD ?
COORD ENDS

Employee STRUCT
Idump BYTE "000000000"
Lastname BYTE 30 DUP(0)
ALIGN WORD
Years WORD 0
ALIGN DWORD
SalaryHistory DWORD 0,0,0,0
Employee ENDS

.data
point1 COORD <5,10>
point2 COORD <20>
point3 COORD <>
worker Employee <>
;初始化,域 Idump
person1 Employee <"555223333">
;初始化,域 Idump,且多余的位以 20h 填充
person2 Employee {"23333"}
;使用逗号跳过,域 Idump,初始化 Lastname 域
person3 Employee <,"dJones">
;初始化,域 SalaryHistory,且其余值用 0 填充
person4 Employee <,,,2 DUP(20000)>
;AllPoints 的每个元素的域 X 和 域 Y都被初始化成 1
NumPoints = 3
AllPoints COORD NumPoints DUP(<1,1>)

.code
main PROC
mov esi,OFFSET person1
mov eax,DWORD PTR person2.Idump
mov esi,OFFSET person3.Lastname
mov esi,OFFSET person4.SalaryHistory
mov esi,OFFSET AllPoints
exit
main ENDP

引用结构变量

7
8
9

循环遍历数组: 可以使用循环及间接寻址、变址寻址方式呢来操作结构数组。下面的程序(AllPoints.asm)为 AllPoints 数组赋坐标值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;---------------------------
;程序名:AllPoints.asm
;功能:演示数据结构,循环遍历数组
;作者:9unk
;编写时间:2023-4-2
;----------------------------
INCLUDE Irvine32.inc
NumPoints = 3

.data
ALIGN WORD
AllPoints COORD NumPoints DUP(<0,0>)

.code
main PROC
;mov edi,0
mov ecx,NumPoints
mov ax,1
L1:
mov AllPoints.X,ax
;mov (COORD PTR AllPoints[edi]).X,ax
mov AllPoints.Y,ax
;mov (COORD PTR AllPoints[edi]).Y,ax
add edi,TYPE COORD
inc ax
loop L1

exit
main ENDP
END main

结构成员对齐与否对性能的影响
下面使用两个版本的 Employee 结构进行简单的测试,研究对齐结构对于未对齐结构的时效性影响有多大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
;---------------------------
;程序名:StrAli.asm
;功能:演示结构体对齐
;作者:9unk
;编写时间:2023-4-2
;----------------------------
INCLUDE Irvine32.inc
EmployeeBad STRUCT
Idump BYTE "000000000"
Lastname BYTE 30 DUP(0)
Years WORD 0
SalaryHistory DWORD 0,0,0,0
EmployeeBad ENDS

Employee STRUCT
Idump BYTE "000000000"
Lastname BYTE 30 DUP(0)
ALIGN WORD
Years WORD 0
ALIGN DWORD
SalaryHistory DWORD 0,0,0,0
Employee ENDS

.data
ALIGN DWORD
startTime DWORD ? ;对齐 startTime
;emp Employee <> ;使用对齐
emp EmployeeBad <> ;未对齐

.code
main PROC
call GetMSeconds
mov startTime,eax

mov ecx,0FFFFFFFFh
L1:
mov emp.Years,5
mov emp.SalaryHistory,35000
loop L1

call GetMSeconds
sub eax,startTime
call WriteDec
exit
main ENDP
END main

10

在我的笔记本(i7 12代的处理器)中,未对齐的结构体访问起来更花时间。

例子:显示系统时间

11
12

程序清单:下面的程序(ShowTime.asm)获取当前的系统时间并在指定的屏幕位置上显示,该程序只能在保护模式下运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;---------------------------
;程序名:ShowTime.asm
;功能:获取当前的系统时间并在指定的屏幕位置上显示,该程序只能在保护模式下运行。
;作者:9unk
;编写时间:2023-4-3
;----------------------------
INCLUDE Irvine32.inc

.data
sysTime SYSTEMTIME <>
XYPos COORD <10,5>
consoleHandle DWORD ?
colonStr 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 colonStr ;":"
call WriteString
movzx eax,sysTime.wMinute ;分钟
call WriteDec
call WriteString
movzx eax,sysTime.wSecond ;秒
call WriteDec
call Crlf
call WaitMsg
exit
main ENDP
END main

13

14
15

结构的嵌套

16

醉汉走路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
;---------------------------
;程序名:Walk.asm
;功能:醉汉走路
;作者:9unk
;编写时间:2023-4-3
;----------------------------
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
exit
main ENDP

;--------------------------------
TakeDrunKenWalk PROC
LOCAL currX:WORD,currY:WORD
;功能:随机生成走的路线
;入口参数:esi = DrunkardWalk 结构体指针
;返回值:该结构被初始化的随机值
;--------------------------------
pushad
mov edi,esi
add edi,OFFSET DrunkardWalk.path
mov ecx,WalkMax
mov currX,StartX
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
call RandomRange
.IF eax == 0
dec currY
.ELSEIF eax == 1
inc currY
.ELSEIF eax == 2
dec currX
.ELSE
inc currX
.ENDIF

add edi,TYPE COORD
loop Again

Finish:
mov (DrunkardWalk PTR [esi]).pathsUsed,WalkMax
popad
ret
TakeDrunKenWalk ENDP

;---------------------------------------
DisplayPosition PROC,
currX:WORD,
currY:WORD
;功能:显示当前坐标 X 和 Y
;---------------------------------------
.data
commaStr BYTE ",",0
.code
pushad
movzx eax,currX
call WriteDec
mov edx,OFFSET commaStr
call WriteString
movzx eax,currY
call WriteDec
call Crlf
popad
ret
DisplayPosition ENDP
END main

联合的声明和使用

17
18
19
20

宏过程(macro procedure)是一个命名的汇编语句块。一旦定义之后,宏过程就可以在程序中被调用任意多次。
声明的位置: 宏可以直接在程序的头部定义,或者也可以放在单独的文本文件中,通过 INCLUDE 伪指令把宏定义复制(插入)到源程序中。宏是在汇编器的预处理阶段展开的。在预处理阶段,预处理器读取宏的定义并扫描程序中其余的代码,在调用宏的地方插入宏代码的一份副本。汇编器在试图汇编任何调用宏的而语句前,必须首先找到宏的定义。如果程序定义了宏但没有调用,那么编译后的程序内不会包含宏的代码。

21

宏的定义

22
23

宏的调用

24
25

宏的其他特性

26
27
28
29
30
31

本书附带的宏库

32

这些宏的具体使用,请看书中相关章节。

例子程序

下面的程序(Wraps.asm)演示书中封装的宏,本书中的宏基本在 Macros.inc 文件中定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
;---------------------------
;程序名:Wraps.asm
;功能:演示调用宏
;作者:9unk
;编写时间:2023-4-3
;----------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

.data
array DWORD 1,2,3,4,5,6,7,8
firstName BYTE 31 DUP(?)
lastName BYTE 31 DUP(?)

.code
main PROC
;mGotoxy 0,0
call Crlf ;将光标定位到下一行的开始位置
mWrite <"Sample Macro Program",0Dh,0Ah>
;输入用户名
mGotoxy 0,5
mWrite "Please enter your first name: "
mReadString firstName
call Crlf

mWrite "Please enter your last name: "
mReadString lastName
call Crlf

;显示用户名
mWrite "Your name is "
mWriteString firstName
mWriteSpace
mWriteString lastName
call Crlf

;显示数组中的整数
mDumpMem OFFSET array,LENGTHOF array,TYPE array
exit
main ENDP
END main

33

条件汇编伪指令

条件汇编伪指令和宏联合使用可以使得宏更加灵活,条件汇编伪指令的一半格式是:

1
2
3
4
IF condition
statements
[ELSE
statements]

本章介绍的常量条件伪指令和前面介绍的运行时伪指令(.IF和.ENDIF)是不一样的。运行时伪指令是基于存储在寄存器或变量中的运行时值对表达式的求值。

下表列出常用的条件汇编伪指令,当说明中提到一条伪指令 “允许汇编” 的时候,就意味着其后直到下一个相邻的 ELSE 或 ENDIF 伪指令之间的所有语句都会被编译,这里必须强调的是,表中列出的伪指令是在编译时求值的。
34
35

检查缺少的参数

宏可以检查其任何一个参数是否为空。通常如果一个宏接收了空参数,预处理器展开宏的时候就会导致生成无效指令。例如,如果调用宏 mWriteString 的时候不传递参数,在宏展开的时候,把字符串偏移送 EDX 的指令就成了无效指令。如下图所示,编译产生了错误信息:
36

为防止缺少参数产生的错误,可使用 IFB(if blank)伪指令,该伪指令在宏参数为空时返回真;也可以使用 IFNB(if not blank)伪指令,它在宏参数非空时返回真。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;---------------------------
;程序名:Macro2.asm
;功能:演示 IFB 伪指令,忽略宏参数错误
;作者:9unk
;编写时间:2023-4-7
;----------------------------
INCLUDE Irvine32.inc

mWriteString MACRO string
IFB <string>
ECHO --------------------------------------------
ECHO * Error: parameter missing in mWriteString
ECHO * (no code generated)
ECHO --------------------------------------------
EXITM
ENDIF
push edx
mov edx,OFFSET string
call WriteString
pop edx
ENDM


.code
main PROC
mWriteString
main ENDP
END main

37

EXITM 伪指令告诉预处理器退出宏,不要再展开宏中其后的语句。

默认的参数初始化值

38

布尔表达式

39

IF,ELSE和ENDIF伪指令

40

例子:mGotoxyConst 宏。mGotoxyConst 宏使用 LT 和 GT 操作符对传递给宏的参数进行范围检查,参数 X 和 Y 必须是常量。另一个符号常量 ERRS 用于统计发现的错误次数,检查 ERRS 可能被设置为 1;检查 Y 参数时,可能会再对 ERRS 加 1,最后 ERRS 的值大于 0,那么使用 EXITM 伪指令来退出宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
;-------------------------------------------------
;程序名:Macro3.asm
;功能:演示 IF,ELSE和ENDIF伪指令如何使用后布尔表达式
;作者:9unk
;编写时间:2023-4-7
;-------------------------------------------------
INCLUDE Irvine32.inc

;------------------------------------------------
;REQ 修饰宏参数,标志这个参数是必须的
mGotoxyConst MACRO X:REQ, Y:REQ
; X为列,Y为行,将光标定位在(X,Y)上
; X和Y必须是常量
; 范围:0 <= X <80,0 <= Y < 24
;------------------------------------------------
LOCAL ERRS
ERRS = 0
IF (X LT 0) OR (X GT 79)
ECHO Warning: First argument to mGotoxy (X) is out of range.
ECHO ******************************************************
ERRS = 1
ENDIF
IF (Y LT 0) OR (Y GT 24)
ECHO Warning: First argument to mGotoxy (Y) is out of range.
ECHO ******************************************************
ERRS = ERRS + 1
ENDIF
IF ERRS GT 0
EXITM
ENDIF
push edx
mov dh,Y
mov dl,X
call Gotoxy
pop edx
ENDM

.code
main PROC
mGotoxyConst -1,5
main ENDP
END main

41

IFIDN 和 IFIDNI 伪指令

42

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
;---------------------------
;程序名:mReadBuf.asm
;功能:演示 IFIDNI 伪指令的使用
;作者:9unk
;编写时间:2023-4-8
;----------------------------
INCLUDE Irvine32.inc
.data
buffer DB 20 DUP(?)

;--------------------------------------------------------
mReadBuf MACRO bufferPtr,maxChars
;
;功能:读取键盘输入,并存储到缓冲区 buffer 中
;入口参数:buffer缓冲区的偏移地址,键盘输入字符数数量的最大值
;第二个参数不能是 edx或EDX
;--------------------------------------------------------
IFIDNI <maxChars>,<EDX>
ECHO Warning: Second argument to mReadBuf cannont be EDX
ECHO ****************************************************
EXITM
ENDIF
push ecx
push edx
mov edx,bufferPtr
mov ecx,maxChars
call ReadString
pop edx
pop ecx
ENDM

.code
main PROC
mReadBuf OFFSET buffer,edx
main ENDP
END main

43

例子:对矩阵行求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
;---------------------------
;程序名:mRowSum.asm
;功能:计算选定数组的和
;作者:9unk
;编写时间:2023-4-9
;----------------------------
INCLUDE Irvine32.inc

;--------------------------------------------------------
mcalc_row_sum MACRO index,arrayOffset,rowsize,eltType
;入口参数:EBX = 表的偏移,EAX = 行数,ECX = 行的大小(BYTE)
;出口参数:EAX = 选定行的和
;--------------------------------------------------------
LOCAL L1
push ebx
push ecx
push edx
push esi
;设置参数
mov eax,index
mov ebx,arrayOffset
mov ecx,rowsize
;计算偏移
mul ecx
add ebx,eax
;准备循环计数器
shr ecx,(TYPE eltType / 2)
;初始化累加器和列索引
mov eax,0
mov esi,0
L1:
IFIDNI <eltType>,<DWORD>
mov edx,eltType PTR[ebx + esi*(TYPE eltType)]
ELSE
movzx edx,BYTE PTR[ebx + esi*(TYPE eltType)]
ENDIF

add eax,edx
inc esi
loop L1
pop esi
pop edx
pop ecx
pop ebx
ENDM


.data
TableB BYTE 1h ,2h ,3h,4h ,5h ,6h ,7h ,8h
rowsizeB = ($-TableB)
BYTE 9h ,10h,11h,12h,13h,14h,15h,16h
BYTE 17h,18h,19h,20h,21h,22h,23h,24h

TableW WORD 1h ,2h ,3h,4h ,5h ,6h ,7h ,8h
rowsizeW = ($-TableW)
WORD 9h ,10h,11h,12h,13h,14h,15h,16h
WORD 17h,18h,19h,20h,21h,22h,23h,24h

TableD DWORD 1h ,2h ,3h,4h ,5h ,6h ,7h ,8h
rowsizeD = ($-TableD)
DWORD 9h ,10h,11h,12h,13h,14h,15h,16h
DWORD 17h,18h,19h,20h,21h,22h,23h,24h

index DWORD 3

.code
main PROC
mcalc_row_sum index,OFFSET TableB,rowsizeB,BYTE
mcalc_row_sum index,OFFSET TableW,rowsizeW,WORD
mcalc_row_sum index,OFFSET TableD,rowsizeD,DWORD
exit
main ENDP
END main

特殊操作符

44

替换操作符

45
46
47
48

展开操作符(%)

49
50

51
52

文本操作符(<>)

53
54

宏函数

55
56

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;---------------------------
;程序名:HelloNew.asm
;功能:演示宏函数的使用
;作者:9unk
;编写时间:2023-4-10
;----------------------------
INCLUDE Macros.inc
IF IsDefined( RealMode )
INCLUDE Irvine16.inc
ELSE
INCLUDE Irvine32.inc
ENDIF

;Startup MACRO
; IF IsDefined(RealMode)
; mov ax,@data
; mov ds,ax
; ENDIF
;ENDM

.code
main PROC
Startup
mWrite <"This program can be assembled to run ",0dh,0ah>
mWrite <"in both Real mode and Protected mode.",0dh,0ah>
exit
main ENDP
END main

定义重复块

57

WHILE 指令

只要在特定常量表达式为真,WHILE 伪指令就重复语句块。格式如下:

1
2
3
WHILE constExpression
statements
ENDM

58

REPEAT 伪指令

59

FOR 伪指令

60
61

FORC 伪指令

62

链表

63

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
;---------------------------
;程序名:List.asm
;功能:演示链表的使用
;作者:9unk
;编写时间:2023-4-10
;----------------------------
INCLUDE Irvine32.inc

ListNode STRUCT
NodeData DWORD ?
NextPtr DWORD ?
ListNode ENDS

ToTalNodeCount = 15
NULL = 0
Counter = 0

.data
LinkedList LABEL PTR ListMode
REPEAT ToTalNodeCount
Counter = Counter + 1
ListNode <Counter,($+Counter*SIZEOF ListNode)>
ENDM
ListNode <0,0>

.code
main PROC
mov esi,OFFSET LinkedList

;显示 NodeData 域中的整数
NextNode:
;检查是否是尾节点
mov eax,(ListNode PTR [esi]).NextPtr
cmp eax,NULL
je quit
;显示节点数据
mov eax,(ListNode PTR [esi]).NodeData
call WriteDec
call Crlf
;获取下一个节点的指针
mov esi,(ListNode PTR [esi]).NextPtr
jmp NextNode
quit:
exit
main ENDP
END main

64

  • 本文标题:80386汇编-结构和宏
  • 本文作者:9unk
  • 创建时间:2023-04-01 10:45:00
  • 本文链接:https://9unkk.github.io/2023/04/01/80386-hui-bian-jie-gou-he-hong/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!