继续转载……
NASM中文手册(4 - 5章)
--------------------------------
第四章 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
;if 满足时接下来的代码被汇编。
%elif
; 当if不满足,而满足时,该段代码被汇编。
%else
;当跟都不满足时,该段代码被汇编。
%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'和'%ifidn'相似,但是大小写不敏感。
比如,下面的宏把一个寄存器或数字压栈,并允许你把IP作为一个真实的寄
存器使用:
%macro pushparam 1
%ifidni %1,ip
call %%label
%%label:
%else
push %1
%endif
%endmacro
就像大多数的'%if'结构,'%ifidn'也有一个'%elifidn',并有它的反面的形
式'%ifnidn','%elifnidn'.相似的,'%ifidni'也有'%elifidni',`%ifnidni'
和`%elifnidni'。
4.4.6 `%ifid', `%ifnum', `%ifstr': 测试记号的类型。
有些宏会根据传给它们的是一个数字,字符串或标识符而执行不同的动作。
比如,一个输出字符串的宏可能会希望能够处理传给它的字符串常数或一
个指向已存在字符串的指针。
当且仅当在参数列表的第一个记号存在且是一个标识符时,条件汇编指令
'%ifid'会让接下来的一段代码被汇编。'%ifnum'相似。但测试记号是否是
数字;'%ifstr'测试是否是字符串。
比如,4.3.3中定义的宏'writefile'可以用'%ifstr'作进一步改进,如下:
%macro writefile 2-3+
%ifstr %2
jmp %%endstr
%if %0 = 3
%%str: db %2,%3
%else
%%str: db %2
%endif
%%endstr: mov dx,%%str
mov cx,%%endstr-%%str
%else
mov dx,%2
mov cx,%3
%endif
mov bx,%1
mov ah,0x40
int 0x21
%endmacro
这个宏可以处理以下面两种方式进行的调用:
writefile [file], strpointer, length
writefile [file], "hello", 13, 10
在第一种方式下,'strpointer'是作为一个已声明的字符串的地址,而
'length'作为它的长度;第二种方式中,一个字符串被传给了宏,所以
宏就自己声明它,并为它分配地址和长度。
注意,'%ifstr'中的'%if'的使用方式:它首先检测宏是否被传递了两个参
数(如果是这样,那么字符串就是一个单个的字符串常量,这样'db %2'就
足够了)或者更多(这样情况下,除了前两个参数,后面的全部参数都要被
合并到'%3'中,这就需要'db %2,%3'了。)
常见的'%elifXXX','%ifnXXX'和'%elifnXXX'/版本在'%ifid','%ifnum',和
'%ifstr'中都是存在的。
4.4.7 `%error': 报告用户自定义错误。
预处理操作符'%error'会让NASM报告一个在汇编时产生的错误。所以,如果
别的用户想要汇编你的源代码,你必须保证他们用下面的代码定义了正确的
宏:
%ifdef SOME_MACRO
; do some setup
%elifdef SOME_OTHER_MACRO
; do some different setup
%else
%error Neither SOME_MACRO nor SOME_OTHER_MACRO was defined.
%endif
然后,任何不理解你的代码的用户都会被汇编时得到关于他们的错误的警告
信息,不必等到程序在运行时再出现错误却不知道错在哪儿。
4.5 预处理器循环: `%rep'
虽然NASM的'TIMES'前缀非常有用,但是不能用来作用于一个多行宏,因为
它是在NASM已经展开了宏之后才被处理的。所以,NASM提供了另外一种形式
的循环,这回是在预处理器级别的:'%rep'。
操作符'%rep'和'%endrep'('%rep'带有一个数值参数,可以是一个表达式;
'%endrep'不带任何参数)可以用来包围一段代码,然后这段代码可以被复制
多次,次数由预处理器指定。
%assign i 0
%rep 64
inc word [table+2*i]
%assign i i+1
%endrep
这段代码会产生连续的64个'INC'指令,从内存地址'[table]'一直增长到
'[table+126]'。
对于一个复杂的终止条件,或者想要从循环中break出来,你可以使用
'%exitrep'操作符来终止循环,就像下面这样:
fibonacci:
%assign i 0
%assign j 1
%rep 100
%if j > 65535
%exitrep
%endif
dw j
%assign k j+i
%assign i j
%assign j k
%endrep
fib_number equ ($-fibonacci)/2
上面的代码产生所有16位的Fibonacci数。但要注意,循环的最大次数还是要
作为一个参数传给'%rep'。这可以防止NASM预处理器进入一个无限循环。在
多任务或多用户系统中,无限循环会导致内存被耗光或其他程序崩溃。
4.6 包含其它文件。
又一次使用到一个跟C预处理器语法极其相似的操作符,它可以让你在你的代
码中包含其它源文件。这可以通过'%include'来实现:
%include "macros.mac"
这会把文件'macros.mac'文件中的内容包含到现在的源文件中。
被包含文件会被在当前目录下寻找(就是你在运行NASM时所在的目录,并不是
NASM可执行文件所在的目录或源程序文件所在的目录),你可以在NASM的命令行
上使用选项'-i'来增加搜索路径。
C语言中防止文件被重复包含的习惯做法在NASM中也适用:如果文件
'macros.mac'中有如下形式的代码:
%ifndef MACROS_MAC
%define MACROS_MAC
; now define some macros
%endif
这样多次包含该文件就不会引起错误,因为第二次包含该文件时,什么
也不会发生,因为宏'MACROS_MAC'已经被定义过了。
在没用'%include'操作符包含一个文件时,你可以强制让这个文件被包含
进来,做法是在NASM命令行上使用'-p'选项
4.7 上下文栈。
那些对一个宏定义来讲是本地的Labels有时候还不够强大:有时候,你需
要能够在多个宏调用之间共享label。比如一个'REPEAT'...'UNTIL'循
环,'REPEAT'宏的展开可能需要能够去引用'UNTIL'中定义的宏。而且在
使用这样的宏时,你可能还会嵌套多层循环。
NASM通过上下文栈提供这个层次上的功能。预处理器维护了一个包含上下
文的栈,每一个上下文都有一个名字作为标识。你可以通过指令'%push'
往上下文栈中加一个新的上下文,或通过'%pop'去掉一个。你可以定义一
些只针对特定上下文来说是本地的labels。
4.7.1 `%push' and `%pop': 创建和删除上下文。
'%push'操作符用来创建一个新的上下文,然后把它放在上下文栈的顶端。
'%push'需要一个参数,它是这个上下文的名字,例如:
%push foobar
这会把一个新的叫做'foobar'的上下文放到栈顶。你可以在一个栈中拥有
多个具有相同名字的上下文:它们之间仍旧是可以区分的。
操作符'%pop'不需要参数,删除栈顶的上下文,并把它销毁,同时也删除
跟它相关的labels。
4.7.2 Context-Local Labels
就像'%%foo'会定义一个对于它所在的那个宏来讲是本地的label一样,
'%$foo'会定义一个对于当前栈顶的上下文来讲是本地的lable。所以,上
文提到的'REPEAT','UNTIL'的例子可以以下面的方式实现:
%macro repeat 0
%push repeat
%$begin:
%endmacro
%macro until 1
j%-1 %$begin
%pop
%endmacro
然后象下面这样使用它:
mov cx,string
repeat
add cx,3
scasb
until e
它会扫描每个字符串中的第四个字节,以查找在al中的字节。
如果你需要定义,或存取对于不在栈顶的上下文本地的label,你可以使用
'%$$foo',或'%$$$foo'来存取栈下面的上下文。
4.7.3 Context-Local单行宏。
NASM也允许你定义对于一个特定的上下文是本地的单行宏,使用的方式大致
相面:
%define %$localmac 3
这会定义一个对于栈顶的上下文本地的单行宏'%$localmax',当然,在又一个
'%push'操作之后,它还是可以通过'%$$localmac'来存取。
4.7.4 `%repl': 对一个上下文改名。
如果你需要改变一个栈顶上下文的名字(比如,为了响应'%ifctx'),你可以
在'%pop'之后紧接着一个'%push';但它会产生负面效应,会破坏所有的跟栈
顶上下文相关的context-local labels和宏。
NASM提供了一个操作符'%repl',它可以在不影响相关的宏与labels的情况下,
为一个上下文换一个名字,所以你可以把下面的破坏性代码替换成另一种形
式:
%pop
%push newname
换成不具破坏性的版本: `%repl newname'.
4.7.5 使用上下文栈的例子: Block IFs
这个例子几乎使用了所有的上下文栈的特性,包括条件汇编结构'%ifctx',
它把一个块IF语句作为一套宏来执行:
%macro if 1
%push if
j%-1 %$ifnot
%endmacro
%macro else 0
%ifctx if
%repl else
jmp %$ifend
%$ifnot:
%else
%error "expected `if' before `else'"
%endif
%endmacro
%macro endif 0
%ifctx if
%$ifnot:
%pop
%elifctx else
%$ifend:
%pop
%else
%error "expected `if' or `else' before `endif'"
%endif
%endmacro
这段代码看上去比上面的`REPEAT'和`UNTIL'宏要饱满多了。因为它使用了
条件汇编去验证宏以正确的顺序被执行(比如,不能在'if'之间调用'endif'
)如果出现错误,执行'%error'。
另外,'endif'宏要处理两种不同的情况,即它可能直接跟在'if'后面,也
可能跟在'else'后面。它也是通过条件汇编,判断上下文栈的栈顶是'if'还
是'else',并据此来执行不同的动作。
'else'宏必须把上下文保存到栈中,好让'if'宏跟'endif'宏中定义的
'%$ifnot'引用。但必须改变上下文的名字,这样'endif'就可以知道这中间
还有一个'else'。这是通过'%repl'来做这件事情的。
下面是一个使用这些宏的例子:
cmp ax,bx
if ae
cmp bx,cx
if ae
mov ax,cx
else
mov ax,bx
endif
else
cmp ax,cx
if ae
mov ax,cx
endif
endif
通过把在内层'if'中描述的另一个上下文压栈,放在外层'if'中的上下文
的上面,这样,'else'和'endif'总能引用到匹配的'if'或'else'。这个
块-'IF'宏处理嵌套的能力相当好,
4.8 标准宏。
NASM定义了一套标准宏,当开始处理源文件时,这些宏都已经被定义了。
如果你真的希望一个程序在执行前没有预定义的宏存在,你可以使用
'%clear'操作符清空预处理器的一切。
大多数用户级的操作符(第五章)是作为宏来运行的,这些宏进一步调用原
始的操作符;这些在第五章介绍。剩余的标准宏在这里进行描述。
4.8.1 `__NASM_MAJOR__', `__NASM_MINOR__', `__NASM_SUBMINOR__'和
`___NASM_PATCHLEVEL__': NASM版本宏。
单行宏`__NASM_MAJOR__', `__NASM_MINOR__',`__NASM_SUBMINOR__'和
`___NASM_PATCHLEVEL__'被展开成当前使用的NASM的主版本号,次版本号,
子次版本号和补丁级。所在,在NASM 0.98.32p1版本中,`__NASM_MAJOR__'
被展开成0,`__NASM_MINOR__'被展开成98,`__NASM_SUBMINOR__'被展开成
32,`___NASM_PATCHLEVEL__'被定义为1。
4.8.2 `__NASM_VERSION_ID__': NASM版本ID。
单行宏`__NASM_VERSION_ID__'被展开成双字整型数,代表当前使用的版本
的NASM的全版本数。这个值等于把`__NASM_MAJOR__',`__NASM_MINOR__',
`__NASM_SUBMINOR__'和`___NASM_PATCHLEVEL__'连结起来产生一个单个的
双字长整型数。所以,对于0.98.32p1,返回值会等于
dd 0x00622001
或者
db 1,32,98,0
注意,上面两行代码产生的是完全相同的代码,第二行只是用来指出内存
中存在的各个值之间的顺序。
4.8.3 `__NASM_VER__': NASM版本字符串。
单行宏`__NASM_VER__'被展开成一个字符串,它定义了当前使用的NASM的
版本号。所以,在NASM0.98.32下:
db __NASM_VER__
会被展开成:
db "0.98.32"
4.8.4 `__FILE__' and `__LINE__': 文件名和行号。
就像C的预处理器,NASM允许用户找到包含有当前指令的文件的文件名和行
数。宏'__FILE__'展开成一个字符串常量,该常量给出当前输入文件的文件
名(如果含有'%include'操作符,这个值会在汇编的过程中改变),而
`__LINE__'会被展开成一个数值常量,给出在输入文件中的当前行的行号。
这些宏可以使用在宏中,以查看调试信息,当在一个宏定义中包含宏
'__LINE__'时(不管 是单行还是多行),会返回宏调用,而不是宏定义处
的行号。这可以用来确定是否在一段代码中发生了程序崩溃。比如,某人
可以编写一个子过程'stillhere',它通过'EAX'传递一个行号,然后输出一
些信息,比如:"line 155: still here'。你可以这样编写宏:
%macro notdeadyet 0
push eax
mov eax,__LINE__
call stillhere
pop eax
%endmacro
然后,在你的代码中插入宏调用,直到你发现发生错误的代码为止。
4.8.5 `STRUC' and `ENDSTRUC': 声明一个结构体数据类型。
在NASM的内部,没有真正意义上的定义结构体数据类型的机制; 取代它的
是,预处理器的功能相当强大,可以把结构体数据类型以一套宏的形式来
运行。宏 ‘STRUCT’ 和'ENDSTRUC'是用来定义一个结构体数据类型的。
‘STRUCT’带有一个参数,它是结构体的名字。这个名字代表结构体本身,它
在结构体内的偏移地址为零,名字加上一个_size后缀组成的符号,用一个
'EQU'给它赋上结构体的大小。一旦‘STRUC'被执行,你就开始在定义一个
结构体,你可以用'RESB'类伪指令定义结构体的域,然后使用'ENDSTRUC'来
结束定义。
比如,定义一个叫做'mytype'的结构体,包含一个longword,一个word,一个
byte,和一个字符串,你可以这样写代码:
struc mytype
mt_long: resd 1
mt_word: resw 1
mt_byte: resb 1
mt_str: resb 32
endstruc
上面的代码定义了六个符号:'m_long'在地置0(从结构体'mytype'开头开始
到这个longword域的偏移),`mt_word'在地置4, `mt_byte'6, `mt_str'7,
`mytype_size'是39,而`mytype' 自己在地置0
之所以要把结构体的名字定义在地址零处,是因为要让结构体可以使用本地
labels机制的缘故:如果你想要在多个结构体中使用具有同样名字的成员,
你可以把上面的结构体定义成这个样子:
struc mytype
.long: resd 1
.word: resw 1
.byte: resb 1
.str: resb 32
endstruc
在这个定义中,把结构体域的偏移值定义成了:'mytype.long',
`mytype.word', `mytype.byte' and `mytype.str'.
NASM因此而没有内部的结构体支持,也不支持以句点形式引用结构体中的成
员,所以代码'mov ax, [mystruc.mt_word]’是非法的,'mt_word'是一个常数,
就像其它类型的常数一样,所以,正确的语法应该是
'mov ax,[mystruc+mt_word]'或者`mov ax,[mystruc+mytype.word]'.
4.8.6 `ISTRUC', `AT' and `IEND': 声明结构体的一个实例。
定义了一个结构体类型以后,你下一步要做的事情往往就是在你的数据段
中声明一个结构体的实例。NASM通过使用'ISTRUC'机制提供一种非常简单
的方式。在程序中声明一个'mytype'结构体,你可以象下面这样写代码:
mystruc:
istruc mytype
at mt_long, dd 123456
at mt_word, dw 1024
at mt_byte, db 'x'
at mt_str, db 'hello, world', 13, 10, 0
iend
‘AT’宏的功能是通过使用'TIMES'前缀把偏移位置定位到正确的结构体域上,
然后,声明一个特定的数据。所以,结构体域必须以在结构体定义中相同的
顺序被声明。
如果为结构体的域赋值要多于一行,那接下的内容可直接跟在'AT'行后面,比
如:
at mt_str, db 123,134,145,156,167,178,189
db 190,100,0
按个人的喜好不同,你也可以不在'AT'行上写数据,而直接在第二行开始写
数据域:
at mt_str
db 'hello, world'
db 13,10,0
4.8.7 `ALIGN' and `ALIGNB': 数据对齐
宏'ALIGN'和'ALIGNB'提供一种便捷的方式来进行数据或代码的在字,双字,段
或其他边界上的对齐(有些汇编器把这两个宏叫做'EVEN'),有关这两个宏的语法
是:
align 4 ; align on 4-byte boundary
align 16 ; align on 16-byte boundary
align 8,db 0 ; pad with 0s rather than NOPs
align 4,resb 1 ; align to 4 in the BSS
alignb 4 ; equivalent to previous line
这两个个参数都要求它们的第一个参数是2的幂;它们都会计算需要多少字节来
来存储当前段,当然这个字节数必须向上对齐到一个2的幂值。然后用它们的第
二个参数来执行'TIMES'前缀进行对齐。
如果第二个参数没有被指定,那'ALIGN'的缺省值就是'NOP',而'ALIGNB'的缺省
值就是'RESB 1'.当第二个参数被指定时,这两个宏是等效的。通常,你可以在
数据段与代码段中使用'ALIGN',而在BSS段中使用'ALIGNB',除非有特殊用途,一
般你不需要第二个参数。
作为两个简单的宏,'ALIGN'与'ALIGNB'不执行错误检查:如果它们的第一个参
数不是2的某次方,或它们的第二个参数大于一个字节的代码,他们都不会有警
告信息,这两种情况下,它们都会执行错误。
'ALIGNB'(或者,'ALIGN'带上第二个参数'RESB 1')可以用在结构体的定义中:
struc mytype2
mt_byte:
resb 1
alignb 2
mt_word:
resw 1
alignb 4
mt_long:
resd 1
mt_str:
resb 32
endstruc
这可以保证结构体的成员被对齐到跟结构体的基地址之间有一个正确的偏移值。
最后需要注意的是,'ALIGN'和'ALIGNB'都是以段的开始地址作为参考的,而
不是整个可执行程序的地址空间。如果你所在的段只能保证对齐到4字节的边
界,那它会对齐对16字节的边界,这会造成浪费,另外,NASM不会检测段的对
齐特性是否可被'ALIGN'和'ALIGNB'使用。
4.9 TASM兼容预处理指令。
接下来的预处理操作符只有在用'-t'命令行开关把TASM兼容模式打开的情况下
才可以使用(这个开关在2.1.16介绍过)
(*) `%arg' (见4.9.1节)
(*) `%stacksize' (见4.9.2节)
(*) `%local' (见4.9.3节)
4.9.1 `%arg'操作符
'%arg'操作符用来简化栈上的参数传递操作处理。基于栈的参数传递在很多高
级语言中被使用,包括C,C++和Pascal。
而NASM企图通过宏来实现这种功能(参阅7.4.5),它的语法使用上不是很舒服,
而且跟TASM之间是不兼容的。这里有一个例子,展示了只通过宏'%arg'来处理:
some_function:
%push mycontext ; save the current context
%stacksize large ; tell NASM to use bp
%arg i:word, j_ptr:word
mov ax,[i]
mov bx,[j_ptr]
add ax,[bx]
ret
%pop ; restore original context
这跟在7.4.5中定义的过程很相似,把j_ptr指向的值加到i中,然后把相加的结
果在AX中返回,对于'push'和'pop'的展开请参阅4.7.1关于上下文栈的使用。
4.9.2 `%stacksize'指令。
'%stacksize'指令是跟'%arg'和'%local'指令结合起来使用的。它告诉NASM
为'%arg'和'%local'使用的缺省大小。'%stacksize'指令带有一个参数,它
是'flat','large'或'small'。
%stacksize flat
这种形式将使NASM使用相对于'ebp'的基于栈的参数地址。它假设使用一个
近调用来得到这个参数表。(比如,eip被压栈).
%stacksize large
而这种形式会用'bp'来进行基于栈的参数寻址,假设使用了一个远调用来
获得这个地址(比如,ip和cs都会被压栈)。
%stacksize small
这种形式也使用'bp'来进行基于栈的参数寻址,但它跟'large'不同,因为
他假设bp的旧值已经被压栈。换句话说,你假设bp,ip和cs正在栈顶,在它们
下面的所有本地空间已经被'ENTER'指令开辟好了。当和'%local'指令结合的
时候,这种形式特别有用。
4.9.3 `%local'指令。
'%local'指令用来简化在栈框架中进行本地临时栈变量的分配。C语言中的自
动本地变量是这种类型变量的一个例子。'%local'指令跟'%stacksize'一起
使用的时候特别有用。并和'%arg'指令保持兼容。它也允许简化对于那些用
'ENTER'指令分配在栈中的变量的引用(关于ENTER指令,请参况B.4.65)。这
里有一个关于它们的使用的例子:
silly_swap:
%push mycontext ; save the current context
%stacksize small ; tell NASM to use bp
%assign %$localsize 0 ; see text for explanation
%local old_ax:word, old_dx:word
enter %$localsize,0 ; see text for explanation
mov [old_ax],ax ; swap ax & bx
mov [old_dx],dx ; and swap dx & cx
mov ax,bx
mov dx,cx
mov bx,[old_ax]
mov cx,[old_dx]
leave ; restore old bp
ret ;
%pop ; restore original context
变量'%$localsize'是在'%local'的内部使用,而且必须在'%local'指令使用前,
被定义在当前的上下文中。不这样做,在每一个'%local'变量声明的地方会引
发一个表达式语法错误。它然后可以用在一条适当的ENTER指令中。
4.10 其他的预处理指令。
NASM还有一些预处理指令允许从外部源中获取信息,现在,他们包括:
下面的预处理指令使NASM能正确地自理C++/C语言预处理器的输出。
(*) `%line' 使NASM能正确地自理C++/C语言预处理器的输出。(参阅4.10.1)
(*) `%!' 使NASM从一个环境变量中读取信息,然后这些信息就可以在你的程序
中使用了。(4.10.2)
4.10.1 `%line'操作符。
'%line'操作符被用来通知NASM,输入行与另一个文件中指定的行号相关。一般
这另一个文件会是一个源程序文件,它作为现在NASM的输入,但它是一个预处理
器的输出。'%line'指令允许NASM输出关于在这个源程序文件中的指定行号的信
息,而不是被NASM读进来的整个文件。
这个预处理指令通常不会被程序员用到,但会让预处理器的作者感兴趣,'%line'
的使用方法如下:
%line nnn[+mmm] [filename]
在这个指令中,'nnn'指定源程序文件中与之相关的特定行,'mmm'是一个可选的
参数,它指定一个行递增的值;每一个被读进来的源文件行被认为与源程序文件
中的'mmm'行相关。最终,'filename'可选参数指定源程序文件的文件名。
在读到一条'%line'预处理指令后,NASM会报告与指定的值相关的所有的文件名和
行号
4.10.2 `%!'`': 读取一个环境变量。
`%!' 操作符可以在汇编时读取一个环境变量的值,这可以用在一个环境变量
的内容保存到一个字符串中。该字符串可以用在你程序的其他地方。
比如,假设你有一个环境变量'FOO',你希望把'FOO'的值嵌入到你的程序中去。你可
以这样做:
%define FOO %!FOO
%define quote '
tmpstr db quote FOO quote
在写的时候,在定义'quote'时,它会产生一个'没有结束的字符串'的警告信息,
它会自己在读进来的字符串的前后加上一个空格。我没有办法找到一个简单的
工作方式(尽管可以通过宏来创建),我认为,你没有必要学习创建更为复杂
的宏,或者如果你用这种方式使用这个特性,你没有必要使用额外的空间。
第五章: 汇编器指令。
-------------------------------
尽管NASM极力避免MASN和TASM中的那些庸肿复杂的东西,但还是不得不支持少
量的指令,这些指令在本章进行描述。
NASM的指令有两种类型:用户级指令和原始指令。一般地,每一条指令都有一
个用户级形式和原始形式。在大多数情况下,我们推荐用户使用有户级指令,
它们以宏的形式运行,并去调用原始形式的指令。
原始指令被包含在一个方括号中;用户级指令没有括号。
除了本章所描述的这些通用的指令,每一种目标文件格式为了控制文件格式
的一些特性,可以使用一些另外的指令。这些格式相关的指令在第六章中跟
相关的文件格式一起进行介绍。
5.1 `BITS': 指定目标处理器模式。
'BITS'指令指定NASM产生的代码是被设计运行在16位模式的处理器上还是运行
在32位模式的处理器上。语法是'BITS 16'或'BITS 32'
大多数情况下,你可能不需要显式地指定'BITS'。'aout','coff','elf'和
'win32'目标文件格式都是被设计用在32位操作系统上的,它们会让NASM缺
省选择32位模式。而'obj'目标文件格式允许你为每一个段指定'USE16'或
'USE32',然后NASM就会按你的指定设定操作模式,所以多次使用'BITS'是
没有必要的。
最有可能使用'BITS'的场合是在一个纯二进制文件中使用32位代码;这是因
为'bin'输出格式在作为DOS的'.COM'程序,DOS的'.SYS'设备驱动程序,或引
导程序时,默认都是16位模式。
如果你仅仅是为了在16位的DOS程序中使用32位指令,你不必指定'BITS 32',
如果你这样做了,汇编器反而会产生错误的代码,因为这样它会产生运行在
16位模式下,却以32位平台为目标的代码。
当NASM在'BITS 16'状态下时,使用32位数据的指令可以加一个字节的前缀
0x66,要使用32位的地址,可以加上0x67前缀。在'BITS 32'状态下,相反的
情况成立,32位指令不需要前缀,而使用16位数据的指令需要0x66前缀,使
用16位地址的指令需要0x67前缀。
'BITS'指令拥有一个等效的原始形式:[BITS 16]和[BITS 32]。而用户级的
形式只是一个仅仅调用原始形式的宏。
5.1.1 `USE16' & `USE32': BITS的别名。
'USE16'和'USE32'指令可以用来取代'BITS 16'和'BITS 32',这是为了和其他
汇编器保持兼容性。
5.2 `SECTION'或`SEGMENT': 改变和定义段。
'SECTION'指令('SEGMENT'跟它完全等效)改变你正编写的代码将被汇编进的段。
在某些目标文件格式中,段的数量与名称是确定的;而在别一些格式中,用户
可以建立任意多的段。因此,如果你企图切换到一个不存在的段,'SECTION'有
时可能会给出错误信息,或者定义出一个新段,
Unix的目标文件格式和'bin'目标文件格式,都支持标准的段'.text','.data'
和'bss'段,与之不同的的,'obj'格式不能辩识上面的段名,并需要把段名开
头的句点去掉。
5.2.1 宏 `__SECT__'
'SECTION'指令跟一般指令有所不同,的用户级形式跟它的原始形式在功能上有
所不同,原始形式[SECTION xyz],简单地切换到给出的目标段。用户级形式,
'SECTION xyz'先定义一个单行宏'__SECT__',定义为原始形式[SECTION],这正
是要执行的指令,然后执行它。所以,用户级指令:
SECTION .text
被展开成两行:
%define __SECT__ [SECTION .text]
[SECTION .text]
用户会发现在他们自己的宏中,这是非常有用的。比如,4.3.3中定义的宏
'writefile'以下面的更为精致的写法会更有用:
%macro writefile 2+
[section .data]
%%str: db %2
%%endstr:
__SECT__
mov dx,%%str
mov cx,%%endstr-%%str
mov bx,%1
mov ah,0x40
int 0x21
%endmacro
这个形式的宏,一次传递一个用出输出的字符串,先用原始形式的'SECTION'切
换至临时的数据段,这样就不会破会宏'__SECT__'。然后它把它的字符串声明在
数据段中,然后调用'__SECT__'切换加用户先前所在的段。这样就可以避免先前
版本的'writefile'宏中的用来跳过数据的'JMP'指令,而且在一个更为复杂的格
式模型中也不会失败,用户可以把这个宏放在任何独立的代码段中进行汇编。
5.3 `ABSOLUTE': 定义绝对labels。
'ABSOLUTE'操作符可以被认为是'SECTION'的另一种形式:它会让接下来的代码不
在任何的物理段中,而是在一个从给定地址开始的假想段中。在这种模式中,你
唯一能使用的指令是'RESB'类指令。
`ABSOLUTE'可以象下面这样使用:
absolute 0x1A
kbuf_chr resw 1
kbuf_free resw 1
kbuf resw 16
这个例子描述了一个关于在段地址0x40处的PC BIOS数据域的段,上面的代码把
'kbuf_chr'定义在0x1A处,'kbuf_free'定义在地址0x1C处,'kbuf'定义在地址
0x1E。
就像'SECTION'一样,用户级的'ABSOLUTE'在执行时会重定义'__SECT__'宏。
'STRUC'和'ENDSTRUC'被定义成使用'ABSOLUTE'的宏(同时也使用了'__SECT__')
'ABSOLUTE'不一定需要带有一个绝对常量作为参数:它也可以带有一个表达式(
实际上是一个临界表达式,参阅3.8),表达式的值可以是在一个段中。比如,一
个TSR程序可以在用它重用它的设置代码所占的空间:
org 100h ; it's a .COM program
jmp setup ; setup code comes last
; the resident part of the TSR goes here
setup:
; now write the code that installs the TSR here
absolute setup
runtimevar1 resw 1
runtimevar2 resd 20
tsr_end:
这会在setup段的开始处定义一些变量,所以,在setup运行完后,它所占用的内存
空间可以被作为TSR的数据存储空莘而得到重用。符号'tsr_end'可以用来计算TSR
程序所需占用空间的大小。
5.4 `EXTERN': 从其他的模块中导入符中。
'EXTERN'跟MASM的操作符'EXTRN',C的关键字'extern'极其相似:它被用来声明一
个符号,这个符号在当前模块中没有被定义,但被认为是定义在其他的模块中,但
需要在当前模块中对它引用。不是所有的目标文件格式都支持外部变量的:'bin'文
件格式就不行。
'EXTERN'操作符可以带有任意多个参数,每一个都是一个符号名:
extern _printf
extern _sscanf,_fscanf
有些目标文件格式为'EXTERN'提供了额外的特性。在所有情况下,要使用这些额外
特性,必须在符号名后面加一个冒号,然后跟上目标文件格式相关的一些文字。比如
'obj'文件格式允许你声明一个以外部组'dgroup'为段基址一个变量,可以象下面这样
写:
extern _variable:wrt dgroup
原始形式的'EXTERN'跟用户级的形式有所不同,因为它只能带有一个参数:对于多个参
数的支持是在预处理器级上的特性。
你可以把同一个变量作为'EXTERN'声明多次:NASM会忽略掉第二次和后来声明的,只采
用第一个。但你不能象声明其他变量一样声明一个'EXTERN'变量。
5.5 `GLOBAL': 把符号导出到其他模块中。
'GLOBAL'是'EXTERN'的对立面:如果一个模块声明一个'EXTERN'的符号,然后引用它,
然后为了防止链接错误,另外某一个模块必须确实定义了该符号,然后把它声明为
'GLOBAL',有些汇编器使用名字'PUBLIC'。
'GLOBAL'操作符所作用的符号必须在'GLOBAL'之后进行定义。
'GLOBAL'使用跟'EXTERN'相同的语法,除了它所引用的符号必须在同一样模块中已经被
定义过了,比如:
global _main
_main:
; some code
就像'EXTERN'一样,'GLOBAL'允许目标格式文件通过冒号定义它们自己的扩展。比如
'elf'目标文件格式可以让你指定全局数据是函数或数据。
global hashlookup:function, hashtable:data
就象'EXTERN'一样,原始形式的'GLOBAL'跟用户级的形式不同,仅能一次带有一个参
数
5.6 `COMMON': 定义通用数据域。
'COMMON'操作符被用来声明通用变量。一个通用变量很象一个在非初始化数据段中定义
的全局变量。所以:
common intvar 4
功能上跟下面的代码相似:
global intvar
section .bss
intvar resd 1
不同点是如果多于一个的模块定义了相同的通用变量,在链接时,这些通用变量会被
合并,然后,所有模块中的所有的对'intvar'的引用会指向同一片内存。
就角'GLOBAL'和'EXTERN','COMMON'支持目标文件特定的扩展。比如,'obj'文件格式
允许通用变量为NEAR或FAR,而'elf'格式允许你指定通用变量的对齐需要。
common commvar 4:near ; works in OBJ
common intarray 100:4 ; works in ELF: 4 byte aligned
它的原始形式也只能带有一个参数。
5.7 `CPU': 定义CPU相关。
'CPU'指令限制只能运行特定CPU类型上的指令。
选项如下:
(*) `CPU 8086' 只汇编8086的指令集。
(*) `CPU 186' 汇编80186及其以下的指令集。
(*) `CPU 286' 汇编80286及其以下的指令集。
(*) `CPU 386' 汇编80386及其以下的指令集。
(*) `CPU 486' 486指令集。
(*) `CPU 586' Pentium指令集。
(*) `CPU PENTIUM' 同586。
(*) `CPU 686' P6指令集。
(*) `CPU PPRO' 同686
(*) `CPU P2' 同686
(*) `CPU P3' Pentium III and Katmai指令集。
(*) `CPU KATMAI' 同P3
(*) `CPU P4' Pentium 4 (Willamette)指令集
(*) `CPU WILLAMETTE' 同P4
(*) `CPU IA64' IA64 CPU (x86模式下)指令集
所有选项都是大小写不敏感的,在指定CPU或更低一级CPU上的所有指令都会
被选择。缺省情况下,所有指令都是可用的。