NASM中文手册{Paste}

文章来源:www.oldlinux.org

NASM中文手册
                       ============

第一章: 简介
-----------------------

  1.1 什么是NASM

      NASM是一个为可移植性与模块化而设计的一个80x86的汇编器。它支持相当多
      的目标文件格式,包括Linux和'NetBSD/FreeBSD','a.out','ELF','COFF',微软16
      位的'OBJ'和'Win32'。它还可以输出纯二进制文件。它的语法设计得相当的简
      洁易懂,和Intel语法相似但更简单。它支持'Pentium','P6','MMX','3DNow!',
      'SSE' and 'SSE2'指令集,
     
  1.1.1 为什么还需要一个汇编器?

      NASM当初被设计出来的想法是'comp.lang.asm.x86'(或者可能是'alt.lang.asm'
      ,我忘了),从本质上讲,是因为没有一个好的免费的x86系例的汇编器可以使用,
      所以,必须有人来写一个。      

      (*)'a86'不错,但不是免费的,而且你不可能得到32位代码编写的功能,除非你
      付费,它只使用在dos上。

      (*) 'gas'是免费的,而且在dos下和unix下都可以使用,但是它是作为'gcc'的一
      个后台而设计的,并不是很好,'gcc'一直就提供给它绝对正确的代码,所以它的
      错误检测功能相当弱,还有就是对于任何一个想真正利用它写点东西的人来讲,
      它的语法简直太可怕了,并且你无法在里面写正确的16位代码。

      (*) 'as86'是专门为Minix和Linux设计的,但看上去并没有很多文档可以参考。

      (*) 'MASM'不是很好,并且相当贵,还且只能运行在DOS下。

      (*) 'TASM'好一些,但却极入与MASM保持兼容,这就意味着无数的伪操作码和繁琐
      的约定,并且它的语法本质上就是MASM的,伴随着的就是一些自相矛盾和奇怪的
      东西。它也是相当贵的,并且只能运行在DOS下。

所以,只有NASM才能使您愉悦得编程。目前,它仍在原型设计阶段-我们不期望它
能够超越所有的这些汇编器。但请您发给我们bug报告,修正意见,和其他有用的
信息,还有其他任何你手头有的对我们有用的信息(感谢所有已经这样在做了的
人们),我们还会不断地改进它。

   1.1.2 许可条件

请阅读作为NASM发布的一部分的文件'Licence',只有在该许可条件下你才可以使
用NASM。

  1.2 联系信息

当前版本的NASM(0.98.08)由一个开发小组在维护,你可以从'nasm-devel'邮件列表
中得到(看下面的链接),如果你想要报告bug,请先阅读10.2节
     
NASM有一个主页:'http://www.web-sites.co.uk/nasm',更多的信息还可以在
`http://nasm.2y.net/'上获取。
     
最初的作者你可以通过email:`jules@dsf.org.uk'和`anakin@pobox.com'和他们联
系,但后来的开发小组并不在其中。

最新的NASM发布被上传至官方网站`http://www.web-sites.co.uk/nasm'和`ftp.kernel.org',
`ibiblio.org'

公告被发布至`comp.lang.asm.x86', `alt.lang.asm' 和`comp.os.linux.announce'
 
如果你想了解NASM beta版的发布,和当前的开发状态,请通过在
`http://groups.yahoo.com/group/nasm-devel',
      `http://www.pairlist.net/mailman/listinfo/nasm-devel' and
      `http://sourceforge.net/projects/nasm'
注册来捐助'nasm-devel'邮件列表。

    在网站Sourceforge上的列表是较好的一个列表,它也是最新nasm源代码与发布的
一个网站,另外的列表也是公开的,但有可能不会被继续长期支持。

  1.3 安装

  1.3.1 在dos和Windows下安装NASM

