65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (8投票s)

2016年2月23日

CPOL

12分钟阅读

viewsIcon

32061

downloadIcon

266

讨论了一些 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

如上所示,TYPESIZEOF 之间,或者 WORDSWORD 之间没有数值差异。前四条指令都将字节数 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 运算符可用于区分传递的有符号或无符号参数,因为 SWORDWORD 是不同的类型。而 SIZEOF 仅仅是字节数的比较,因为 SWORDWORD 都是 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. 参数作为寄存器

由于参数可以是寄存器,让我们调用前面的两个宏来检查其 TYPESIZEOF

   mParameterTYPE AL     
   mParameterSIZEOF AL  
   mParameterTYPE AX
   mParameterSIZEOF AX

我们收到以下消息

正如我们在这里看到的,对于类型检查,ALAX(甚至是 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。以下是比较参数类型与所需大小的一种可能的解决方案。%OUTECHO 相同,作为替代。

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。此外,让我们生成其列表文件,看看汇编器创建了哪些指令来替换宏调用。正如预期的那样,bValswValwValsdVal 都很好,但没有 qVal;而 AHBXEDX 很好,但没有 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

我在末尾添加了六个额外的 DWORD01111111h 用于填充,虽然不是必需的,但在内存窗口中格式化观察很方便。

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

正如预期的那样,EDX00404010 是给 LinkedList 的;EBX00404070 是给 LinkedList2 的;ESI004040D0 是给 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:

不幸的是,我们还没有实现任何输出过程来调用这里来显示移动到 EAXNodeData。但是在调试时,只需在那里设置一个断点来观察 EAX 就足以从 31h32h、...、到 38h 进行验证。

摘要

通过仔细审视上述示例,我们揭示了一些关于 MASM 汇编编程中宏的你可能不知道的事情。汇编语言程序可以在运行时使用 Intel 或 AMD 指定的指令执行。另一方面,MASM 提供了许多指令、运算符和符号,在汇编时控制和组织指令和内存变量,类似于其他编程语言中的预处理。事实上,凭借所有这些功能,MASM 宏本身就可以被视为一个子语言或迷你编程语言,具有顺序、条件和重复三种控制机制。

然而,MASM 宏的一些用法尚未得到详细讨论。在本文中,我们首先介绍了一种更好的方法来输出错误或警告文本,使其易于跟踪宏的行为。然后,通过 if-else 结构,我们展示了如何检查宏参数的类型和大小,这通常是内存或寄存器参数的常见做法。最后,我们讨论了宏的重复,并提供了三个示例来生成相同的链表,以及更好地理解如何使用当前地址定位器 $ 符号。可下载的 zip 文件包含所有示例的 .asm 文件。项目文件 MacroTest.vcxproj 已在 VS 2010 中创建,但可以在任何较新版本的 VS 中打开和升级。

本文不涉及 .NET 或 C# 等热门技术。汇编语言相对传统,没有太多轰动。但请参考 TIOBE 编程社区指数,最近汇编语言的排名呈上升趋势,这意味着各种类型的汇编语言在新设备开发中发挥着重要作用。在学术上,汇编语言编程被认为是计算机科学中的一门高要求课程。因此,我希望本文能为学生以及可能还有开发人员提供解决问题的示例。

参考文献

历史

  • 2016年2月26日 -- 首次发布
© . All rights reserved.