第十九讲: 宏定义

程序员小x大约 14 分钟汇编语言

第十九讲: 宏定义

就像 C/C++ 一样,YASM 有一个预处理器,它在实际进行汇编之前对汇编程序的文本进行操作。因为它在汇编时运行,所以它实际上可以是比汇编本身更丰富的“语言”。另一方面,因为它在汇编时运行,所以它不能引用任何运行时信息(寄存器或内存的内容)。基于文本的宏定义语言实际上在计算机科学中相当常见,因此值得深入了解至少一种(如果您还没有研究过 C/C++ 预处理器)。

错误

我们可以使用 %error 宏停止汇编并打印消息:

%error Something went wrong.

包含文件

就像在 C/C++ 中一样,我们可以在当前 .s 文件中包含另一个文件的内容:

%include "file.s"

将直接包含 file.s 的内容。

请注意,有一种不同的机制可将二进制文件嵌入到汇编文件中:

data:     incbin "file.data"

将二进制文件file.dat的内容包含到可执行文件中,标记为data

单行宏

从本质上讲,宏预处理器的工作原理是用一些其他文本替换一些文本。例如:

%define accumulator     rax

表示每当看到accumulator时,都应该将其替换为文本 rax。因此,我们现在可以写类似的东西:

add accumulator, 10   ; Equivalent to    add rax, 10

宏可以重新定义,我们可以做:

%define accumulator rax

然后做:

%define accumulator rcx

accumulator将扩展到最新定义的内容。

我们可以用另一个宏来定义一个宏

%define increment   inc accumulator

现在,当我们写:

increment

这将首先扩展为 inc accumulator,然后扩展为 inc rax。请注意,accumulator的扩展不会在我们%define increment时发生,而是在我们使用increment时发生。这意味着在定义其他宏期间,宏扩展会暂时停止。如果稍后重新定义accumulator(如上所述),则accumulator将使用最新的定义,而不是定义时的定义。

另一方面,有时我们希望在定义时扩展定义,而不是等到使用时才扩展。如果使用 %xdefine 定义宏,则定义将在定义点立即展开。通过重新定义宏可以看出差异:

%define a  1
%define b  a
%xdefine c a        ; Equivalent to %define x 1
...
%define a 2
mov rax, b          ; Expands to mov rax, 2
mov rcx, c          ; Expands to mov rax, 1

如果定义一个宏来扩展自身,例如 %define x x,使用它不会使汇编器进入无限循环。当我们稍后看到函数式宏时,这意味着递归是不可能的。请注意,这对于 %xdefine 宏来说不是问题。

通过使用 %undef 可以取消定义宏(因此使用其名称是错误的):

%undef accumulator

...
mov rbx, accumulator 

第二行将假设累加器是一个标签,如果没有这样定义,则给出错误。

函数式宏

单行宏可以有参数

%define increment(r)    inc r

要使用它,我们必须提供参数,无论何时使用 r,该参数都将被拼接到扩展中:

increment(rax)              ; expands to inc rax
increment(qword [var])      ; expands to inc qword [var]

多个参数之间用逗号分隔:

%define swap(a,b)       xchg a, b
...
swap(rax, rdi)          ; Expands to xchg rax, rdi

YASM允许“宏重载”;多个宏具有相同的名称和不同数量的参数,具有不同的定义。

区分大小写

宏默认区分大小写:%define foo 1 将扩展 foo,但不扩展 FooFOO。有些汇编器不区分大小写,因此为了兼容性,YASM 有 %idefine%xidefine 定义不区分大小写的宏。

连接宏扩展

有时我们需要通过一些宏扩展来形成单个字符串。例如,如果我们写:

%define reg(b) r b

reg(ax)         ; Expands to r ax, which is *not* a register!

a %+ b 连接左右两侧的文本,“吃掉”其周围的任何空格。 reg 的正确定义是:

%define reg(b) r %+ b

reg(ax)        ; Expands to rax

算术扩展宏

假设我们要定义一个包含一个数值的宏,然后能够递增它。我们可以尝试这样的事情:

%xdefine v 0
...
%xdefine v v+1      ; Now v expands to 0+1

这是可行的,因为汇编器将在预处理器完成后执行算术 0+1 并计算出正确的值,但它很麻烦。经过很多几次增量后,我们才可以将 v 扩展到更大的数字 。

