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,'。
这也意味着NASM不需要MASM的'OFFSET'关键字,因为MASM的代码'mov ax,offset bar'
同NASM的语法'mov ax,bar'是完全等效的。如果你希望让大量的MASM代码能够被
NASM汇编通过,你可以编写'%idefine offset'让预处理器把'OFFSET'处理成一个无
操作符。
这个问题在'a86'中就更混乱了。
NASM因为关注简洁性,同样不支持MASM和它的衍生产品支持的的混合语法,比如像
:'mov ax, table',这里,一个中括号外的部分加上括号内的一个部分引用一个
内存地址,上面代码的正确语法是:'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 ,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'。
更复杂一些的有效地址,比如含有多个寄存器的,也是以同样的方式工作:
mov eax,[ebx*2+ecx+offset]
mov ax,
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)。比如:''。有一种特殊情
况,‘'会被汇编成'[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的办法得到解决,或者强制有效地址的尺寸大
小,象这样写代码:
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)