FASM 第二章 – 2.3 预处理伪指令

    Author: 徐艺波  From: xuyibo.org  Updated: 2021-04-16

      建议  有什么好的建议,可以贴一下。
      捐助  你的支持,让我们做的更好。

    2.3 预处理伪指令
    2.3.1 包含源文件
    2.3.2 符号常量
    2.3.3 宏指令
    2.3.4 结构
    2.3.5 重复宏指令
    2.3.6 条件宏指令
    2.3.7 处理顺序

    2.3 预处理伪指令

    所有预处理指令在主汇编过程前处理,因此不受控制伪指令的影响。此时所有的注释都已被去除。

    2.3.1 包含源文件

    include伪指令在其使用位置包含指定源文件。它后面必须跟着要包含的文件名,例如:

    include 'macros.inc'

    整个包含文件会在下一行被预处理前预处理。要包含的文件无个数限制。

    文件路径可以包含“%”括起的环境变量,它们将被替换为实际的值,“”和“/”都允许作为地址分隔符。如果没有指定文件的绝对地址,将首先在包含它的那个文件路径下搜索该文件,没有找到的话,将在主源码文件目录搜索(命令行中指定的)。这些规则也适用与file伪指令。

    2.3.2 符号常量

    符号常量不同于数值常量,在汇编过程前所有符号常量都被它们的值替代,它们的值可以为任何东西。

    符号常量定义由常量名后面跟着equ伪指令组成。这条伪指令后面跟着的任何东西都将成为常量的值。如果符号常量的值包含其他符号常量,他们将在赋值给新的常量值前会先替换它们的值。例如:

    d equ dword
    NULL equ d 0
    d equ edx

    在这三个定义后,NULL常量的值为dword 0,d的值为edx。所以“push NULL”将被汇编为“push dword 0”,“push d”将被汇编为“push edx”。然后下面的行:

    d equ d,eax
    常量d将得到新的值“edx,eax”。这种方式可以用来定义增长的符号列表。

    restore伪指令用来恢复上次定义的常量的值。它后面应当跟着一个或多个用逗号分隔的符号常量。所以在上面定义后“restore d”将给常量d恢复到值edx,再来一次“restore d”将恢复至dword,再次恢复d将变成初始时的含义(常量d没有定义)。如果没有定义给定名称的常量,restore不会导致错误,它将忽略掉。

    符号常量可以用来调节汇编器语法。例如下面的定义为尺寸操作符提供了方便的快捷方式:

    b equ byte
    w equ word
    d equ dword
    p equ pword
    f equ fword
    q equ qword
    t equ tword
    x equ dqword

    因为符号常量允许为空值,可以在任何地址值前使用offset语法:

    offset equ

    这样定义后“mov ax,offset char”将拷贝变量char的偏移到寄存器ax,因为offset被替换为空值,因此将被忽略掉。

    符号常量也可以用fix伪指令来定义,它和equ有相同的语法,但定义常量的优先级更高 – 它们甚至在处理预处理伪指令和宏指令前被替换,唯一例外的是fix伪指令本身,它有最高可能的优先级,这点可以用来重新定义常量。

    fix伪指令可以用来调整预处理器伪指令的语法,这通常不能用equ伪指令实现。例如:

    incl fix include

    上面的语句将为include伪指令定义一个短名称,而同样的equ伪指令定义不能得到那样的结果,因为标准的符号常量是在搜索预处理伪指令之后才进行替换的。

    2.3.3 宏指令

    macro伪指令允许定义自己的复杂指令,称为宏指令。宏指令极大的简化编程过程。最简单的形式类似符号常量的定义。例如,下面为指令“test al,0xFF”定义快捷方式。

    macro tst {test al,0xFF}

    macro伪指令之后为宏指令名和以”{” 和”}”括起来的内容。在这个定义后你可以在任何地方使用tst指令,它将被汇编为“test al,0xFF”。定义符号常量tst将有相同的效果,但不同的是宏指令名仅作为指令助记符识别。此外,宏指令在符号常量替换之前被替换相应代码。所以如果你定义了同名的宏指令和符号常量,并且当作指令助记符使用,其内容将被替换为宏指令的,但如果在操作数使用将被替换为符号常量的值。

    宏指令的定义可以有多行组成,因为字符“{”和“}”不必和macro宏指令在同一行,例如:

    macro stos0
    {
      xor al,al
      stosb
    }

    任何使用宏指令stos0的地方将被两条汇编指令替换。

    如同指令需要一些操作数一样,宏指令也可定义为接受一些一些用逗号分隔的参数。需要的参数名称必须跟在同行macro宏指令后面。宏指令中出现的任何参数名都将被替换为宏指令使用时的对应值。下面是一个用于二进制输出格式数据对齐的宏指令:

    macro align value { rb (value-1)-($+value-1) mod value }

    这条宏指令定义后,当发现“align 4”指令后,value将为4,所以结果为“rb (4-1)-($+4-1) mod 4”。

    如果宏指令定义中出现了同名的指令,那么将使用前面定义的含义。这可以用来重定义宏指令,例如:

    macro mov op1,op2
    {
      if op1 in  & op2 in 
        push op2
        pop op1
      else
        mov op1,op2
      end if
    }

    这条宏指令扩展了mov指令的语法,允许两个操作数都为段寄存器。例如“mov ds,es”将被汇编为“push es”和“pop ds”。其他情形下将使用标准的mov指令。这个mov的语法还能进一步的扩展,比如:

    macro mov op1,op2,op3
    {
      if op3 eq
        mov op1,op2
      else
        mov op1,op2
        mov op2,op3
      end if
    }

    它允许mov指令接受三个操作数,但仍然允许两个操作数,因为当宏指令传入比实际需要少的参数时,剩余的参数将为空值。当三个参数都给定时,这条宏指令将变成两个前面定义的宏指令,所以“mov es,ds,dx”将被汇编为“push ds, pop es”和“mov ds,dx”。

    参数名后带有字符“*”表明这个参数是必须的-预处理器不允许这个参数为空值。例如上面的宏指令可以这样声明:“macro mov op1*,op2*,op3”来确保头两个参数必须给定非空值。

    当传给宏指令包含逗号的参数时,参数必须用尖括号括起来。如果包含不止一个字符“<”,应当使用同样数目的字符“>”来结束参数值。

    purge伪指令消除上次定义的宏指令。它必须跟着一个或多个逗号分隔的宏指令名。如果这个宏指令没有定义,将不会产生任何错误。例如上面定义的扩展mov宏指令后,你可以通过“purge mov”宏指令来删除三个操作符的宏指令。另外“purge mov”将删除2个操作符的宏指令,以及所有这些伪指令将没有任何效果。

    如果macro伪指令后存在一些方括号括起的参数名,那么当使用这条宏指令时它允许为这个参数给定多个值。这个宏指令将会依次展开给定参数。这也是为什么中括号后不年能再有任何参数的原因。宏指令的内容将被参数组分别处理。一个简单的包含一个在中括号中的参数例子:

    macro stoschar [char]
    {
      mov al,char
      stosb
    }

    这条宏指令接受无限数目的参数,并且每一个将分别处理成两天指令。例如stoschar 1,2,3将被汇编出以下指令:

    mov al,1
    stosb
    mov al,2
    stosb
    mov al,3
    stosb

    有一些只能在宏指令定义中使用特殊的宏指令。local宏指令定义局部名称,每次宏指令使用时将被替换为唯一值。它必须跟着用逗号分隔的名称。如果参数名以“.”或“..”开头,由每一个计算的宏指令生成的唯一标号将有相同的属性。这通常用来定义在宏指令内部使用的常量和标号。例如:

    macro movstr
    {
      local move
    move:
      lodsb
      stosb
      test al,al
      jnz move
    }

    每次宏指令使用,mov在其指令中将变成唯一的名称,因此不会产生标号重复定义错误。

    forward,reverse和common伪指令把宏指令分成块,每块都在前面的块完成后处理。它们的不同点仅在当宏指令接受多个参数组时。forward伪指令后的指令块将依次处理每个参数组,从第一个到最后一个 – 类似默认的块(前面没有任何这些伪指令)。跟在reverse伪指令的块将以相反的顺序处理 – 从最后一个到第一个。跟在common伪指令的块只处理一次,通常是对于所有的参数组。在处理同组参数时,在某块中定义的局部名字对其他块而言是可见的。当然common块中定义的局部标号对所有块都是可见的,与处理的是哪个参数组无关。

    下面是一个在字符串之前创建字符地址数组的例子::

    macro strtbl name,[string]
    {
      common
      label name dword
      forward
      local label
      dd label
      forward
      label db string,0
    }

    这个宏指令的第一个参数将成为地址表的标号,后面的参数应当为字符串。第一个块仅处理一次,它定义了标号。第二个块为每个字符串声明局部名,并且定义一个表项来存放字符串地址。第三个块用相应的标号名为每个字符串定义数据。

    开始块的伪指令可以和其后的代码放在同一行,比如:

    macro stdcall proc,[arg]
    {
      reverse push arg
      common call proc
    }

    这条宏指令用STDCALL约定来调用过程,堆栈以相反的方向压栈。例如“stdcall foo,1,2,3”将被汇编为:

    push 3
    push 2
    push 1
    call foo

    如果宏指令中一些名称有不同的值(既可以为方括号中的参数,也可以为跟在forward或reverse伪指令块中定义的局部名称)并且用在common伪指令块中,它将被替换为以逗号分隔的所有的值。例如下面的例子把所有参数传递给前面定义的stdcall宏:

    macro invoke proc,[arg]
    { common stdcall [proc],arg }

    它可以用来STDCALL方式间接调用(通过内存指针)过程。

    在宏指令内部也可以使用特殊的#操作符。这个操作符将两个名称连接在一起。由于连接是在所有的参数和局部变量都用真实值代替之后,所以有时可能会很有用。下面的宏指令将根据cond参数生成条件跳转:

    macro jif op1,cond,op2,label
    {
      cmp op1,op2
      j#cond label
    }

    例如“jif ax,ae,10h,exit”将被汇编为“cmp ax,10h”和“jae exit”指令。

    #操作符也能合并两个字符串。宏指令中也可以用’操作符将名称转换为字符串。它转换后面的名称为字符串,但注意,当它后面跟着多个符号宏参数时,只有第一个被转换,也就是说’操作符值转换后面紧跟着的那个符号。下面是使用这两个特性的一个例子:

    macro label name
    {
      label name
      if ~ used name
        display ‘name # " is defined but not used.",13,10
      end if
    }

    以这种方式定义的宏在源文件中未使用的时候,宏就会发出警告信息,通知哪个标号未使用。

    为使宏能够根据参数类型的不同其行为有所不同,可使用”eqtype”比较操作符。下面是一个区分字符串和其他参数的宏指令:

    macro message arg
    {
      if arg eqtype ""
        local str
        jmp @f
        str db arg,0Dh,0Ah,24h
        @@:
        mov dx,str
      else
        mov dx,arg
      end if
      mov ah,9
      int 21h
    }

    上面的宏用于DOS程序中显示信息。当这个宏的参数为一些数字,标号,或变量,将会显示该地址的字符串,但当参数为字符串时,创建的代码将显示后面包含回车和换行字符的字符串。

    宏指令中也可以在另一个宏指令声明,这样可以使用一个宏定义另外一个宏,但这样做存在一个问题宏指令中不能出现字符“}”,因为它表示宏定义的结束。为了解决这个问题,可以在宏指令内部使用转义字符。通过在任何符号(甚至特殊字符)前放置一个或多个字符“”。预处理将它们作为一个单一的符号处理,每次在处理宏指令时遇到这些符号,都将取出前面的字符“”。例如“}”被当作一单一符号,当处理宏指令时变成了符号“}”。这允许在一个宏指令定义中嵌套另一个:

    macro ext instr
    {
      macro instr op1,op2,op3
      {
        if op3 eq
          instr op1,op2
        else
          instr op1,op2
          instr op2,op3
        end if
      }
    }
    ext add
    ext sub

    ext宏定义正确,当使用它时,“{”和“}”变成了符号“{”和“}”。所以当处理ext add时,宏的内容变成有效的宏指令定义,add宏将被定义。同样ext sub定义了sub宏。这里使用符号“{”并不是必须的,但这样做可以让定义更清晰些。

    如果某些只用于宏指令的伪指令,比如local或common也需要在以这种方式嵌套在宏中,它们也可以以同种方式转义。用多于一个的“”转义符号也是允许的,这允许用来定义多层嵌套的宏。

    另一种在一个宏指令中定义另一个的技术是fix伪指令,当某些宏指令仅以另一个的定义开始时,而没有结束它时将变得很有用处。例如:

    macro tmacro [params]
    {
    common macro params {
    }
    MACRO fix tmacro
    ENDM fix }

    定义了另一种定义宏指令的语法,如同:

    MACRO stoschar char
      mov al,char
      stosb
    ENDM

    注意,这样个性化的定义必须使用”fix”来定义,因为只有更高优先级的常量才能在预处理器在定义宏时查找”}”字符之前处理。这对于那些想在定义结束之后执行一些额外操作的时候可能是一个问题,但还有一个特性可能有助于这个问题的处理。也就是说可以将任何伪指令、指令或宏指令放在结束宏的”}”字符之后,这就会使得他像被放到了下一行一样进行处理。

    2.3.4 结构

    struc伪指令是用于定义数据的”macro”的变例。使用”struc” 定义的宏在使用时前面必需加一个标号(和数据定义伪指令一样),这个标号会附加在所有以”.”开头的元素名称前面。以”struc”定义的宏和使用”macro”定义的宏名字可以相同,不会相互影响。所有适用于”macro”的规则也同样适用于”struc”。以下是一个结构宏指令的例子

    下面为一个结构宏指令例子:

    struc point x,y
    {
      .x dw x
      .y dw y
    }

    例如my point 7,11将定义结构标号my,包含两个值为7的变量my.x和值为11的变量my.y。

    如果在结构定义中出现字符“.”,它将被替换为结构的标号名,而且此时不会自动定义该标号,以允许完全的手动定义。下面为利用这个特性来扩展数据定义伪指令db以能够计算定义数据的大小例子:

    struc db [data]
    {
      common
      . db data
      .size = $ - .
    }

    这样定义后,“msg db ‘Hello!’,13,10”还将定义“msg.size”常量,其值等于定义数据占用的字节大小。

    定义寄存器或绝对值寻址的数据结构应当使用和结构宏指令一起使用的virtual伪指令(见2.2.5)。

    restruc伪指令消除上次的结构定义,如同purge处理宏指令,restore处理符号常量一样。它也有相同的语法 – 跟着一个或多个逗号分隔的结构宏指令。

    2.3.5 重复宏指令

    rept伪指令是特殊种类的宏指令,用来重复用括号括起来的块到指定数目。rept伪指令的基本语法是跟着数字(不能为表达式,因为预处理器不做计算,如果需要重复的值基于汇编器计算,使用汇编器处理的代码重复指令,见2.2.3),然后是字符“{”和“}”之间的源码块。一个简单的例子:

    rept 5 {in al, dx}

    将变成5份重复的in al,dx行。和可以在宏指令内部使用的标准宏指令和任何特殊操作符和伪指令同样方式定义的指令块也允许使用。当重复次数为0时,块将被简单的跳过,如果你定义宏指令但从不使用它。重复次数可以跟着重复次数符号名称,它将被替换为当前生成的重复次数。如同:

    rept 3 counter
    {
      byte#counter db counter
    }

    将生成:

    byte1 db 1
    byte2 db 2
    byte3 db 3

    应用于rept块的重复机制和宏指令中处理多个参数组的相同,所以像forward,common和reverse伪指令都可以使用。因此伪指令:

    rept 7 num { reverse display ‘num }

    将显示数字7到1为文本。local伪指令的行为和带有多个参数组的伪指令中的相同,所以:

    rept 21
    {
      local label
      label: loop label
    }

    将为每次重复生成唯一的标号。

    重复次数符号默认次数为1,你可以在计数器名称后面跟着冒号和起始次数来改变默认的起始次数。例如:

    rept 8 n:0 { pxor xmm#n,xmm#n }

    生成的代码将 清除8个SSE寄存器的值。你可以用逗号来定义多个计数器,每一个都可以有不同的起始计数。

    irp伪指令通过给定的列表参数来逐一重复。irp后面跟着参数名,然后是逗号和一串参数。参数的指定类似宏指令中的方式,所以他们可以以逗号分隔并且每一个都可以包含在“<”和“>”之间。另外 参数名称也可以跟着字符“*”来说明某个参数不能传空值。这样的块:

    irp value, 2,3,5
    { db value }

    将生成:

    db 2
    db 3
    db 5

    irps伪指令从给定列表符号中重复,它必须跟着参数名,然后逗号和任何符号序列。序列中的每一个符号,无论是否为名称符号,符号字符或字符串,都将成为一次重复的参数。如果逗号后没有任何符号,将不会执行任何重复。例如:

    irps reg, al bx ecx
    { xor reg,reg }

    将生成:

    xor al,al
    xor bx,bx
    xor ecx,ecx

    irp和irps定义的块将和任何宏指令以相同方式处理,所以只用于宏指令的操作符和伪指令此时也可以自由使用。

    2.3.6 条件宏指令

    match伪指令将引起一些块被预处理,而且仅当给定符号序列匹配指定模式时传给汇编器。模式在前,以逗号结束,然后是执行匹配操作的符号,最后是在“{”和“}”之间的块源码。

    有几条规则用来创建匹配表达式,第一是任何符号字符和字符串应当精确匹配。例如:

    match +,+ { include ’first.inc’ }
    match +,- { include ’second.inc’ }

    第一个文件将被包含,因为逗号后的“+”匹配模式“+”,第二个文件将不会包含,因为不匹配。

    为了照字面意思匹配,模板前面必须放置字符“=”,为了匹配字符“=”,或“,”,必须使用“==”和“=,”。例如模板“=a==”就匹配“a=”序列。

    如果在模板中放置一些名称符号,它匹配其中至少一个符号的任何序列,并且这个名称将被替换为匹配序列,类似宏指令的参数。例如:

    match a-b, 0-7
    { dw a,b-a }

    将生成dw 0,7-0指令。每一个名称都将匹配为尽可能小的符号,留下剩余做为后面的一个,所以在这个例子中:

    match a b, 1+2+3 {db a}

    名称a匹配符号1,剩下+2+3序列来匹配b。而:

    match a b, 1 {db a}

    将不会为b剩下任何东西匹配,所以这个块将不会处理任何事情。

    match定义的源码块将和宏指令以相同方式处理,所以宏指令的任何操作符也都能在这里使用。

    使得match伪指令更加有用的事实是执行匹配前在匹配序列符号中(逗号后源码块开始前的任意地方)它替换符号常量为它们的值。这样可以用来在一些符号常量给定了值的某些条件下处理源码块:

    match =TRUE, DEBUG { include ’debug.inc’ }

    当定义了符号常量DEBUG为TRUE时将包含文件。

    2.3.7 处理顺序

    当组合各种不同的预处理器特性时,知道它们处理的先后顺序是很重要的。正如上面已经提到的,最高优先级为fix伪指令和用它替换定义的。在做任何预处理前完成的,一次下面的源码:

    V fix {
    macro empty
    V
    V fix }
    V

    成为一个有效的空宏指令定义。它可以被解析为:fix伪指令和有优先级的的符号常量在单独阶段处理,所有其他的预处理将在此后的源码中完成。

    标准的预处理在后面,在每个识别为别的第一个符号开头。它从检查预处理伪指令开始,当没有发现时,预处理将检查是否第一个符号为伪指令。如果没有发现伪指令,它移动到第二行的符号,再一次的开始检查伪指令,此时只有equ伪指令,如同在第二行符号只有一个。如果没有伪指令,第二个符号将被检查结构宏指令,当所有这些都没有检查出正值时,符号常量将被替换为他们的值,并且这些行将传给汇编器。

    为了在例子中验证这点,假设有存在一个已定义的宏指令foo和结构宏指令bar。下面的行:

    foo equ
    foo bar

    将都被解析为宏指令foo调用,因为第一个符号的意思将覆盖第二的。

    宏指令从它们的定义块生成新的行,用参数的值替换参数,然后处理符号中的转义字符“#”“’”。转换操作符比连接操作符有更高优先级,如果它们包含转义字符,在完成处理前转义字符将被忽略。当这完成时,新生成的行将执行上面描述的标准预处理。

    虽然符号常见通常仅在行中替换,当即没有预处理伪指令也没有宏指令发现时,在这里包含伪指令的替换位置有一些特例。第一是符号常量的定义,在equ关键词后的任何地方完成的的替换,结果将被赋给新的常量(见2.3.2)。第二中情形是match伪指令,替换将在匹配模板前逗号后面的符号完成。这些特性可以用来维护列表,如下面的定义:

    list equ
    macro append item
    {
      match any, list { list equ list,item }
      match , list { list equ item }
    }

    list常量将被初始化为空值,append宏指令可以增加新的以逗号分隔的项到列表。这个宏指令第一个匹配仅发生在当它们列表值不为空(见2.3.6),此时新的值为上一次值并且新的值将被添加到后面。第二个匹配只当列表不为空时发生,并且此时列表定义中只包含新值。所以从空表开始,append 1将定义list equ 1,后面跟着的append 2将定义list equ 1,2。作为某些伪指令的参数也可以使用。但它不能在直接使用 – 如果foo为伪指令,foo list将作为宏参数传给符号list,因为符号常量此阶段还没有回滚。

    match params, list {foo params}

    如果list值不为空,和params匹配,当有括号块中定义的新行将会稍后被替换为匹配值。所以如果list含有值1,2,上面的行将生产包含foo 1,2的行,这些都会稍后进入标准处理。

    还有一个特殊情形 – 当预处理检查行中第二个参数为冒号(如同标号定义那样由汇编器解析),它在当前位置停止并完成第一个符号的预处理(所以如果它是符号常量它将回滚),如果它仍为标号,它将从标号后位置执行标准预处理。这允许在标号后放置预处理伪指令和宏指令,类似汇编器处理指令和伪指令,例如:

    start: include 'start.inc'

    然而如果在预处理时断掉(例如当为空的符号常量时),只剩余行中的符号常量将继续。

    它必须记住,预处理执行的工作首先为文本符号,他们在主汇编过程前有一个简单的单趟处理。预处理结果后的文本将传给汇编器,然后汇编器将进行多趟操作。因此仅有汇编器识别和处理的控制伪指令 – 它们取决于数值可能在不同趟中改变 – 不能有汇编器以任何方式扫描并且影响预处理。考虑下面的例子:

    if 0
      a = 1
      b equ 2
    end if
    dd b

    当预处理上面的语句,唯一有预处理社别的伪指令为equ,它定义了符号常量b,稍后再源码中符号b的值被替换为2.除了这个替换外,对于汇编器其他航没有变化。当预处理后,上面源码将变成:

    if 0
      a = 1
    end if
    dd 2

    此时当汇编器传入它是,if的条件为false,常量a没有定义。然后符号常量b将做普通处理,即使它的定义放在a之后。所以每次你必须非常小心的混合伪指令和汇编器特性 – 经常想象你的源码在预处理后将变成什么,汇编器将看到的,以及它的多遍扫描。

  1. 建议:
    User:
Built on: 2021-5-12 14:12:42
Copyright © 2006-2008 xuyibo.org All rights reserved.