第四章 NASM预处理器。 -------------------------------- NASM拥有一个强大的宏处理器,它支持条件汇编,多级文件包含,两种形式的 宏(单行的与多行的),还有为更强大的宏能力而设置的‘context stack'机制 预处理指令都是以一个'%'打头。 预处理器把所有以反斜杠(/)结尾的连续行合并为一行,比如: %define THIS_VERY_LONG_MACRO_NAME_IS_DEFINED_TO / THIS_value 、 会像是单独一行那样正常工作。 4.1 单行的宏。 4.1.1 最常用的方式: `%define' 单行的宏是以预处理指令'%define'定义的。定义工作同C很相似,所以你可 以这样做: %define ctrl 0x1F & %define param(a,b) ((a)+(a)*(b)) mov byte [param(2,ebx)], ctrl 'D' 会被扩展为: mov byte [(2)+(2)*(ebx)], 0x1F & 'D' 当单行的宏被扩展开后还含有其它的宏时,展开工作会在执行时进行,而不是 定义时,如下面的代码: %define a(x) 1+b(x) %define b(x) 2*x mov ax,a(8) 会如预期的那样被展开成'mov ax, 1+2*8', 尽管宏'b'并不是在定义宏a 的时候定义的。 用'%define'定义的宏是大小写敏感的:在代码'%define foo bar'之后,只有 'foo'会被扩展成'bar':'Foo'或者'FOO'都不会。用'%idefine'来代替'%define' (i代表'insensitive'),你可以一次定义所有的大小写不同的宏。所以 '%idefine foo bar'会导致'foo','FOO','Foo'等都会被扩展成'bar'。 当一个嵌套定义(一个宏定义中含有它本身)的宏被展开时,有一个机制可以 检测到,并保证不会进入一个无限循环。如果有嵌套定义的宏,预处理器只 会展开第一层,因此,如果你这样写: %define a(x) 1+a(x) mov ax,a(3) 宏 `a(3)'会被扩展成'1+a(3)',不会再被进一步扩展。这种行为是很有用的,有 关这样的例子请参阅8.1。 你甚至可以重载单行宏:如果你这样写: %define foo(x) 1+x %define foo(x,y) 1+x*y 预处理器能够处理这两种宏调用,它是通过你传递的参数的个数来进行区分的, 所以'foo(3)'会变成'1+3',而'foo(ebx,2)'会变成'1+ebx*2'。尽管如此,但如果 你定义了: %define foo bar 那么其他的对'foo'的定义都不会被接受了:一个不带参数的宏定义不允许 对它进行带有参数进行重定义。 但这并不能阻止单行宏被重定义:你可以像这样定义,并且工作得很好: %define foo bar 然后在源代码文件的稍后位置重定义它: %define foo baz 然后,在引用宏'foo'的所有地方,它都会被扩展成最新定义的值。这在用 '%assign'定义宏时非常有用(参阅4.1.5) 你可以在命令行中使用'-d'选项来预定义宏。参阅2.1.11 4.1.2 %define的增强版: `%xdefine' 与在调用宏时展开宏不同,如果想要调用一个嵌入有其他宏的宏时,使用 它在被定义的值,你需要'%define'不能提供的另外一种机制。解决的方案 是使用'%xdefine',或者它的大小写不敏感的形式'%xidefine'。 假设你有下列的代码: %define isTrue 1 %define isFalse isTrue %define isTrue 0 val1: db isFalse %define isTrue 1 val2: db isFalse 在这种情况下,'val1'等于0,而'val2'等于1。这是因为,当一个单行宏用 '%define'定义时,它只在被调用时进行展开。而'isFalse'是被展开成 'isTrue',所以展开的是当前的'isTrue'的值。第一次宏被调用时,'isTrue' 是0,而第二次是1。 如果你希望'isFalse'被展开成在'isFalse'被定义时嵌入的'isTrue'的值, 你必须改写上面的代码,使用'%xdefine': %xdefine isTrue 1 %xdefine isFalse isTrue %xdefine isTrue 0 val1: db isFalse %xdefine isTrue 1 val2: db isFalse 现在每次'isFalse'被调用,它都会被展开成1,而这正是嵌入的宏'isTrue' 在'isFalse'被定义时的值。 4.1.3 : 连接单行宏的符号: `%+' 一个单行宏中的单独的记号可以被连接起来,组成一个更长的记号以 待稍后处理。这在很多处理相似的事情的相似的宏中非常有用。 举个例子,考虑下面的代码: %define BDASTART 400h ; Start of BIOS data area struc tBIOSDA ; its structure .COM1addr RESW 1 .COM2addr RESW 1 ; ..and so on endstruc 现在,我们需要存取tBIOSDA中的元素,我们可以这样: mov ax,BDASTART + tBIOSDA.COM1addr mov bx,BDASTART + tBIOSDA.COM2addr 如果在很多地方都要用到,这会变得非常的繁琐无趣,但使用下面 的宏会大大减小打字的量: ; Macro to access BIOS variables by their names (from tBDA): %define BDA(x) BDASTART + tBIOSDA. %+ x 现在,我们可以象下面这样写代码: mov ax,BDA(COM1addr) mov bx,BDA(COM2addr) 使用这个特性,我们可以简单地引用大量的宏。(另外,还可以减少打 字错误)。 4.1.4 取消宏定义: `%undef' 单行的宏可以使用'%undef'命令来取消。比如,下面的代码: %define foo bar %undef foo mov eax, foo 会被展开成指令'mov eax, foo',因为在'%undef'之后,宏'foo'处于无定义 状态。 那些被预定义的宏可以通过在命令行上使用'-u'选项来取消定义,参阅 2.1.12。 4.1.5 预处理器变量 : `%assign' 定义单行宏的另一个方式是使用命令'%assign'(它的大小写不敏感形式 是%iassign,它们之间的区别与'%idefine','%idefine'之间的区别完全相 同)。 '%assign'被用来定义单行宏,它不带有参数,并有一个数值型的值。它的 值可以以表达式的形式指定,并要在'%assing'指令被处理时可以被一次 计算出来, 就像'%define','%assign'定义的宏可以在后来被重定义,所以你可以这 样做: %assign i i+1 以此来增加宏的数值 '%assing'在控制'%rep'的预处理器循环的结束条件时非常有用:请参 阅4.5的例子。另外的关于'%assign'的使用在7.4和8.1中的提到。 赋给'%assign'的表达式也是临界表达式(参阅3.8),而且必须可被计算 成一个纯数值型(不能是一个可重定位的指向代码或数据的地址,或是包 含在寄存器中的一个值。) 4.2 字符串处理宏: `%strlen' and `%substr' 在宏里可以处理字符串通常是非常有用的。NASM支持两个简单的字符 串处理宏,通过它们,可以创建更为复杂的操作符。 4.2.1 求字符串长度: `%strlen' '%strlen'宏就像'%assign',会为宏创建一个数值型的值。不同点在于 '%strlen'创建的数值是一个字符串的长度。下面是一个使用的例子: %strlen charcnt 'my string' 在这个例子中,'charcnt'会接受一个值8,就跟使用了'%assign'一样的 效果。在这个例子中,'my string'是一个字面上的字符串,但它也可以 是一个可以被扩展成字符串的单行宏,就像下面的例子: %define sometext 'my string' %strlen charcnt sometext 就像第一种情况那样,这也会给'charcnt'赋值8 4.2.2 取子字符串: `%substr' 字符串中的单个字符可以通过使用'%substr'提取出来。关于它使用的 一个例子可能比下面的描述更为有用: %substr mychar 'xyz' 1 ; equivalent to %define mychar 'x' %substr mychar 'xyz' 2 ; equivalent to %define mychar 'y' %substr mychar 'xyz' 3 ; equivalent to %define mychar 'z' 在这个例子中,mychar得到了值'z'。就像在'%strlen'(参阅4.2.1)中那样, 第一个参数是一个将要被创建的单行宏,第二个是字符串,第三个参数 指定哪一个字符将被选出。注意,第一个索引值是1而不是0,而最后一 个索引值等同于'%strlen'给出的值。如果索引值超出了范围,会得到 一个空字符串。 4.3 多行宏: `%macro' 多行宏看上去更象MASM和TASM中的宏:一个NASM中定义的多行宏看上去就 象下面这样: %macro prologue 1 push ebp mov ebp,esp sub esp,%1 %endmacro 这里,定义了一个类似C函数的宏prologue:所以你可以通过一个调用来使 用宏: myfunc: prologue 12 这会把三行代码扩展成如下的样子: myfunc: push ebp mov ebp,esp sub esp,12 在'%macro'一行上宏名后面的数字'1'定义了宏可以接收的参数的个数。 宏定义里面的'%1'是用来引用宏调用中的第一个参数。对于一个有多 个参数的宏,参数序列可以这样写:'%2','%3'等等。 多行宏就像单行宏一样,也是大小写敏感的,除非你使用另一个操作符 ‘%imacro' 如果你必须把一个逗号作为参数的一部分传递给多行宏,你可以把整 个参数放在一个括号中。所以你可以象下面这样编写代码: %macro silly 2 %2: db %1 %endmacro silly 'a', letter_a ; letter_a: db 'a' silly 'ab', string_ab ; string_ab: db 'ab' silly {13,10}, crlf ; crlf: db 13,10 4.3.1 多行宏的重载 就象单行宏,多行宏也可以通过定义不同的参数个数对同一个宏进行多次 重载。而这次,没有对不带参数的宏的特殊处理了。所以你可以定义: %macro prologue 0 push ebp mov ebp,esp %endmacro 作为函数prologue的另一种形式,它没有开辟本地栈空间。 有时候,你可能需要重载一个机器指令;比如,你可能想定义: %macro push 2 push %1 push %2 %endmacro 这样,你就可以如下编写代码: push ebx ; this line is not a macro call push eax,ecx ; but this one is 通常,NASM会对上面的第一行给出一个警告信息,因为'push'现在被定义成 了一个宏,而这行给出的参数个数却不符合宏的定义。但正确的代码还是 会生成的,仅仅是给出一个警告而已。这个警告信息可以通过 '-w'macro-params’命令行选项来禁止。(参阅2.1.17)。 4.3.2 Macro-Local Labels NASM允许你在多行宏中定义labels.使它们对于每一个宏调用来讲是本地的:所 以多次调用同一个宏每次都会使用不同的label.你可以通过在label名称前面 加上'%%'来实现这种用法.所以,你可以创建一条指令,它可以在'Z'标志位被 设置时执行'RET'指令,如下: %macro retz 0 jnz %%skip ret %%skip: %endmacro 你可以任意多次的调用这个宏,在你每次调用时的时候,NASM都会为'%%skip' 建立一个不同的名字来替换它现有的名字.NASM创建的名字可能是这个样子 的:'..@2345.skip',这里的数字2345在每次宏调用的时候都会被修改.而 '..@'前缀防止macro-local labels干扰本地labels机制,就像在3.9中所 描述的那样.你应该避免在定义你自己的宏时使用这种形式('..@'前缀,然后 是一个数字,然后是一个句点),因为它们会和macro-local labels相互产生 干扰. 4.3.3 不确定的宏参数个数. 通常,定义一个宏,它可以在接受了前面的几个参数后, 把后面的所有参数都 作为一个参数来使用,这可能是非常有用的,一个相关的例子是,一个宏可能 用来写一个字符串到一个MS-DOS的文本文件中,这里,你可能希望这样写代码: writefile [filehandle],"hello, world",13,10 NASM允许你把宏的最后一个参数定义成"贪婪参数", 也就是说你调用这个宏时 ,使用了比宏预期得要多得多的参数个数,那所有多出来的参数连同它们之间 的逗号会被作为一个参数传递给宏中定义的最后一个实参,所以,如果你写: %macro writefile 2+ jmp %%endstr %%str: db %2 %%endstr: mov dx,%%str mov cx,%%endstr-%%str mov bx,%1 mov ah,0x40 int 0x21 %endmacro 那上面使用'writefile'的例子会如预期的那样工作:第一个逗号以前的文本 [filehandle]会被作为第一个宏参数使用,会被在'%1'的所有位置上扩展,而 所有剩余的文本都被合并到'%2'中,放在db后面. 这种宏的贪婪特性在NASM中是通过在宏的'%macro'一行上的参数个数后面加 上'+'来实现的. 如果你定义了一个贪婪宏,你就等于告诉NASM对于那些给出超过实际需要的参 数个数的宏调用该如何扩展; 在这种情况下,比如说,NASM现在知道了当它看到 宏调用'writefile'带有2,3或4个或更多的参数的时候,该如何做.当重载宏 时,NASM会计算参数的个数,不允许你定义另一个带有4个参数的'writefile' 宏. 当然,上面的宏也可以作为一个非贪婪宏执行,在这种情况下,调用语句应该 象下面这样写: writefile [filehandle], {"hello, world",13,10} NASM提供两种机制实现把逗号放到宏参数中,你可以选择任意一种你喜欢的 形式. 有一个更好的办法来书写上面的宏,请参阅5.2.1 4.3.4 缺省宏参数. NASM可以让你定义一个多行宏带有一个允许的参数个数范围.如果你这样做了, 你可以为参数指定缺省值.比如: %macro die 0-1 "Painful program death has occurred." writefile 2,%1 mov ax,0x4c01 int 0x21 %endmacro 这个宏(它使用了4.3.3中定义的宏'writefile')在被调用的时候可以有一个 错误信息,它会在退出前被显示在错误输出流上,如果它在被调用时不带参数 ,它会使用在宏定义中的缺省错误信息. 通常,你以这种形式指定宏参数个数的最大值与最小值; 最小个数的参数在 宏调用的时候是必须的,然后你要为其他的可选参数指定缺省值.所以,当一 个宏定义以下面的行开始时: %macro foobar 1-3 eax,[ebx+2] 它在被调用时可以使用一到三个参数, 而'%1'在宏调用的时候必须指定,'%2' 在没有被宏调用指定的时候,会被缺省地赋为'eax','%3'会被缺省地赋为 '[ebx+2]'. 你可能在宏定义时漏掉了缺省值的赋值, 在这种情况下,参数的缺省值被赋为 空.这在可带有可变参数个数的宏中非常有用,因为记号'%0'可以让你确定有 多少参数被真正传给了宏. 这种缺省参数机制可以和'贪婪参数'机制结合起来使用;这样上面的'die'宏 可以被做得更强大,更有用,只要把第一行定义改为如下形式即可: %macro die 0-1+ "Painful program death has occurred.",13,10 最大参数个数可以是无限,以'*'表示.在这种情况下,当然就不可能提供所有 的缺省参数值. 关于这种用法的例子参见4.3.6. 4.3.5 `%0': 宏参数个数计数器. 对于一个可带有可变个数参数的宏, 参数引用'%0'会返回一个数值常量表示 有多少个参数传给了宏.这可以作为'%rep'的一个参数(参阅4.5),以用来遍历 宏的所有参数. 例子在4.3.6中给出. 4.3.6 `%rotate': 循环移动宏参数. Unix的shell程序员对于'shift' shell命令再熟悉不过了,它允许把传递给shell 脚本的参数序列(以'$1,'$2'等引用)左移一个,所以, 前一个参数是‘$1'的话 左移之后,就变成’$2'可用了,而在'$1'之前是没有可用的参数的。 NASM具有相似的机制,使用'%rotate'。就象这个指令的名字所表达的,它跟Unix 的'shift'是不同的,它不会让任何一个参数丢失,当一个参数被移到最左边的 时候,再移动它,它就会跳到右边。 '%rotate'以单个数值作为参数进行调用(也可以是一个表达式)。宏参数被循环 左移,左移的次数正好是这个数字所指定的。如果'%rotate'的参数是负数,那么 宏参数就会被循环右移。 所以,一对用来保存和恢复寄存器值的宏可以这样写: %macro multipush 1-* %rep %0 push %1 %rotate 1 %endrep %endmacro 这个宏从左到右为它的每一个参数都依次调用指令'PUSH'。它开始先把它的 第一个参数'%1'压栈,然后调用'%rotate'把所有参数循环左移一个位置,这样 一来,原来的第二个参数现在就可以用'%1'来取用了。重复执行这个过程, 直到所有的参数都被执行完(这是通过把'%0'作为'%rep'的参数来实现的)。 这就实现了把每一个参数都依次压栈。 注意,'*'也可以作为最大参数个数的一个计数,表明你在使用宏'multipush'的 时候,参数个数没有上限。 使用这个宏,确实是非常方便的,执行同等的'POP'操作,我们并不需要把参数 顺序倒一下。一个完美的解决方案是,你再写一个'multipop'宏调用,然后把 上面的调用中的参数复制粘贴过来就行了,这个宏会对所有的寄存器执行相反 顺序的pop操作。 这可以通过下面定义来实现: %macro multipop 1-* %rep %0 %rotate -1 pop %1 %endrep %endmacro 这个宏开始先把它的参数循环右移一个位置,这样一来,原来的最后一个参数 现在可以用'%1'引用了。然后被pop,然后,参数序列再一次右移,倒数第二个 参数变成了'%1',就这样,所以参数被以相反的顺序一一被执行。 4.3.7 连结宏参数。 NASM可以把宏参数连接到其他的文本中。这个特性可以让你声明一个系例 的符号,比如,在宏定义中。你希望产生一个关于关键代码的表格,而代码 跟在表中的偏移值有关。你可以这样编写代码: %macro keytab_entry 2 keypos%1 equ $-keytab db %2 %endmacro keytab: keytab_entry F1,128+1 keytab_entry F2,128+2 keytab_entry Return,13 会被扩展成: keytab: keyposF1 equ $-keytab db 128+1 keyposF2 equ $-keytab db 128+2 keyposReturn equ $-keytab db 13 你可以很轻易地把文本连接到一个宏参数的尾部,这样写即可:'%1foo'。 如果你希望给宏参数加上一个数字,比如,通过传递参数'foo'来定义符 号'foo1'和'foo2',但你不能写成'%11',因为这会被认为是第11个参数。 你必须写成'%{1}1',它会把第一个1跟第二个分开 这个连结特性还可以用于其他预处理问题中,比如macro-local labels(4.3.2) 和context-local labels(4.7.2)。在所有的情况中,语法上的含糊不清都可以 通过把'%'之后,文本之前的部分放在一个括号中得到解决:所以'%{%foo}bar 会把文本'bar'连接到一个macro-local label:’%%foo'的真正名字的后面(这 个是不必要的,因为就NASM处理macro-local labels的机制来讲,'%{%foo}bar 和%%foobar都会被扩展成同样的形式,但不管怎么样,这个连结的能力是在的) 4.3.8 条件代码作为宏参数。 NASM对于含有条件代码的宏参数会作出特殊处理。你可以以另一种形式 '%+1'来使用宏参数引用'%1',它告诉NASM这个宏参数含有一个条件代码, 如果你调用这个宏时,参数中没有有效的条件代码,会使预处理器报错。 为了让这个特性更有用,你可以以'%-1'的形式来使用参数,它会让NASM把 这个条件代码扩展成它的反面。所以4.3.2中定义的宏'retz'还可以以 下面的方式重写: %macro retc 1 j%-1 %%skip ret %%skip: %endmacro 这个指令可以使用'retc ne'来进行调用,它会把条件跳转指令扩展成'JE', 或者'retc po'会把它扩展成'JPE'。 '%+1'的宏参数引用可以很好的把参数'CXZ'和'ECXZ'解释为有效的条件 代码;但是,'%-1'碰上上述的参数就会报错,因为这两个条件代码没有相 反的情况存在。 4.3.9 禁止列表扩展。 当NASM为你的源程序产生列表文件的时候,它会在宏调用的地方为你展开 多行宏,然后列出展开后的所有行。这可以让你看到宏中的哪些指令展 开成了哪些代码;尽管如此,有些不必要的宏展开会把列表弄得很混乱。 NASM为此提供了一个限定符'.nolist',它可以被包含在一个宏定义中,这 样,这个宏就不会在列表文件中被展开。限定符'.nolist'直接放到参数 的后面,就像下面这样: %macro foo 1.nolist 或者这样: %macro bar 1-5+.nolist a,b,c,d,e,f,g,h 4.4 条件汇编 跟C预处理器相似,NASM允许对一段源代码只在某特定条件满足时进行汇编, 关于这个特性的语法就像下面所描述的: %if<condition> ;if <condition>满足时接下来的代码被汇编。 %elif<condition2> ; 当if<condition>不满足,而<condition2>满足时,该段代码被汇编。 %else ;当<condition>跟<condition2>都不满足时,该段代码被汇编。 %endif '%else'跟'%elif'子句都是可选的,你也可以使用多于一个的'%elif'子句。 4.4.1 `%ifdef': 测试单行宏是否存在。 '%ifdef MACRO'可以用来开始一个条件汇编块,跟在它后面的代码当且仅 当一个叫做'MACRO'单行宏被定义时才会被会汇编。如果没有定义,那么 '%elif'和'%else'块会被处理。 比如,当调试一个程序时,你可能希望这样写代码: ; perform some function %ifdef DEBUG writefile 2,"Function performed successfully",13,10 %endif ; go and do something else 你可以通过使用命令行选项'-dDEBUG'来建立一个处理调试信息的程序,或 不使用该选项来产生最终发布的程序。 你也可以测试一个宏是否没有被定义,这可以使用'%ifndef'。你也可以在 '%elif'块中测试宏定义,使用'%elifdef'和'%elifndef'即可。 4.4.2 `ifmacro': 测试多行宏是否存在。 除了是测试多行宏的存在的,'%idmacro'操作符的工作方式跟'%ifdef'是一 样的。 比如,你可能在编写一个很大的工程,而且无法控制存在链接库中的宏。你 可能需要建立一个宏,但必须先确保这个宏没有被建立过,如果被建立过了, 你需要为你的宏换一个名字。 如果你定义的一个有特定参数个数与宏名的宏与现有的宏会产生冲突,那么 '%ifmacro'会返回真。比如: %ifmacro MyMacro 1-3 %error "MyMacro 1-3" causes a conflict with an existing macro. %else %macro MyMacro 1-3 ; insert code to define the macro %endmacro %endif 如果没有现有的宏会产生冲突,这会建立一个叫'MyMacro 1-3"的宏,如果 会有冲突,那么就产生一条警告信息。 你可以通过使用'%ifnmacro'来测试是否宏不存在。还可以使用'%elifmacro' 和'%elifnmacro'在'%elif'块中测试多行宏。 4.4.3 `%ifctx': 测试上下文栈。 当且仅当预处理器的上下文栈中的顶部的上下文的名字是'ctxname'时,条 件汇编指令'%ifctx ctxname'会让接下来的语句被汇编。跟'%ifdef'一样, 它也有'%ifnctx','%elifctx','%elifnctx'等形式。 关于上下文栈的更多细节,参阅4.7, 关于'%ifctx'的一个例子,参阅4.7.5. 4.4.4 `%if': 测试任意数值表达式。 当且仅当数值表达式'expr'的值为非零时,条件汇编指令'%if expr'会让接 下来的语句被汇编。使用这个特性可以确定何时中断一个'%rep'预处理器循 环,例子参阅4.5。 '%if'和'%elif'的表达式是一个临界表达式(参阅3.8) '%if' 扩展了常规的NASM表达式语法,提供了一组在常规表达式中不可用的 相关操作符。操作符'=','<','>','<=','>='和'<>'分别测试相等,小于,大 于,小于等于,大于等于,不等于。跟C相似的形式'=='和'!='作为'=','<>' 的另一种形式也被支持。另外,低优先级的逻辑操作符'&&','^^',和'||'作 为逻辑与,逻辑异或,逻辑或也被支持。这些跟C的逻辑操作符类似(但C没 有提供逻辑异或),这些逻辑操作符总是返回0或1,并且把任何非零输入看 作1(所以,比如, '^^'它会在它的一个输入是零,另一个非零的时候,总 返回1)。这些操作符返回1作为真值,0作为假值。 4.4.5 `%ifidn' and `%ifidni': 测试文本相同。 当且仅当'text1'和'text2'在作为单行宏展开后是完全相同的一段文本时, 结构'%ifidn text1,text2'会让接下来的一段代码被汇编。两段文本在空格 个数上的不同会被忽略。 '%ifidni'和'%i |
|