如果你拿到了NASM的DOS安装包,'nasmXXX.zip'(这里.'XXX'表示该安装包的NASM版
本号),把它解压到它自己的目录下(比如:‘c:\nasm')

该包中会包含有四个可执行文件:NASM可拟行文件'nasm.exe'和'nasmw.exe',还有
NDISASM可执行文件'ndisasm.exe'和'ndisasmw.exe'。文件名以'w'结尾的是'Win32'
可执行格式。是运行在'Windows 95'或'Windows NT'的Intel处理器上的,另外的是
16位的'DOS'可执行文件。

NASM运行时需要的唯一文件就是它自己的可执行文件,所以可以拷贝'nasm.exe'
和'nasmw.exe'的其中一个到你自己的路径下,或者可以编写一个'autoexec.bat'把
nasm的路径加到你的'PATH'环境变量中去。(如果你只安装了Win32版本的,你可能
希望把文件名改成'nasm.exe'。)

就这样,NASM装好了。你不需要为了运行nasm而让'nasm'目录一直存在(除非你把它
加到了你的'PATH'中,所以如果你需要节省空间,你可删掉它,但是,你可能需要保留
文档或测试程序。

如果你下载了DOS版的源码包,'nasmXXXs.zip',那'nasm'目录还会包含完整的NASM源
代码,你可以选择一个Makefiles来重新构造你的NASM版本。

注意源文件`insnsa.c', `insnsd.c', `insnsi.h'和`insnsn.c'是由'standard.mac'中
的指令自动生成的,尽管NASM0.98发布版中包含了这些产生的文件,你如果改动了
insns.dat,standard.mac或者文件,可能需要重新构造他们,在将来的源码发布中有
可能将不再包含这些文件,多平台兼容的Perl可以从www.cpan.org上得到。

  1.3.2 在unix下安装NASM

如果你得到了Unix下的NASM源码包'nasm-x.xx.tar.gz'(这里x.xx表示该源码包中的
nasm的版本号),把它解压压到一个目录,比如'/usr/local/src'。包被解压后会创建
自己的子目录'nasm-x.xx'

NASM是一个自动配置的安装包:一旦你解压了它,'cd'到它的目录下,输入'./configuer',
该脚本会找到最好的C编译器来构造NASM,并据此建立Makefiles。

一旦NASM被自动配置好后,你可以输入'make'来构造'nasm'和'ndisasm'二进制文件,
然后输入'make install'把它们安装到'/usr/local/bin',并把man页安装到
'/usr/local/man/man1'下的'nasm.1和'ndisasm.1'或者你可以给配置脚本一个
'--prefix'选项来指定安装目录,或者也可以自己来安装。

NASM还附带一套处理'RDOFF'目标文件格式的实用程序,它们在'rdoff'子目录下,
你可以用'make rdf'来构造它们,并使用'make rdf_install'来安装。如果你需
要的话。

如果NASM在自动配置的时候失败了,你还是可以使用文件'Makefile.unx'来编译它们,
把这个文件改名为'Makefile',然后输入'make'。在'rdoff'子目录下同样有一个
Makefile.unx文件。

第二章 运行NASM
-----------------------

  2.1 NASM命令行语法

要汇编一个文件,你可以以下面的格式执行一个命令:

nasm -f <format> <filename> [-o <output>]

比如,

nasm -f elf myfile.asm

会把文件'myfile.asm'汇编成'ELF'格式 的文件'myfile.o'.还有:

nasm -f bin myfile.asm -o myfile.com

会把文件'myfile.asm'汇编成纯二进制格式的文件'myfile.com'。

想要以十六进制代码的形式产生列表文件输出,并让代码显示在源代码的左侧,
使用'-l'选项并给出列表文件名,比如:

nasm -f coff myfile.asm -l myfile.lst

想要获取更多的关于NASM的使用信息,请输入:

nasm -h

它同时还会输出可以使用的输出文件格式,
如果你使用Linux并且不清楚你的系统是'a.out'还是'ELF',请输入:

file nasm

(在nasm二进制文件的安装目录下使用),如果系统输出类似下面的信息:

nasm: ELF 32-bit LSB executable i386 (386 and up) Version 1

那么你的系统就是'ELF'格式的,然后你就应该在产生Linux目标文件时使用选
项'-f elf',如果系统输入类似下面的信息:

nasm: Linux/i386 demand-paged executable (QMAGIC)

或者与此相似的,你的系统是'a.out'的,那你应该使用'-f aout'(Linux的'a.out'
系统很久以前就过时了,现在已非常少见。)
     
就像其他的Unix编译器与汇编器,NASM在碰到错误以前是不输出任何信息的,所
以除了出错信息你看不到任何其他信息。

   2.1.1 '-o'选项:指定输出文件的文件名。

NASM会为你的输出文件选择一个文件名;具体如何做取决于目标文件的格式,对
于微软的目标文件格式('obj'和'win32'),它会去掉你的源文件名的'.asm'扩展
名(或者其他任何你喜欢使用的扩展名,NASM并不关心具体是什么),并替换上
'obj'。对于Unix的目标文件格式('aout','coff','elf'和'as86')它会替换成
'.o', 对于'rdf',它会使用'.rdf',还有为'bin'格式,它会简单地去掉扩展名,所以
'myfile.asm'会产生的一个输出文件'myfile'。

如果输出文件已经存在,NASM会覆盖它,除非它的文件名与输入文件同名,在这种
情况下,它会给出一个警告信息,并使用'nasm.out'作为输出文件的文件名。

在某些情况下,上述行为是不能接受的,所以,NASM提供了'-o'选项,它能让你指定
你的输出文件的文件名,你使用'-o'后面紧跟你为输出文件取的名字,中间可以加
空格也可以不加。比如:

nasm -f bin program.asm -o program.com
nasm -f bin driver.asm -odriver.sys

请注意这是一个小写的o,跟大写字母O是不同的,大写的是用来指定需要传递的选
项的数目,请参阅2.1.15

  2.1.2 `-f'选项:指定输出文件的格式。

如果你没有对NASM使用'-f'选项,它会自己为你选择一个输出文件格式。在发布的
NASM版本中,缺省的输出格式总是'bin';如果你自己编译你的NASM,你可以在编译的
时候重定义'OF_DEFAULT'来选择你需要的缺省格式。

就象'-o','-f'与输出文件格式之间的空格也是可选的,所以'-f elf'和'-felf'都是
合法的。

所有可使用的输出文件格式的列表可以通过运行命令'nasm -hf'得到。

  2.1.3 `-l' 选项: 产生列表文件

如果你对NASM使用了'-l'选项,后面跟一个文件名,NASM会为你产生一个源文件的列表
文件,在里面,地址和产生的代码列在左边,实际的源代码(包括宏扩展,除了那些指定
不需要在列表中扩展的宏,参阅4.3.9)列在右边,比如:

nasm -f elf myfile.asm -l myfile.lst

  2.1.4 `-M'选项: 产生Makefile依赖关系.

该选项可以用来向标准输出产生makefile依赖关系,可以把这些信息重定向到一个文件
中以待进一步处理,比如:
     
NASM -M myfile.asm > myfile.dep

  2.1.5 `-F'选项: 选择一个调试格式

该选项可以用来为输出文件选择一个调试格式,语法跟-f选项相册,唯一不同的是它产
生的输出文件是调试格式的。

一个具体文件格式的完整的可使用调试文件格式的列表可通过命令'nasm -f <format> -y'
来得到。

这个选项在缺省状态下没有被构建时NASM。如何使用该选项的信息请参阅6.10

  2.1.6 `-g' 选项:使调试信息有效。

该选项可用来在指定格式的输出文件中产生调试信息。
更多的信息请参阅2.1.5

  2.1.7 `-E' 选项: 把错误信息输入到文件。
 
  在'MS-DOS'下,尽管有办法,但要把程序的标准错误输出重定向到一个文件还是非常困
难的。因为NASM常把它的警告和错误信息输出到标准错误设备,这将导致你在文本编
辑器里面很难捕捉到它们。

因此NASM提供了一个'-E'选项,带有一个文件名参数,它可以把错误信息输出到指定的
文件而不是标准错误设备。所以你可以输入下面这样的命令来把错误重定向到文件:

nasm -E myfile.err -f obj myfile.asm

  2.1.8 `-s' 选项: 把错误信息输出到'stdout'

'-s'选项可以把错误信息重定向到'stdout'而不是'stderr',它可以在'MS-DOS'下进行
重定向。想要在汇编文件'myfile.asm'时把它的输出用管道输出给'more'程序,可以这样:
     
nasm -s -f obj myfile.asm | more

请参考2.1.7的'-E'选项.

  2.1.9 `-i'选项: 包含文件搜索路径

当NASM在源文件中看到'%include'操作符时(参阅4.6),它不仅仅会在当前目录下搜索给
出的文件,还会搜索'-i'选项在命令行中指定的所有路径。所以你可以从宏定义库中
包含进一个文件,比如,输入:

nasm -ic:\macrolib\ -f obj myfile.asm

(通常,在 '-i'与路径名之间的空格是允许的,并且可选的。)

NASM更多的关注源代码级上的完全可移植性,所以并不理解正运行的操作系统对文件的
命名习惯;你提供给'-i'作为参数的的字符串会被一字不差地加在包含文件的文件名前。
所以,上例中最后面的一个反斜杠是必要的,在Unix下,一个尾部的正斜线也同样是必要的。

(当然,如果你确实需要,你也可以不正规地使用它,比如,选项'-ifoo'会导致
'%incldue "bar.i'去搜索文件'foobar.i'...)

如果你希望定义一个标准的搜索路径,比如像Unix系统下的'/usr/include',你可以在环境
变量NASMENV中放置一个或多个'-i'(参阅2.1.19)

为了与绝大多数C编译器的Makefile保持兼容,该选项也可以被写成'-I'。

  2.1.10 `-p' 选项: 预包含一个文件

NASM允许你通过'-p'选项来指定一个文件预包含进你的源文件。所以,如果运行:

nasm myfile.asm -p myinc.inc

跟在源文件开头写上'%include "myinc.inc"然后运行'nasm myfile.asm'是等效的。

为和'-I','-D','-U'选项操持一致性,该选项也可以被写成'-P'

2.1.11 `-d'选项: 预定义一个宏。

就像'-p'选项给出了在文件头放置'%include'的另一种实现,'-d'选项给出了在文
件中写'%define'的另一种实现,你可以写:

nasm myfile.asm -dFOO=100

     作为在文件中写下面一行语句的一种替代实现:

      %define FOO 100

      在文件的开始,你可以取消一个宏定义,同样,选项'-dFOO'等同于代码'%define FOO'。
这种形式的操作符在选择编译时操作中非常有用,它们可以用'%ifdef'来进行测试,
比如'-dDEBUG'。
     
      为了与绝大多数C编译器的Makefile保持兼容,该选项也可以被写成'-D'。

  2.1.12 `-u' 选项: 取消一个宏定义。
 
  '-u'选项可以用来取消一个由'-p'或'-d'选项先前在命令行上定义的一个宏定义。

比如,下面的命令语句:

      nasm myfile.asm -dFOO=100 -uFOO

会导致'FOO'不是一个在程序中预定义的宏。这在Makefile中不同位置重载一个操
作时很有用。

为了与绝大多数C编译器的Makefile保持兼容,该选项也可以被写成'-U'。

  2.1.13 `-e'选项: 仅预处理。
 
  NASM允许预处理器独立运行。使用'-e'选项(不需要参数)会导致NASM预处理输入
文件,展开所有的宏,去掉所有的注释和预处理操作符,然后把结果文件打印在标
准输出上(如果'-o'选项也被指定的话,会被存入一个文件)。

该选项不能被用在那些需要预处理器去计算与符号相关的表达式的程序中,所以
如下面的代码:

      %assign tablesize ($-tablestart)

会在仅预处理模式中会出错。

  2.1.14 `-a' 选项: 不需要预处理。
 
  如果NASM被用作编译器的后台,那么假设编译器已经作完了预处理,并禁止NASM的预
处理功能显然是可以节约时间,加快编译速度。'-a'选项(不需要参数),会让NASM把
它强大的预处理器换成另一个什么也不做的预处理器。
 
  2.1.15 `-On'选项: 指定多遍优化。
 
  NASM在缺省状态下是一个两遍的汇编器。这意味着如果你有一个复杂的源文件需要
多于两遍的汇编。你必须告诉它。

      使用'-O'选项,你可以告诉NASM执行多遍汇编。语法如下:

      (*)'-O0'严格执行两遍优化,JMP和Jcc的处理和0.98版类似,除了向后跳的JMP是短跳
转,如果可能,立即数在它们的短格式没有被指定的情况下使用长格式。

      (*)'-O1'严格执行两遍优化,但前向分支被汇编成保证能够到达的代码;可能产生比
'-O0'更大的代码,但在分支中的偏移地址没有指定的情况下汇编成功的机率更大,

      (*)'-On' 多编优化,最小化分支的偏移,最小化带符号的立即数,当'strict'关键字
没有用的时候重载指定的大小(参阅3.7),如果2<=n<=3,会有5*n遍,而不是n遍。

注意这是一个大写的O,和小写的o是不同的,小写的o是指定输出文件的格式,可参阅
2.1.1

  2.1.16 `-t'选项: 使用TASM兼容模式。
 
  NASM有一个与Borlands的TASM之间的受限的兼容格式。如果使用了NASM的'-t'选项,
就会产生下列变化:

      (*)本地符号的前缀由'.'改为'@@'
     
      (*)TASM风格的以'@'开头的应答文件可以由命令行指定。这和NASM支持的'-@resp'
风格是不同的。

      (*)扩号中的尺寸替换被支持。在TASM兼容模式中,方括号中的尺寸替换改变了操作
数的尺寸大小,方括号不再支持NASM语法的操作数地址。比如,'mov eax,[DWORD VAL]'
在TASM兼容语法中是合法的。但注意你失去了为指令替换缺省地址类型的能力。
       
      (*)'%arg'预处理操作符被支持,它同TASM的ARG操作符相似。

      (*) `%local'预处理操作符。

      (*) `%stacksize'预处理操作符。

      (*) 某些操作符的无前缀形式被支持。 (`arg', `elif',`else', `endif', `if',
       `ifdef', `ifdifi', `ifndef', `include',`local')

      (*) 还有很多...

需要更多的关于操作符的信息,请参阅4.9的TASM兼容预处理操作符指令。

  2.1.17 `-w'选项: 使汇编警告信息有效或无效。
 
  NASM可以在汇编过程中监视很多的情况,其中很多是值得反馈给用户的,但这些情况
还不足以构成严重错误以使NASM停止产生输出文件。这些情况被以类似错误的形式
报告给用户,但在报告信息的前面加上'warning'字样。警告信息不会阻止NASM产生
输出文件并向操作系统返回成功信息。
 
  有些情况甚至还要宽松:他们仅仅是一些值得提供给用户的信息。所以,NASM支持'-w'
命令行选项。它以使特定类型的汇编警告信息输出有效或无效。这样的警告类型是
以命名来描述的,比如,'orphan-labels',你可以以下列的命令行选项让此类警告信息
得以输出:'-w+orphan-labels',或者以'-w-orphan-labels'让此类信息不能输出。
 
  可禁止的警告信息类型有下列一些:
 
      (*)`macro-params'包括以错误的参数个数调用多行的宏定义的警告。这类警告信息
缺省情况下是输出的,至于为什么你可能需要禁止它,请参阅4.3.1。

      (*)`orphan-labels'包含源文件行中没有指令却定义了一个没有结尾分号的label的
警告。缺省状况下,NASM不输出此类警告。如果你需要它,请参阅3.1的例子。

      (*) 'number-overflow'包含那些数值常数不符合32位格式警告信息(比如,你很容易打
      了很多的F,错误产生了'0x7fffffffffff')。这种警告信息缺省状况下是打开的。
         
  2.1.18 `-v'选项: 打印版本信息。

   输入'NASM -v'会显示你正使用的NASM的版本号,还有它被编译的时间。

如果你要提交bug报告,你可能需要版本号。

  2.1.19 `NASMENV'环境变量。

如果你定义了一个叫'NASMENV'的环境变量,程序会被把它认作是命令行选项附加的一
部分,它会在真正的命令行之前被处理。你可以通过在'NASMENV'中使用'-i'选项来定
义包含文件的标准搜索路径。

环境变量的值是通过空格符分隔的,所以值'-s ic:\nasmlib'会被看作两个单独的操
作。也正因为如此,意味着值'-dNAME='my name'不会象你预期的那样被处理, 因为它
会在空格符处被分开,NASM的命令行处理会被两个没有意义的字符串'-dNAME="my'和
'name"'给弄混。

为了解决这个问题,NASM为此提供了一个特性,如果你在'NASMENV'环境变量的第一个
字符处写上一个非减号字符,NASM就会把这个字符当作是选项的分隔符。所以把环
境变量设成'!-s!-ic:\nasmlib'跟'-s -ic:\nasmlib'没什么两样,但是
'!-dNAME="my name"就会正常工作了。

这个环境变量以前叫做'NASM',从版本0.98.32以后开始叫这个名字。

  2.2 MASM用户速成。

如果你曾使用MASM写程序,或者使用在MASM兼容模式下使用TASM, 或者使用'a86',
本节将阐述MASM与NASM语法之间的主要区别。如果你没有使用过MASM,那最好先
跳过这一节。

  2.2.1 NASM是大小写敏感的。

一个简单的区别是NASM是大小写敏感的。当你调用你的符号'foo','Foo',或
'FOO'时,它们是不同的。如果你在汇编'DOS'或'OS/2', '.OBJ'文件,你可以使
用'UPPERCASE'操作符来保证所有的导出到其他代码模式的符号都是大写的;但
是,在仅仅一个单独的模块中,NASM会区分大小写符事情。

  2.2.2 NASM需要方括号来引用内存地址。
 
NASM的设计思想是语法尽可能简洁。它的一个设计目标是,它将在被使用的过程
中,尽可能得让用户看到一个单行的NASM代码时,就可以说出它会产生什么操作
码。你可以在NASM中这样做,比如,如果你声明了:

      foo     equ     1
      bar     dw      2

      然后有两行的代码:

              mov     ax,foo
              mov     ax,bar

尽管它们有看上去完全相同的语法,但却产生了完全不同的操作码

NASM为了避免这种令人讨厌的情况,拥有一个相当简单的内存引用语未能。规则
是任何对内存中内容的存取操作必须要在地址上加上方括号。但任何对地址值
的操作不需要。所以,形如'mov ax,foo'的指令总是代表一个编译时常数,不管它
是一个 'EQU'或一个变量的地址;如果要取变量'bar'的内容,你必须与
'mov ax,[bar]'。
     
      这也意味着NASM不需要MASM的'OFFSET'关键字,因为MASM的代码'mov ax,offset bar'
同NASM的语法'mov ax,bar'是完全等效的。如果你希望让大量的MASM代码能够被
NASM汇编通过,你可以编写'%idefine offset'让预处理器把'OFFSET'处理成一个无
操作符。

这个问题在'a86'中就更混乱了。

NASM因为关注简洁性,同样不支持MASM和它的衍生产品支持的的混合语法,比如像
:'mov ax, table[bx]',这里,一个中括号外的部分加上括号内的一个部分引用一个
内存地址,上面代码的正确语法是:'mov ax,[table+bx] 。同样,'mov ax,es:[di]'
也是错误的,正确的应该是'mov ax,[es:di]'。

  2.2.3 NASM不存储变量的类型。

NASM被设计成不记住你声明的变量的类型。然而,MASM在看到'var dw 0'时会记住
类型,然后就可以隐式地合用'mov var, 2'给变量赋值。NASM不会记住关于变量
'var'的任何东西,除了它的位置,所以你必须显式地写上代码'mov word [var],2'。

因为这个原因,NASM不支持'LODS','MOVS','STOS','SCANS','CMPS','INS',或'OUTS'
指令,仅仅支持形如'LODSB','MOVSW',和'SCANSD'之灰的指令。它们都显式地指定
被处理的字符串的尺寸。

  2.2.4 NASM不会 `ASSUME'

作为NASM简洁性的一部分,它同样不支持'ASSUME'操作符。NASM不会记住你往段寄
存器里放了什么值,也不会自动产生段替换前缀。

  2.2.5 NASM不支持内存模型。

NASM同样不含有任何操作符来支持不同的16位内存模型。程序员需要自己跟踪那
些函数需要far call,哪些需要near call。并需要确定放置正确的'RET'指令('RETN'
或'RETF'; NASM接受'RET'作为'RETN'的另一种形式);另外程序员需要在调用外部函
数时在需要的编写CALL FAR指令,并必须跟踪哪些外部变量定义是far,哪些是near。

  2.2.6 浮点处理上的不同。

NASM使用跟MASM不同的浮点寄存器名:MASM叫它们'ST(0)','ST(1)'等,而'a86'叫它们
'0','1'等,NASM则叫它们'st0','st1'等。

在版本0.96上,NASM现在以跟MASM兼容汇编器同样的方式处理'nowait'形式的指令,
0.95以及更早的版本上的不同的处理方式主要是因为作者的误解。

  2.2.7 其他不同。
 
  由于历史的原因,NASM把MASM兼容汇编器的'TBYTE'写成'TWORD'。

NASM以跟MASM不同的一种方式声明未初始化的内存。MASM的程序员必须使用
'stack db 64 dup (?)', NASM需要这样写:'stack resb 64',读作"保留64字节"。为了
保持可移植性,NASM把'?'看作是符号名称中的一个有效的字符,所以你可以编写这样
的代码'? equ 0', 然后写'dw ?'可以做一些有用的事情。'DUP'还是一个不被支持的语法。

另外,宏与操作符的工作方式也与MASM完全不同,可以到参阅第4,第5章。

第三章 NASM语言
----------------

  3.1 NASM源程序行的组成。
 
就像很多其他的汇编器,每一行NASM源代码包含(除非它是一个宏,一个预处理操作
符,或一个汇编器操作符,参况第4,5章)下面四个部分的全部或某几个部分:

label:    instruction operands        ; comment

通常,这些域的大部分是可选的;label,instruction,comment存在或不存在都是允
许的。当然,operands域会因为instruction域的要求而必需存或必须不存在。

NASM使用反斜线(\)作为续行符;如果一个以一个反斜线结束,那第二行会被认为
是前面一行的一部分。

NASM对于一行中的空格符并没有严格的限制:labels可以在它们的前面有空格,或
其他任何东西。label后面的冒号同样也是可选的。(注意到,这意味着如果你想
要写一行'lodsb',但却错误地写成了'lodab',这仍将是有效的一行,但这一行不做
任何事情,只是定义了一个label。运行NASM时带上命令行选项'-w+orphan-labels'
会让NASM在你定义了一个不以冒号结尾的label时警告你。

labels中的有效的字符是字母,数字,'-','$','#','@','~','.'和'?'。但只有字母
'.',(具有特殊含义,参阅3.9),'_'和'?'可以作为标识符的开头。一个标识符还可
以加上一个'$'前缀,以表明它被作为一个标识符而不是保留字来处理。这样的话,
如果你想到链接进来的其他模块中定义了一个符号叫'eax',你可以用'$eax'在
NASM代码中引用它,以和寄存器的符号区分开。

instruction域可以包含任何机器指令:Pentium和P6指令,FPU指令,MMX指令还有甚
至没有公开的指令也会被支持。这些指令可以加上前缀'LOCK','REP','REPE/REPZ'
或'REPNE'/'REPNZ',通常,支持显示的地址尺寸和操作数尺寸前缀'A16','A32',
'O16'和'O32'。关于使用它们的一个例子在第九章给出。你也可以使用段寄存器
名作为指令前缀: 代码'es mov [bx],ax'等效于代码'mov [es:bx],ax'。我们推荐
后一种语法。因为它和语法中的其它语法特性一致。但是对于象'LODSB'这样的
指令,它没有操作数,但还是可以有一个段前缀, 对于'es lodsb'没有清晰地语法
处理方式
     
在使用一个前缀时,指令不是必须的,像'CS','A32','LOCK'或'REPE'这样的段前缀
可以单独出现在一行上,NASM仅仅产生一个前缀字节。

作为对实际机器指令的扩展,NASM同时提供了一定数量的伪操作指令,这在3.2节
详细描述。

指令操作数可以使用一定的格式:它们可以是寄存器,仅仅以寄存器名来表示(比
如:'ax','bp','ebx','cr0':NASM不使用'gas'的语法风格,在这种风格中,寄存器名
前必须加上一个'%'符号),或者它们可以是有效的地址(参阅3.3),常数(3.4),或
表达式。

对于浮点指令,NASM接受各种语法:你可以使用MASM支持的双操作数形式,或者你
可以使用NASM的在大多数情况下全用的单操作数形式。支持的所以指令的语法
细节可以参阅附录B。比如,你可以写:

              fadd    st1             ; this sets st0 := st0 + st1
              fadd    st0,st1         ; so does this
     
              fadd    st1,st0         ; this sets st1 := st1 + st0
              fadd    to st1          ; so does this

几乎所有的浮点指令在引用内存时必须使用以下前缀中的一个'DWORD',QWORD'
或'TWORD'来指明它所引用的内存的尺寸。

  3.2 伪指令。
 
伪指令是一些并不是真正的x86机器指令,但还是被用在了instruction域中的指
令,因为使用它们可以带来很大的方便。当前的伪指令有'DB','DW','DD','DQ'和
‘DT’,它们对应的未初始化指令是'RESB','RESW','RESD','RESQ'和'REST','INCBIN'
命令,'EQU'命令和'TIEMS'前缀。

  3.2.1 `DB'一类的伪指令: 声明已初始化的数据。

      在NASM中,`DB', `DW', `DD', `DQ'和`DT'经常被用来在输出文件中声明已初始化
      的数据,你可以多种方式使用它们:

            db    0x55                ; just the byte 0x55
            db    0x55,0x56,0x57      ; three bytes in succession
            db    'a',0x55            ; character constants are OK
            db    'hello',13,10,'$'   ; so are string constants
            dw    0x1234              ; 0x34 0x12
            dw    'a'                 ; 0x41 0x00 (it's just a number)
            dw    'ab'                ; 0x41 0x42 (character constant)
            dw    'abc'               ; 0x41 0x42 0x43 0x00 (string)
            dd    0x12345678          ; 0x78 0x56 0x34 0x12
            dd    1.234567e20         ; floating-point constant
            dq    1.234567e20         ; double-precision float
            dt    1.234567e20         ; extended-precision float

'DQ'和'DT'不接受数值常数或字符串常数作为操作数。

  3.2.2 `RESB'类的伪指令: 声明未初始化的数据。
 
`RESB', `RESW', `RESD', `RESQ' and `REST'被设计用在模块的BSS段中:它们声明
未初始化的存储空间。每一个带有单个操作数,用来表明字节数,字数,或双字数
或其他的需要保留单位。就像在2.2.7中所描述的,NASM不支持MASM/TASM的扣留未
初始化空间的语法'DW ?'或类似的东西:现在我们所描述的正是NASM自己的方式。
'RESB'类伪指令的操作数是有严格的语法的,参阅3.8。
比如:

      buffer:         resb    64              ; reserve 64 bytes
      wordvar:        resw    1               ; reserve a word
      realarray       resq    10              ; array of ten reals

  3.2.3 `INCBIN':包含其他二进制文件。
 
'INCBIN'是从老的Amiga汇编器DevPac中借过来的:它将一个二进制文件逐字逐句地
包含到输出文件中。这能很方便地在一个游戏可执行文件中包含中图像或声音数
据。它可以以下三种形式的任何一种使用:

          incbin  "file.dat"             ; include the whole file
          incbin  "file.dat",1024        ; skip the first 1024 bytes
          incbin  "file.dat",1024,512    ; skip the first 1024, and
                                         ; actually include at most 512

  3.2.4 `EQU': 定义常数。

'EQU'定义一个符号,代表一个常量值:当使用'EQU'时,源文件行上必须包含一个label。
'EQU'的行为就是把给出的label的名字定义成它的操作数(唯一)的值。定义是不可更
改的,比如:

message         db      'hello, world'
      msglen          equ     $-message

把'msglen'定义成了常量12。'msglen'不能再被重定义。这也不是一个预自理定义:
'msglen'的值只被计算一次,计算中使用到了'$'(参阅3.5)在此时的含义。注意
‘EQU’的操作数也是一个严格语法的表达式。(参阅3.8)

  3.2.5 `TIMES': 重复指令或数据。

前缀'TIMES'导致指令被汇编多次。它在某种程序上是NASM的与MASM兼容汇编器的
'DUP'语法的等价物。你可以这样写:

zerobuf:        times 64 db 0

或类似的东西,但'TEIMES'的能力远不止于此。'TIMES'的参数不仅仅是一个数值常
数,还有数值表达式,所以你可以这样做:

buffer: db      'hello, world'
times 64-$+buffer db ' '

它可以把'buffer'的长度精确地定义为64字节,’TIMES‘可以被用在一般地指令上,
所以你可像这要编写不展开的循环:

              times 100 movsb

注意在'times 100 resb 1'跟'resb 100'之间并没有显著的区别,除了后者在汇编
时会快上一百倍。

就像'EQU','RESB'它们一样, 'TIMES'的操作数也是严格语法的表达式。(见3.8)

注意'TIMES'不可以被用在宏上:原因是'TIMES'在宏被分析后再被处理,它允许
’TIMES'的参数包含像上面的'64-$+buffer'这样的表达式。要重复多于一行的代
码,或者一个宏,使用预处理指令'%rep'。

  3.3 有效地址
 
  一个有效地址是一个指令的操作数,它是对内存的一个引用。在NASM中,有效地址
  的语法是非常简单的:它由一个可计算的表达式组成,放在一个中括号内。比如:
 
      wordvar dw      123
              mov     ax,[wordvar]
              mov     ax,[wordvar+1]
              mov     ax,[es:wordvar+bx]

任何与上例不一致的表达都不是NASM中有效的内存引用,比如:'es:wordvar[bx]'。

更复杂一些的有效地址,比如含有多个寄存器的,也是以同样的方式工作:

              mov     eax,[ebx*2+ecx+offset]
              mov     ax,[bp+di+8]

NASM在这些有效地址上具有进行代数运算的能力,所以看似不合法的一些有效地址
使用上都是没有问题的:

          mov     eax,[ebx*5]             ; assembles as [ebx*4+ebx]
          mov     eax,[label1*2-label2]   ; ie [label1+(label1-label2)]

有些形式的有效地址在汇编后具有多种形式;在大多数情况下,NASM会自动产生
最小化的形式。比如,32位的有效地址'[eax*2+0]'和'[eax+eax]'在汇编后具有
完全不同的形式,NASM通常只会生成后者,因为前者会为0偏移多开辟4个字节。

NASM具有一种隐含的机制,它会对'[eax+ebx]'和'[ebx+eax]'产生不同的操作码;
通常,这是很有用的,因为'[esi+ebp]'和'[ebp+esi]'具有不同的缺省段寄存器。

尽管如此,你也可以使用关键字'BYTE','WORD','DWORD'和'NOSPLIT'强制NASM产
生特定形式的有效地址。如果你想让'[eax+3]'被汇编成具有一个double-word的
偏移域,而不是由NASM缺省产生一个字节的偏移。你可以使用'[dword eax+3]',
同样,你可以强制NASM为一个第一遍汇编时没有看见的小值产生一个一字节的偏
移(像这样的例子,可以参阅3.8)。比如:'[byte eax+offset]'。有一种特殊情
况,‘[byte eax]'会被汇编成'[eax+0]'。带有一个字节的0偏移。而'[dword
eax]'会带一个double-word的0偏移。而常用的形式,'[eax]'则不会带有偏移域。

当你希望在16位的代码中存取32位段中的数据时,上面所描述的形式是非常有用
的。关于这方面的更多信息,请参阅9.2。实际上,如果你要存取一个在已知偏
移地址处的数据,而这个地址又大于16位值,如果你不指定一个dword偏移,
NASM会让高位上的偏移值丢失。

类似的,NASM会把'[eax*2]'分裂成'[eax+eax]' ,因为这样可以让偏移域不存在
以此节省空间;实际上,它也把'[eax*2+offset]'分成'[eax+eax+offset]',你
可以使用‘NOSPLIT'关键字改变这种行为:`[nosplit eax*2]'会强制
`[eax*2+0]'按字面意思被处理。

  3.4 常数
 
NASM能理解四种不同类型的常数:数值,字符,字符串和浮点数。

  3.4.1 数值常数。
 
  一个数值常数就只是一个数值而已。NASM允许你以多种方式指定数值使用的
  进制,你可以以后缀'H','Q','B'来指定十六进制数,八进制数和二进制数,
  或者你可以用C风格的前缀'0x'表示十六进制数,或者以Borland Pascal风
  格的前缀'$'来表示十六进制数,注意,'$'前缀在标识符中具有双重职责
  (参阅3.1),所以一个以'$'作前缀的十六进制数值必须在'$'后紧跟数字,而
  不是字符。
 
  请看一些例子:

              mov     ax,100          ; decimal
              mov     ax,0a2h         ; hex
              mov     ax,$0a2         ; hex again: the 0 is required
              mov     ax,0xa2         ; hex yet again
              mov     ax,777q         ; octal
              mov     ax,10010011b    ; binary

  3.4.2 字符型常数。

一个字符常数最多由包含在双引号或单引号中的四个字符组成。引号的类型
与使用跟NASM其它地方没什么区别,但有一点,单引号中允许有双引号出现。

一个具有多个字符的字符常数会被little-endian order,如果你编写:

                mov eax,'abcd'

      产生的常数不会是`0x61626364',而是`0x64636261',所以你把常数存入内存
      的话,它会读成'abcd'而不是'dcba'。这也是奔腾的'CPUID'指令理解的字符常
      数形式(参阅B.4.34)

  3.4.3 字符串常数。
 
  字符串常数一般只被一些伪操作指令接受,比如'DB'类,还有'INCBIN'。

一个字符串常数和字符常数看上去很相像,但会长一些。它被处理成最大长
度的字符常数之间的连接。所以,以下两个语句是等价的:

            db    'hello'               ; string constant
            db    'h','e','l','l','o'   ; equivalent character constants

还有,下面的也是等价的:

            dd    'ninechars'           ; doubleword string constant
            dd    'nine','char','s'     ; becomes three doublewords
            db    'ninechars',0,0,0     ; and really looks like this

注意,如果作为'db'的操作数,类似'ab'的常数会被处理成字符串常量,因
为它作为字符常数的话,还不够短,因为,如果不这样,那'db 'ab'会跟
'db 'a''具有同样的效果,那是很愚蠢的。同样的,三字符或四字符常数会
在作为'dw'的操作数时被处理成字符串。

  3.4.4 浮点常量
 
  浮点常量只在作为'DD','DQ','DT'的操作数时被接受。它们以传统的形式表
  达:数值,然后一个句点,然后是可选的更多的数值,然后是选项'E'跟上
  一个指数。句点是强制必须有的,这样,NASM就可以把它们跟'dd 1'区分开,
  它只是声明一个整型常数,而'dd 1.0'声明一个浮点型常数。
 
一些例子:

            dd    1.2                     ; an easy one
            dq    1.e10                   ; 10,000,000,000
            dq    1.e+10                  ; synonymous with 1.e10
            dq    1.e-10                  ; 0.000 000 000 1
            dt    3.141592653589793238462 ; pi

NASM不能在编译时求浮点常数的值。这是因为NASM被设计为可移植的,尽管它
常产生x86处理器上的代码,汇编器本身却可以和ANSI C编译器一起运行在任
何系统上。所以,汇编器不能保证系统上总存在一个能处理Intel浮点数的浮
点单元。所以,NASM为了能够处理浮点运算,它必须含有它自己的一套完整
的浮点处理例程,它大大增加了汇编器的大小,却获得了并不多的好处。
 
  3.5 表达式
 
  NASM中的表达式语法跟C里的是非常相似的。
 
  NASM不能确定编译时在计算表达式时的整型数尺寸:因为NASM可以在64位系
  统上非常好的编译和运行,不要假设表达式总是在32位的寄存器中被计算的,
  所以要慎重地对待整型数溢出的情况。它并不总能正常的工作。NASM唯一能
  够保证的是:你至少拥有32位长度。

NASM在表达式中支持两个特殊的记号,即'$'和'$$',它们允许引用当前指令
的地址。'$'计算得到它本身所在源代码行的开始处的地址;所以你可以简
单地写这样的代码'jmp $'来表示无限循环。'$$'计算当前段开始处的地址,
所以你可以通过($-$$)找出你当前在段内的偏移。

NASM提供的运算符以运算优先级为序列举如下:

  3.5.1 `|': 位或运算符。
 
运算符'|'给出一个位级的或运算,所执行的操作与机器指令'or'是完全相
同的。位或是NASM中优先级最低的运算符。

  3.5.2 `^': 位异或运算符。

      `^' 提供位异或操作。

  3.5.3 `&': 位与运算符。

      `&' 提供位与运算。

  3.5.4 `<<' and `>>': 位移运算符。

      `<<' 提供位左移, 跟C中的实现一样,所以'5<<3'相当于把5乘上8。'>>'提
      供位右移。在NASM中,这样的位移总是无符号的,所以位移后,左侧总是以
      零填充,并不会有符号扩展。
     
  3.5.5 `+' and `-': 加与减运算符。
 
  '+'与'-'运算符提供完整的普通加减法功能。

  3.5.6 `*', `/', `//', `%'和`%%': 乘除法运算符。
 
  '*'是乘法运算符。'/'和'//'都是除法运算符,'/'是无符号除,'//'是带
  符号除。同样的,'%'和'%%'提供无符号与带符号的模运算。
 
  同ANSI C一样,NASM不保证对带符号模操作执行的操作的有效性。
 
  因为'%'符号也被宏预处理器使用,你必须保证不管是带符号还是无符号的
  模操作符都必须跟有空格。
 
  3.5.7 一元运算符: `+', `-', `~'和`SEG'

这些只作用于一个参数的一元运算符是NASM的表达式语法中优先级最高的。
'-'把它的操作数取反,'+'不作任何事情(它只是为了和'-'保持对称),
'~'对它的操作数取补码,而'SEG'提供它的操作数的段地址(在3.6中会有
详细解释)。

  3.6 `SEG'和`WRT'
 
  当写很大的16位程序时,必须把它分成很多段,这时,引用段内一个符号的
  地址的能力是非常有必要的,NASM提供了'SEG'操作符来实现这个功能。
 
  'SEG'操作符返回符号所在的首选段的段基址,即一个段基址,当符号的偏
  移地址以它为参考时,是有效的,所以,代码:

              mov     ax,seg symbol
              mov     es,ax
              mov     bx,symbol

总是在'ES:BX'中载入一个指向符号'symbol'的有效指针。

而事情往往可能比这还要复杂些:因为16位的段与组是可以相互重叠的,
你通常可能需要通过不同的段基址,而不是首选的段基址来引用一个符
号,NASM可以让你这样做,通过使用'WRT'关键字,你可以这样写:

              mov     ax,weird_seg        ; weird_seg is a segment base
              mov     es,ax
              mov     bx,symbol wrt weird_seg

会在'ES:BX'中载入一个不同的,但功能上却是相同的指向'symbol'的指
针。

通过使用'call segment:offset',NASM提供fall call(段内)和jump,这里
'segment'和'offset'都以立即数的形式出现。所以要调用一个远过程,你
可以如下编写代码:

              call    (seg procedure):procedure
              call    weird_seg:(procedure wrt weird_seg)

(上面的圆括号只是为了说明方便,实际使用中并不需要)


NASM支持形如'call far procedure'的语法,跟上面第一句是等价的。'jmp'
的工作方式跟'call'在这里完全相同。

在数据段中要声明一个指向数据元素的远指针,可以象下面这样写:

              dw      symbol, seg symbol

NASM没有提供更便利的写法,但你可以用宏自己建造一个。

  3.7 `STRICT': 约束优化。
 
  当在汇编时把优化器打开到2或更高级的时候(参阅2.1.15)。NASM会使用
  尺寸约束('BYTE','WORD','DWORD','QWORD',或'TWORD'),会给它们尽可
  能小的尺寸。关键字'STRICT'用来制约这种优化,强制一个特定的操作
  数为一个特定的尺寸。比如,当优化器打开,并在'BITS 16'模式下:
 
              push dword 33

      会被编码成 `66 6A 21',而

              push strict dword 33

会被编码成六个字节,带有一个完整的双字立即数`66 68 21 00 00 00'.

而当优化器关闭时,不管'STRICT'有没有使用,都会产生相同的代码。

  3.8 临界表达式。
 
  NASM的一个限制是它是一个两遍的汇编器;不像TASM和其它汇编器,它总是
  只做两遍汇编。所以它就不能处理那些非常复杂的需要三遍甚至更多遍汇编
  的源代码。
 
  第一遍汇编是用于确定所有的代码与数据的尺寸大小,这样的话,在第二遍
  产生代码的时候,就可以知道代码引用的所有符号地址。所以,有一件事
  NASM不能处理,那就是一段代码的尺寸依赖于另一个符号值,而这个符号又
  在这段代码的后面被声明。比如:

              times (label-$) db 0
            label:  db      'Where am I?'

'TIMES'的参数本来是可以合法得进行计算的,但NASM中不允许这样做,因为
它在第一次看到TIMES时的时候并不知道它的尺寸大小。它会拒绝这样的代码。

              times (label-$+1) db 0
              label:  db      'NOW where am I?'

在上面的代码中,TIMES的参数是错误的。

NASM使用一个叫做临界表达式的概念,以禁止上述的这些例子,临界表达式
被定义为一个表达式,它所需要的值在第一遍汇编时都是可计算的,所以,
该表达式所依赖的符号都是之前已经定义了的,'TIMES'前缀的参数就是一个
临界表达式;同样的原因,'RESB'类的伪指令的参数也是临界表达式。

临界表达式可能会出现下面这样的情况:


                       mov     ax,symbol1
       symbol1         equ     symbol2
       symbol2:

在第一遍的时候,NASM不能确定'symbol1'的值,因为'symbol1'被定义成等于
'symbols2',而这时,NASM还没有看到symbol2。所以在第二遍的时候,当它遇
上'mov ax,symbol1',它不能为它产生正确的代码,因为它还没有知道'symbol1'
的值。当到达下一行的时候,它又看到了'EQU',这时它可以确定symbol1的值
了,但这时已经太晚了。

NASM为了避免此类问题,把'EQU'右侧的表达式也定义为临界表达式,所以,
'symbol1'的定义在第一遍的时候就会被拒绝。

这里还有一个关于前向引用的问题:考虑下面的代码段:

              mov     eax,[ebx+offset]
      offset  equ     10

NASM在第一遍的时候,必须在不知道'offset'值的情况下计算指令
'mov eax,[ebx+offset]'的尺寸大小。它没有办法知道'offset'足够小,足以
放在一个字节的偏移域中,所以,它以产生一个短形式的有效地址编码的方
式来解决这个问题;在第一遍中,它所知道的所有关于'offset'的情况是:它
可能是代码段中的一个符号,而且,它可能需要四字节的形式。所以,它强制
这条指令的长度为适合四字节地址域的长度。在第二遍的时候,这个决定已经
作出了,它保持使这条指令很长,所以,这种情况下产生的代码没有足够的小,
这个问题可以通过先定义offset的办法得到解决,或者强制有效地址的尺寸大
小,象这样写代码:
      [byte ebx+offset]

  3.9 本地Labels

NASM对于那些以一个句点开始的符号会作特殊处理,一个以单个句点开始的
Label会被处理成本地label, 这意味着它会跟前面一个非本地label相关联.
比如:

      label1  ; some code
     
      .loop
              ; some more code
     
              jne     .loop
              ret
     
      label2  ; some code
     
      .loop
              ; some more code
     
              jne     .loop
              ret

上面的代码片断中,每一个'JNE'指令跳至离它较近的前面的一行上,因为'.loop'
的两个定义通过与它们前面的非本地Label相关联而被分离开来了。

对于本地Label的处理方式是从老的Amiga汇编器DevPac中借鉴过来的;尽管
如此,NASM提供了进一步的性能,允许从另一段代码中调用本地labels。这
是通过在本地label的前面加上非本地label前缀实现的:第一个.loop实际上被
定义为'label1.loop',而第二个符号被记作'label2.loop'。所以你确实需要
的话你可写:

      label3  ; some more code
               ; and some more
     
               jmp label1.loop

有时,这是很有用的(比如在使用宏的时候),可以定义一个label,它可以
在任何地方被引用,但它不会对常规的本地label机制产生干扰。这样的
label不能是非本地label,因为非本地label会对本地labels的重复定义与
引用产生干扰;也不能是本地的,因为这样定义的宏就不能知道label的全
称了。所以NASM引进了第三类label,它只在宏定义中有用:如果一个label
以一个前缀'..@'开始,它不会对本地label产生干扰,所以,你可以写:

      label1:                         ; a non-local label
      .local:                         ; this is really label1.local
      ..@foo:                         ; this is a special symbol
      label2:                         ; another non-local label
      .local:                         ; this is really label2.local
     
              jmp     ..@foo          ; this will jump three lines up

NASM还能定义其他的特殊符号,比如以两个句点开始的符号,比如
'..start'被用来指定'.obj'输出文件的执行入口。(参阅6.2.6)
     
第四章 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
&n

posted @ 2006-02-28 09:58  xCvM  阅读(2896)  评论(0)    收藏  举报