我们可以使用 %assign 来代替这样做。 %assign 的工作方式与 %xdefine 类似,只不过它评估其定义中的任何算术。所以我们可以使用:

%assign v 0
%assign v v+1       ; Now v expands to 1

因此,v 的展开总是类似于数字的东西,而不是像 0+1 这样的字符串。

字符串处理

YASM 的预处理器具有一些处理字符串文字的功能:“...”或“...”。您可以提取字符串文字的长度(字符数):

%strlen len "String"

%assign将 len设置为6。请注意,以下内容将不起作用:

string:     db  "String"

%strlen len string

因为 string 不是字符串,而是指向内存中字符串第一个字符的标签(地址)。另一方面,我们可以定义一个扩展为字符串文字的宏,然后询问其长度:

%define string "String"
%strlen len string

同样,您可以在字符串文字中添加“下标”来添加额外的单个字符:

%substr c "String" 2        ; Defines c to be 'r'

下标以 1 开头,而不是 0,因此最后一个字符的位置等于字符串的 %strlen。(TODO 这里描述前后不一致)

多行宏

更复杂的宏将需要多行定义。这是通过使用 %macro%endmacro 来完成的:

%macro swap 2
    mov r11, %1
    mov %1, %2
    mov %2, r11
%endmacro

与单行宏不同,多行宏只知道参数的数量(上面的 2 个),而不知道它们的名称。参数的名称始终为 %1%2 等。

要调用多行宏,请使用其名称,后跟其参数(不在括号中)

swap rax, rcx
; Expands into 
;   mov r11, rax
;   mov rax, rbx
;   mov rbx, r11

与单行宏一样,多行宏可以在参数数量上超载。您甚至可以定义与指令同名的多行宏:

%macro push 2
    push %1
    push %2
%endmacro
...
push rax            ; Normal push instruction
push rax, rbx       ; Expands to the above

汇编器会发出警告,但上面的代码工作得很好

其余参数

您可以创建一个多行宏,它接受任意数量的参数。例如,

%macro print 1+
  section .data
    %%string:   db      %1
    %%strlen:   equ     $-%%string

  section .text
    mov rax, 1
    mov rdi, 1
    mov rsi, %%string
    mov rdx, %%strlen
    syscall
%endmacro

在这里,我们暂时切换到 .data 部分以添加新的字符串常量,然后切换回 .text 并展开到打印它的系统调用。我们可以像这样使用它:

print "Hello world!", 10

并且无论给出多少个参数,它们都会被放在%1中。

请注意,我们不能再使用 2、3 等参数来重载 print。这给定的定义有效地定义了从 1 到无穷大的所有参数计数的 print 的不同版本。

默认参数

我们可以支持一个范围,并为任何省略的参数提供默认值:

%macro swap 2-3 r11
    mov %3, %1
    mov %1, %2
    mov %2, %3
%endmacro

这里的2-3表示swap接受2-3个参数。如果提供两个参数,则r11则默认作为第三个参数。

如果我们将其用作交换 raxrbx,则 %3 会扩展为默认值 r11。另一方面,如果我们提供第三个参数,例如 swap rax、rbx、r15,则提供的第三个参数r15将用于 %3

如果我们创建一个具有 3-5 个参数的宏,那么我们必须提供 5-3 = 2 个默认值,这些默认值将成为参数 %4%5 的默认值。如果省略默认值,则默认值将不扩展为任何内容。

默认参数可以与其余参数组合;你可以写 3-5+,这意味着 3 或更多,但任何超过 5 的都进入 %5

您可以通过写入 3-*(三到无穷大)来指定无限的最大参数数量。当然,您无法为所有这些编写默认值。它和 + 之间的区别在于 + 将所有剩余参数分组为一个参数,同时这使得它们都可以单独访问。 %0表示的是提供的实际参数的数量。

旋转参数列表

假设宏采用三个参数:%1%2%3。我们可以通过发出宏 %rotate 1 来旋转列表。旋转后,原来的第二个参数会在第一个位置,第三个参数会在第二个位置,而第一个参数会一直旋转到3。这在重复宏中最有用,因为它允许我们访问所有参数而无需数字索引。例如,以下是推送的一个版本,它接受任意数量的参数并推送所有参数:例如,以下是推送的一个版本,它接受任意数量的参数并推送所有参数:

