您可能不知道的关于 MASM 中宏的知识






4.96/5 (8投票s)
讨论了一些 MASM 宏的使用,包括 ECHO 指令、参数类型/大小检查以及使用位置计数器 $ 进行重复。
引言
宏是一种符号名称,你将其赋予一系列字符,称为文本宏,或者赋予一个或多个语句,称为宏过程或函数。当汇编器评估你程序的每一行时,它会扫描源代码,查找早期定义的宏的名称,然后用宏定义替换宏名称。宏过程是已命名的汇编语言语句块。一旦定义,就可以多次调用(调用)它,甚至可以传递不同的参数。这样你就可以避免在多个地方重复编写相同的代码。
在本文中,我们将讨论一些未被深入讨论或未被清晰记录的使用方法。我们将使用 Microsoft Visual Studio IDE 中的示例进行演示和分析。主题将零散地涉及 ECHO
指令,宏过程中检查参数类型和大小,以及使用当前位置计数器 $
进行内存生成和重复。
这里呈现的所有材料都来自于我多年的教学 [2]。因此,阅读本文需要对 Intel x86-64 汇编语言有基本了解,并且熟悉 Visual Studio 2010 或更高版本。最好是已经阅读过像 [3] 这样的典型教科书;或者 MASM 程序员指南 [5],该指南最初由 Microsoft 于 1992 年发布,但在当今的 MASM 学习中仍然非常有价值。如果你正在学习汇编语言编程课程,这可能是一篇补充阅读或学习参考。
使用 ECHO 输出到输出窗口
根据 MSDN 的说法,ECHO
指令会将消息显示到标准输出设备。在 Visual Studio 中,你可以使用 ECHO
将字符串发送到 IDE 的输出窗格,以显示汇编/编译消息,例如警告或错误,类似于 MSBuild 的工作方式。
一个简化的代码片段可以用来测试 ECHO
,如下所示
.386
.model flat,stdcall
ExitProcess proto,dwExitCode:DWORD
mTestEcho MACRO
ECHO ** Test Echo with Nothing **
ENDM
.code
main PROC
mTestEcho
invoke ExitProcess,0
main ENDP
END main
这段代码在 VS 2008 中可以正常工作。不幸的是,自 VS 2010 起,ECHO
指令在 Visual Studio 中不再写入输出窗口。正如 [4] 中所述,除非你配置它以生成详细的输出以汇编你的代码,否则它不会输出。要做到这一点,你应该
转到 工具->选项->项目和解决方案->生成和运行
然后在“MSBuild 项目生成输出详细程度”下拉框中,选择“详细”选项(请注意,默认值为“最小”)
要测试宏,你只需要编译一个单独的 .ASM 文件,而不是构建整个项目。只需右键单击你的文件并选择 **编译**
要验证,你必须查看 VS 输出窗格中的显示。ECHO
指令确实在工作,但是你感兴趣的消息“** Test Echo with Nothing **”埋藏在 MSBuild 生成的数百行中。你必须费力地搜索才能找到。
在学习 MASM 汇编编程时,这绝对不是一个首选的练习方式。我建议使用文本“Error:”或“Warning:”结合 ECHO
,同时保持 MSBuild 的默认输出设置“最小”不变。
1. 输出为错误
只需在宏的 ECHO
语句中添加“Error:
”并将其命名为 mTestEchoError
mTestEchoError MACRO
ECHO Error: ** Test Echo with Error **
ENDM
现在,让我们在 main PROC
中调用 mTestEchoError
。编译代码,你可以看到如下简洁的最小输出。请注意,由于这里出现错误,结果合理地显示为失败。
2. 输出为警告
只需在 ECHO
语句中添加“Warning:
”并将其命名为 mTestEchoWarning
mShowEchoWarning MACRO
ECHO Warning: ** Test Echo with Warning **
ENDM
然后调用 main PROC
中的 mTestEchoWarning
并进行编译,你可以看到如下更简单的最小输出。由于只指定了警告,因此编译成功。
如你所知,通过这种方式,ECHO
指令可以生成简洁明了的消息,而无需你搜索输出。样本在可下载的 TestEcho.asm 中。
检查参数类型和大小
当你将参数传递给宏过程时,该过程会通过参数接收它,尽管它只是文本替换。通常,你会检查参数的一些条件来相应地执行操作。由于这发生在汇编时,这意味着汇编器会在满足某个条件时选择一些指令,否则在需要时提供其他指令来处理不满足的条件。当然,你可以检查常量参数值,无论是字符串还是数字。另一个有用的检查可能是基于寄存器和变量的参数类型或大小。例如,一个宏过程只接受无符号整数并忽略有符号整数,而第二个宏可以处理 16 位和 32 位参数,而不处理 8 位和 64 位参数。
1. 参数作为内存变量
让我们在这里定义三个变量
.data
swVal SWORD 1
wVal WORD 2
sdVal SDWORD 3
应用 TYPE
和 <code>SIZEOF
运算符到这些变量,我们得到:
mov eax, TYPE swVal ; 2 bytes
mov eax, SIZEOF swVal ; 2 bytes
mov eax, TYPE wVal ; 2 bytes
mov eax, SIZEOF wVal ; 2 bytes
mov eax, TYPE sdVal ; 4 bytes
mov eax, SIZEOF sdVal ; 4 bytes
如上所示,TYPE
和 SIZEOF
之间,或者 WORD
和 SWORD
之间没有数值差异。前四条指令都将字节数 2
移动到 EAX
。然而,TYPE
不仅仅能返回字节数。让我们尝试检查 SWORD
类型和大小与参数 par
mParameterTYPE MACRO par
IF TYPE par EQ TYPE SWORD
ECHO warning: ** TYPE par is TYPE SWORD
ELSE
ECHO warning: ** TYPE par is NOT TYPE SWORD
ENDIF
ENDM
mParameterSIZEOF MACRO par
IF SIZEOF par EQ SIZEOF SWORD
ECHO warning: ** SIZEOF par is SIZEOF SWORD
ELSE
ECHO warning: ** SIZEOF par is NOT SIZEOF SWORD
ENDIF
ENDM
然后通过传递上面定义的变量来调用两个宏
ECHO warning: --- Checking TYPE and SIZEOF for wVal ---
mParameterTYPE wVal
mParameterSIZEOF wVal
ECHO warning: --- Checking TYPE and SIZEOF for swVal ---
mParameterTYPE swVal
mParameterSIZEOF swVal
ECHO warning: --- Checking TYPE and SIZEOF for sdVal ---
mParameterTYPE sdVal
mParameterSIZEOF sdVal
在 **输出** 中看到以下结果
显然,TYPE
运算符可用于区分传递的有符号或无符号参数,因为 SWORD
和 WORD
是不同的类型。而 SIZEOF
仅仅是字节数的比较,因为 SWORD
和 WORD
都是 2 字节。最后两个检查意味着 SDWORD
的类型不是 SWORD
,而 SDWORD
的大小是 4 字节而不是 2 字节。
此外,让我们直接进行检查,因为这两个运算符也可以应用于此处的数据类型名称
mCheckTYPE MACRO
IF TYPE SWORD EQ TYPE WORD
ECHO warning: ** TYPE SWORD EQ TYPE WORD
ELSE
ECHO warning: ** TYPE SWORD NOT EQ TYPE WORD
ENDIF
ENDM
mCheckSIZEOF MACRO
IF SIZEOF SWORD EQ SIZEOF WORD
ECHO warning: ** SIZEOF SWORD EQ SIZEOF WORD
ELSE
ECHO warning: ** SIZEOF SWORD NOT EQ SIZEOF WORD
ENDIF
ENDM
以下结果直观且简单明了
2. 参数作为寄存器
由于参数可以是寄存器,让我们调用前面的两个宏来检查其 TYPE
和 SIZEOF
mParameterTYPE AL
mParameterSIZEOF AL
mParameterTYPE AX
mParameterSIZEOF AX
我们收到以下消息
正如我们在这里看到的,对于类型检查,AL
或 AX
(甚至是 16 位)都不是有符号 WORD
。实际上,你不能将 SIZEOF
应用于寄存器,这会导致汇编 error A2009
。你可以直接验证:
mov ebx, SIZEOF al ; error A2009: syntax error in expression
mov ebx, TYPE al
但寄存器是什么类型?答案是所有寄存器默认都是无符号的。只需执行此操作:
mParameterTYPE2 MACRO par
IF TYPE par EQ WORD
ECHO warning: ** TYPE par is WORD
ELSE
ECHO warning: ** TYPE par is NOT WORD
ENDIF
ENDM
并调用
mParameterTYPE2 AL ; 1>MASM : warning : ** TYPE AL is NOT WORD
mParameterTYPE2 AX ; 1>MASM : warning : ** TYPE AX is WORD
另外请注意,我在这里直接使用了数据类型名称 WORD
,这等同于使用 TYPE WORD
。
3. 实践中的一个例子
现在让我们看一个具体的例子,需要将一个 8 位、16 位或 32 位有符号整数的参数移动到 EAX
。要创建这样的宏,我们必须根据参数大小使用 mov
指令或符号扩展 movsx
。以下是比较参数类型与所需大小的一种可能的解决方案。%OUT
与 ECHO
相同,作为替代。
mParToEAX MACRO intVal
IF TYPE intVal LE SIZEOF WORD ;; 8- or 16-bit
movsx eax, intVal
ELSEIF TYPE intVal EQ SIZEOF DWORD ;; 32-bit
mov eax,intVal
ELSE
%OUT Error: ***************************************************************
%OUT Error: Argument intVal passed to mParToEAX must be 8, 16, or 32 bits.
%OUT Error:****************************************************************
ENDIF
ENDM
用不同大小和类型的变量和寄存器进行测试
; Test memory
mParToEAX bVal ; BYTE
mParToEAX swVal ; SWORD
mParToEAX wVal ; WORD
mParToEAX sdVal ; SDWORD
mParToEAX qVal ; QWORD
; Test registers
mParToEAX AH ; 8 bit
mParToEAX BX ; 16 bit
mParToEAX EDX ; 32 bit
mParToEAX RDX ; 64 bit
正如预期的那样,输出显示了以下消息,合理地拒绝了 qVal
。对于 RDX
报告的错误也很好,因为我们的 32 位项目不识别 64 位寄存器。
你可以尝试可下载的代码 ParToEAX.asm。此外,让我们生成其列表文件,看看汇编器创建了哪些指令来替换宏调用。正如预期的那样,bVal
、swVal
、wVal
和 sdVal
都很好,但没有 qVal
;而 AH
、BX
和 EDX
很好,但没有 RDX
。
00000000 .data
00000000 03 bVal BYTE 3
00000001 FFFC swVal SWORD -4
00000003 0005 wVal WORD 5
00000005 FFFFFFFA sdVal SDWORD -6
00000009 qVal QWORD 7
0000000000000007
00000000 .code
00000000 main_pe PROC
; Test memory
mParToEAX bVal ; BYTE
00000000 0F BE 05 1 movsx eax, bVal
00000000 R
mParToEAX swVal ; SWORD
00000007 0F BF 05 1 movsx eax, swVal
00000001 R
mParToEAX wVal ; WORD
0000000E 0F BF 05 1 movsx eax, wVal
00000003 R
mParToEAX sdVal ; SDWORD
00000015 A1 00000005 R 1 mov eax,sdVal
mParToEAX qVal ; QWORD
; Test registers
mParToEAX AH ; 8 bit
0000001A 0F BE C4 1 movsx eax, AH
mParToEAX BX ; 16 bit
0000001D 0F BF C3 1 movsx eax, BX
mParToEAX EDX ; 32 bit
00000020 8B C2 1 mov eax,EDX
mParToEAX RDX ; 64 bit
1 IF TYPE RDX LE SIZEOF WORD
AsmCode\ParToEAX.asm(45) : error A2006:undefined symbol : RDX
mParToEAX(1): Macro Called From
AsmCode\ParToEAX.asm(45): Main Line Code
1 ELSE
AsmCode\ParToEAX.asm(45) : error A2006:undefined symbol : RDX
mParToEAX(3): Macro Called From
AsmCode\ParToEAX.asm(45): Main Line Code
invoke ExitProcess,0
00000029 main_pe ENDP
END ; main_pe
生成重复数据
在本节中,我们将讨论使用宏生成内存块,即数据段中的整数数组,而不是在代码中调用宏。我们将展示三种方法来创建相同的链表:使用不变的位置计数器 $
,检索计数器 $
的已更改值,以及在数据段中调用宏。
1. 使用不变的定位计数器 $
我从教科书 [3] 中借用了 LinkedList
片段,并将其修改为八个节点,如下所示
LinkedList ->11h ->12h ->13h ->14h ->15h ->16h ->17h ->18h ->00h
我在末尾添加了六个额外的 DWORD
的 01111111h
用于填充,虽然不是必需的,但在内存窗口中格式化观察很方便。
ListNode STRUCT
NodeData DWORD ?
NextPtr DWORD ?
ListNode ENDS
TotalNodeCount = 8
.data
; LinkedList created with $ not changed:
Counter = 0
LinkedList LABEL PTR ListNode
REPT TotalNodeCount
Counter = Counter + 1
ListNode <Counter+10h, ($ + Counter * SIZEOF ListNode)>
ENDM
ListNode <0,0> ; tail node
DWORD 01111111h, 01111111h, 01111111h, 01111111h, 01111111h, 01111111h
内存已创建。列表头是标签 LinkedList
,它指向 0x00404010
的别名。
每个节点包含一个 4 字节的 DWORD
作为 NodeData
,以及另一个 DWORD
作为 NextPtr
。由于 Intel IA-32 使用小端序,内存中的第一个整数 11 00 00 00
,在十六进制中是 00000011
;其下一个指针 18 40 40 00
,是 0x00404018
。因此,两行覆盖了所有八个列表节点。在第三行,第一个节点有两个零 DWORD
,充当尾部(虽然浪费了一个节点)。紧随其后的是六个 01111111
的填充。
现在让我们看看当前位置计数器 $
会发生什么。正如 [3] 中提到的
表达式 ($
+ Counter
* SIZEOF ListNode
) 告诉汇编器将计数器乘以 ListNode
的大小,并将它们的乘积加到当前位置计数器上。该值被插入到结构中的 NextPtr
字段。[有趣的是,位置计数器的值 ($
) 在列表的第一个节点处保持固定。]
这确实是真的,在 REPT
块的每次迭代中,$
的值始终保持 0x00404010
不变。通过 ($
+ Counter
* SIZEOF ListNode
) 计算的 NextPtr
地址使得节点一个接一个地链接起来,最终生成 LinkedList
。但是,你可能会问,我们是否可以使用实际的当前内存地址来进行迭代?是的。这里来了。
2. 从位置计数器 $ 检索已更改的值
.data
; LinkedList2 created with $ changed:
Counter = 0
LinkedList2 LABEL PTR ListNode
REPT TotalNodeCount
Counter = Counter + 1
ThisPointer = $
ListNode <Counter+20h, (ThisPointer + SIZEOF ListNode)>
ENDM
ListNode <0,0> ; tail node
DWORD 02222222h, 02222222h, 02222222h, 02222222h, 02222222h, 02222222h
len = ($ - LinkedList)/TYPE DWORD
嘿,几乎没有什么变化,只是给一个新的符号常量 ThisPointer
= $
,它只是将 $
的当前内存地址赋值给 ThisPointer
。现在我们可以使用 ThisPointer
进行类似的计算来初始化 ListNode
对象的 NextPtr
字段,使用一个更简单的表达式 (ThisPointer
+ SIZEOF ListNode
)。这也使得节点一个接一个地链接起来,这次生成 LinkedList2
。你可以检查 LinkedList2
的内存,0x00404070
为了区分第一个 LinkedList
,我让 Counter+20h
来使其像
LinkedList2 ->21h ->22h ->23h ->24h ->25h ->26h ->27h ->28h ->00h
通过比较两个内存块,它们都执行完全相同的功能。请注意,最后,我特意计算了 len
来查看直到目前生成了多少 DWORD
。
len = ($ - LinkedList)/TYPE DWORD
作为一项有趣的练习,请在脑海中思考 len
的值。在代码中,将 len
移动到寄存器进行验证。
3. 调用数据段中的宏
通过创建第三个链表,我们可以理解,不仅可以在代码中调用宏,还可以在数据段中调用宏。为此,我定义了一个名为 mListNode
的宏,带有一个名为 start
的参数,其中 ListNode
对象被简单初始化。为了区分前两个,我将 Counter+30h
用于 NodeData
,并将 NodePtr 设置为 (start
+ Counter
* SIZEOF ListNode
)。
.data
; LinkedList3 created with a macro call:
mListNode MACRO start
Counter = Counter + 1
ListNode <Counter+30h, (start + Counter * SIZEOF ListNode)>
ENDM
LinkedList3 = $ ; still cannot directly use $, Must get the current value out
Counter = 0
REPT TotalNodeCount
mListNode LinkedList3 ; What if pass $ as an argument?
ENDM
ListNode <0,0> ; tail node
DWORD 03333333h, 03333333h, 03333333h, 03333333h, 03333333h, 03333333h
第三个列表看起来像
LinkedList3 ->31h ->32h ->33h ->34h->35h ->36h->37h ->38h->00h
现在我们从 LinkedList2
吸取教训,在开始时让 LinkedList3
= $
。请注意,我只是使用符号常量 LinkedList3
作为第三个列表头,而不是使用 LABEL
指令。现在我设置 REPT
重复,只调用一次宏,将头部地址 LinkedList3
传递给 mListNode
。就是这样!在 0x004040D0
查看内存。
想象一下,如果你将 $
作为参数传递给 mListNode
,而不先进行 LinkedList3
= $
?
4. 检查地址和遍历链表
最后,让我们将三个列表的所有生成放在一起并运行 LinkedList.asm(可下载)。在代码段中,我首先检索三个列表头的地址,如下所示:
mov edx,OFFSET LinkedList
mov ebx,OFFSET LinkedList2
mov esi,OFFSET LinkedList3 ; or directly LinkedList3
mov eax, len
正如预期的那样,EDX
,00404010
是给 LinkedList
的;EBX
,00404070
是给 LinkedList2
的;ESI
,004040D0
是给 LinkedList3
的。三个列表的整个内存块彼此相邻,如下所示:
请注意,由于 LinkedList3
是符号化的,我们甚至不必在这里使用 OFFSET
运算符。让我们将 ESI
用于 LinkedList3
,并遍历这个列表,用一个循环来查看每个 NodeData
的值,如下所示:
; Display the integers in the NodeData members.
NextNode:
; Check for the tail node.
mov eax, (ListNode PTR [esi]).NextPtr
cmp eax, 0
je quit
; Display the node data.
mov eax, (ListNode PTR [esi]).NodeData
; call a PROC to show EAX here
; Get pointer to next node.
mov esi, (ListNode PTR [esi]).NextPtr
jmp NextNode
quit:
不幸的是,我们还没有实现任何输出过程来调用这里来显示移动到 EAX
的 NodeData
。但是在调试时,只需在那里设置一个断点来观察 EAX
就足以从 31h
、32h
、...、到 38h
进行验证。
摘要
通过仔细审视上述示例,我们揭示了一些关于 MASM 汇编编程中宏的你可能不知道的事情。汇编语言程序可以在运行时使用 Intel 或 AMD 指定的指令执行。另一方面,MASM 提供了许多指令、运算符和符号,在汇编时控制和组织指令和内存变量,类似于其他编程语言中的预处理。事实上,凭借所有这些功能,MASM 宏本身就可以被视为一个子语言或迷你编程语言,具有顺序、条件和重复三种控制机制。
然而,MASM 宏的一些用法尚未得到详细讨论。在本文中,我们首先介绍了一种更好的方法来输出错误或警告文本,使其易于跟踪宏的行为。然后,通过 if
-else
结构,我们展示了如何检查宏参数的类型和大小,这通常是内存或寄存器参数的常见做法。最后,我们讨论了宏的重复,并提供了三个示例来生成相同的链表,以及更好地理解如何使用当前地址定位器 $
符号。可下载的 zip 文件包含所有示例的 .asm 文件。项目文件 MacroTest.vcxproj 已在 VS 2010 中创建,但可以在任何较新版本的 VS 中打开和升级。
本文不涉及 .NET 或 C# 等热门技术。汇编语言相对传统,没有太多轰动。但请参考 TIOBE 编程社区指数,最近汇编语言的排名呈上升趋势,这意味着各种类型的汇编语言在新设备开发中发挥着重要作用。在学术上,汇编语言编程被认为是计算机科学中的一门高要求课程。因此,我希望本文能为学生以及可能还有开发人员提供解决问题的示例。
参考文献
- Microsoft Macro Assembler Reference
- CSCI 241, Assembly Language Programming class site
- Kip Irvine, Assembly Language for x86 Processors, 7th edition
- MASM ECHO directive does not result in writing to the output window
- MASM 6.1 Documentation
历史
- 2016年2月26日 -- 首次发布