%macro push 2-*
  %rep %0
    push %1
    %rotate 1
  %endrep
%endmacro

push rax, rbx, rcx, qword [var]

这里%0的值为4。

如果 n 为正数,%rotate n 将参数列表向左旋转 n 个空格(朝向参数 %1)。如果n为负数,则向右旋转。

%rep ... %endrep 稍后在重复宏open in new window下讨论。

宏局部名称

如果多行宏可以扩展为代码,我们可能希望扩展为包含标签的代码(例如,扩展为循环)。然而,如果宏被多次扩展,这将会导致问题;那么我们就会对同一个标签有多个定义。为了解决这个问题,我们可以使用宏局部标签。宏局部标签是名称以 %% 开头的普通标签。例如,

%macro retz 0
    jnz     %%skip
    ret

  %%skip:
%endmacro

每次扩展宏时,都会为标签 %%skip 生成一个新的唯一名称,因此所有扩展都不会相互干扰。

宏本地名称实际上不必用作标签。例如,我们可以使用它作为单行宏名称来创建一种"局部变量":

%macro testmacro 0
    %assign %%v 0
    mov rax, %%v
%endmacro

在这里,变量%%v每次宏展开时都会获得不同的名称。

串联多行参数

与需要特殊 %+ 运算符来连接的单行参数不同,多行参数不需要这样的表示法:

%macro string_n 2
    string%1:   db  %2
%endmacro

string_n 7 "Hello"          ; Expands into string7: db "Hello"

如果我们想在参数后面连接一些文本,我们可以写 %{1}text;这将扩展 %1,然后在扩展后立即添加文本,中间没有空格。

条件码参数

YASM 对包含条件代码(zge 等)的参数有特殊支持。如果 %1 扩展为条件代码,则 %-1 扩展为该代码的否定。例如,z 变为 nz,ge 变为 l,等等。类似地,%+1 扩展为原始的、未更改的条件代码,只不过它强制参数实际上是条件代码,如果不是,则给出错误。

例如,上面的 retz 宏可以推广为允许任何条件代码(默认为 z)的宏,方法是:

%macro retcc 0-1 z
    j%-1    %%skip
    ret

  %%skip:
%endmacro

条件宏

通常,我们希望根据某些(汇编时)参数包含源文本的某些部分,并在其余时间排除它或用其他文本替换它。这可以通过条件宏来完成。最基本的条件宏反映了熟悉的 if-else if-else 语句:

%if<condition>
    ...
%elif<condition>
    ...
%else
    ...
%endif

正如我们所期望的,0 个或超过 1 个 %elif-s 是允许的,最后的 %else 是可选的。请注意,条件立即出现在 %if/%elif 之后,中间没有空格。

def – 检查单行宏的定义

我们可以使用 %ifdef 来检查给定的(单行)宏是否已经被定义。例如,

%ifdef DEBUG
    ... ; Debug build code
%else
    ... ; Production code
%endif

未定义可以使用条件 ndef 进行测试。

检查多行定义的宏

宏检查是否定义了多行宏:

%ifmacro push 2+

    ; Multi-arg push is defined

%endif

数值表达式

%if expr 将检查数值表达式 expr,如果其值非零则继续。您可以在表达式中使用普通的比较运算符。请注意,相等是=(单个等于),不等是<>。

idn – 文本比较

如果 t1 和 t2 扩展为相同的文本序列,则 %ifidn t1, t2返回真。

num、id、str – 检查令牌类型

  • 如果 t 扩展为看起来像数字的内容,则 %ifnum t 成功。
  • %ifid t 如果 t 扩展为看起来像标识符的东西(即标签或 equ),则成功
  • 如果 t 扩展为看起来像字符串文字的内容,则 %ifstr t 成功。

重复宏

要重复某些文本一定次数,我们使用 %rep

%rep 10
    inc rax
%endrep

这将扩展为十个 inc rax 指令。 %rep 的参数可以是数值表达式。

%assign 可以与 %rep 一起使用来创建循环变量:

%assign i 0
%rep 10
    mov qword [arr + i], i
    %assign i i+1
%endrep

这将地址 arr 处的 10-qword 数组初始化为 0, 1, 2, 3, … 9

我们可以使用 %exitrep 提前停止 %rep

section .data
data:

%assign i 1
%rep 100
    db i
    %assign i i*2
    %if i > 1024
        %exitrep
    %endif
%endrep

这将创建一个带有标签数据的数组,其初始化为 1, 2, 4, 8, ...。

上下文堆栈

上下文堆栈是一种允许诸如宏本地标签之类的机制(如果宏多次扩展,这些标签不会中断), 但被多个宏定义共享。例如,目前,一个宏中定义的宏局部标签不能以任何方式被另一个宏引用;它是看不见的。这使得定义更高级的宏(通常需要多个定义)变得困难或不可能。

为了解决这个问题,YASM 维护了一个“上下文”堆栈。可以创建堆栈顶部上下文本地的标签。新的上下文可以使用 %push 推入堆栈顶部,并可以使用 %pop 删除。由于使用了堆栈,因此可以嵌套精美的宏而不会相互破坏。

上下文本地标签

要创建当前上下文本地的标签,我们编写 %$name。这也可以用于 %define%assign 一个宏,其名称是当前上下文的本地宏:

%define %$lm 5
%assign %$i 0

这可以防止变量干扰其他作用域。

请注意,当上下文被 %pop-ed 时,其所有本地标签/宏都是未定义的。

示例:块 IF 语句

假设我们想定义一个宏,它允许我们编写一个更自然的 if-like 结构:

IF rax, e, rcx
    ...
ENDIF

这扩展到类似的东西

cmp rax, rcx
jne .endif
    ...
.endif:

除了标签 .endif 应该为每个 IF-ENDIF 唯一生成之外。

我们需要定义两个%宏:

%macro IF 3
    %push if 
    cmp %1, %3
    j%-2 %$endif
%endmacro

%macro ENDIF 0
    %$endif:
    %pop
%endmacro
  • 当我们调用 IF 宏时,它会将 if 上下文压入堆栈,让我们知道我们处于 if 内部。

  • 它还执行比较,如果比较失败则跳转。

  • ENDIF 宏创建作为 (2) 中跳转目标的标签,并从堆栈中删除 if 上下文(因为我们不再位于 if 内部)。

这个宏可以工作,但是如果我们使用没有匹配 IF 的 ENDIF,它会严重失败。我们可以使用 %ifctx 条件来检查堆栈顶部的上下文,如果不在 IF 内,则发出错误:

%macro ENDIF 0
    %ifctx if
        %$endif:
        %pop
    %else
        %error Expected IF before ENDIF
    %endif
%endmacro

我们可以使用类似的技术来定义用于迭代的 DO-WHILE 宏:

%macro DO 0
    %push do_while
    %$do
%endmacro

%macro WHILE 3
    %ifctx do_while
        cmp %1, %3
        j%-2 %$do
    %else
        %error Expected DO before WHILE
    %endif
%endmacro

这可以用作

mov rax, 0
DO
    mov qword [arr + rax], rax
    inc rax

WHILE rax, le, 100 

这些循环/if 甚至可以相互嵌套,只要它们使用不同的寄存器即可。

另一个例子:PROC/ENDPROC在微软汇编器中用于标记函数的开始/结束:

PROC myfunction
    ... Stuff

ENDPROC

在 MASM 中,需要这些来使函数内部的标签成为函数的本地标签; YASM不需要这个,因为我们有本地标签,但我们仍然可以定义它们以实现兼容性。我们甚至可以添加一些错误检查,这样没有 PROC 的 ENDPROC 或嵌套 PROC 就是一个汇编时错误:

%macro PROC 1
    %ifnctx proc
        %push proc
        %{1}:
    %else
        %error Found PROC without preceding closing ENDPROC
    %endif
%endmacro

%macro ENDPROC 0
    %ifctx proc
        %pop 
    %else
        %error Found ENDPROC without preceding PROC
    %endif
%endmacro

原文链接: https://staffwww.fullcoll.edu/aclifton/cs241/lecture-macros.htmlopen in new window

yasm 介绍文档: https://www.tortall.net/projects/yasm/manual/html/manual.html#nasm-macro-context-localopen in new window

Loading...