邪恶八进制信息安全团队技术讨论组's Archiver

EvilOctal 2005-10-22 02:40

[转载]NASM中文文档资料

信息来源:剑雨销香 CNIDP组织 联合制作

目录详细介绍

第一章:简介

第二章:运行NASM

第三章:NASM语法介绍

第四章:NASM的预处理器

第五章:汇编方向性

第六章:输出格式

第七章:写16位代码(DOS,Windows 3/3.1)

第八章:写32位代码(Unix,Win32,DJGPP)

第九章:16位与32位混和编程

第十章:问题解答

---------------- Copyright (c) 2003 CNIDP 剑雨销香 all rights reserved ----------------

本来我们也开始翻译这个,想不到已经有人翻译好了,衷心感谢那些翻译的人!

--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:04:28

--
nasm网际编译器手册:
此手册为NASM,网际汇编器:一个目标为Intelx86系列处理器,开放源码的编译器。
第一章:简介
1.1节 什么是NASM?
1.1.1节 为什么用另外的编译器?
1.1.2节 许可证
1.2节 联络方式
1.3节 安装
1.3.1节 在MS_DOS或Windows下安装NASM
1.3.2节 在Unix下安装NASM

第二章:运行NASM
第2.1节 :那NASM命令行参数
第2.1.1节:-o参数:指定输出文件名
第2.1.2节:-f参数:指定输出文件格式
第2.1.3节:-l参数:输出列表文件
第2.1.4节:-E参数:输出错误信息到文件中
第2.1.5节:-s参数:输出错误信息到屏幕
第2.1.6节:-i参数:指定头文件路径
第2.1.7节:-p参数:预编译头文件
第2.1.8节:-d参数:预编译宏
第2.1.9节:-u参数:取消宏定义
第2.1.10节:-e参数:只预处理
第2.1.11节:-a参数:根本不预处理
第2.1.12节:-w参数:允许或禁止汇编警告信息
第2.1.13节:NASM的环境变量
第2.2节 对于MASM用户的快速入门
第2.2.1节 NASM是区分大小写的
第2.2.2节 NASM对内存操作需要方括号
第2.2.3节 NASM不存储存变量类型
第2.2.4节 NASM不用ASSUME关键字
第2.2.5节 NASM不支持内存模型
第2.2.6节 浮点数方面的区别
第2.2.7节 其它区别
第三章
第3.1节 NASM源码行号的分布
第3.2节 伪指令
第3.2.1节 DB及相关指令:声明初始化数据
第3.2.2节 RESB及相关指令:声明未初始化数据
第3.2.3节 INCBIN:包含外部二进制文件
第3.2.4节 EQU:定义常量
第3.2.5节 TIMES:重复指令或数据
第3.3节 有效地址
第3.4节 常量
第3.4.1节 数字常量
第3.4.2节 字符常量
第3.4.3节 字符串常量
第3.4.4节 浮点数常量
第3.5节 表达式
第3.5.1节 |:位运算符或
第3.5.2节 ^位运算符异或
第3.5.3节 &位运算符与
第3.5.4节 <<和>>:位移动操作符
第3.5.5节 +和-:加和减操作符
第3.5.6节 *,/,//,%和%%:乘法与除法操作
第3.5.7节 一元操作符:+,-,~和SEG
第3.6节 SEG和WRT
第3.7节 临界资源表达式
第3.8节 本地变量
第4章 NASM的预处理器
第4.1节 单行的宏操作
第4.1.1节 正常处理方式:%define
第4.1.2节 立即处理的单行宏:%xdefine
第4.1.3节 未定义的宏:%undef
第4.1.4节 预处理变量:%assign
第4.2节 多行宏的操作
第4.2.1节 重载多行宏:%macro
第4.2.2节 本地宏标识
第4.2.3节 贪婪的宏参数
第4.2.4节 默认宏参数
第4.2.5节 %0:宏参数记数
第4.2.6节 %rotate:旋转宏参数
第4.2.7节 控制台的宏参数
第4.2.8节 用宏参数做为条件语句
第4.2.9节 禁止列表展开
第4.3节 条件汇编
第4.3.1节 %ifdef:测试单行宏的存在性
第4.3.2节 %ifctx:测试上下相连的堆栈
第4.3.3节 %if:测试任意数字表达式
第4.3.4节 %ifidn和%ifidni:测试试验性文本标记
第4.3.5节 %ifid,%ifnum,%ifstr:测试标记类型
第4.3.6节 %error:报告用户自定义的错误
第4.4节 预处理循环:%rep
第4.5节 包含其它文件
第4.6节 上下相关的堆栈
第4.6.1节 %push和%pop:创建和移走上下环境
第4.6.2节 前后相关的本地变量
第4.6.3节 前后相关的单行宏
第4.6.4节 %repl:重命名一个前后相关环境
第4.6.5节 用前后相关堆栈的一个例子:IFs块
第4.7节 标准宏
第4.7.1节 _NASM_MAJOR_和_NASM_MINOR_:NASM版本信息
第4.7.2节 _FLLE_和_LINE_:文件名和行号
第4.7.3节 STRUC 与ENDSTRUC:定义结构数据类型
第4.7.4节 ISTRUC,AT 与IEND :定义结构的变量
第4.7.5节 ALIGN 和ALIGNB:数据对齐方式
第5章 汇编方向性
第5.1节 BITS:指定目标处理器模式
第5.2节 SECTION或SEGMENT:改变和定义段
第5.2.1节 _SECT_宏
第5.3节 ABSOLUTE:定义绝对标号
第5.4节 EXTERN:从其它模块导入符号
第5.5节 GLOBAL:向其它模块输入信息
第5.6节 COMMON:定义通用数据块

第6章 输出格式
第6.1节 bin:平坦二进制输出
第6.1.1节 ORG:二进制文件源码
第6.1.2节 bin扩展到SECTION方向
第6.2节 obj:微软的OMF目标文件格式
第6.2.1节 obj扩展到SECTION方向
第6.2.2节 GROUP:定义段的组信息
第6.2.3节 UPPERCASE:禁止输出时大小区别
第6.2.4节 IMPORT:导入DLL符号
第6.2.5节 EXPORT:导出DLL符号
第6.2.6节 ..start:定义程序输入点位置
第6.2.7节 obj 扩展到EXTERN 方向
第6.2.8节 obj扩展到COMMON方向
第6.3节 win32:微软Win32目标文件
第6.3.1节 win32:扩展到SECTION方向
第6.4节 coff:通用目标文件格式
第6.5节 elf:Linux ELF目标文件
第6.5.1节 elf扩展到SECTION方向
第6.5.2节 位置独立代码:elf指定符号和WRT
第6.5.3节 elf扩展到GLOBAL方向
第6.5.4节 elf扩展到COMMON方向
第6.6节 aout:Linux a.out目标文件
第6.7节 aoutb:NetBSD/FreeBSD/OpenBSD a.out目标文件
第6.8节 as86:Linux as86 目标文件
第6.9节 rdf:重定位动态目标文件格式
第6.9.1节 需求库:LIBERARY方向
第6.10节 dbg:Debuging 格式

第7章 写16位代码(DOS,Windows 3/3.1)
第7.1节 生成.EXE文件
第7.1.1节 用obj格式生成.exe文件
第7.1.2节 用bin格式生成.exe文件
第7.2节 生成.com文件
第7.2.1节 用bin格式生成.com文件
第7.2.2节 用obj格式生成.com文件
第7.3节 生成.sys文件
第7.4节 与16位C程序的接口
第7.4.1节 外部符号名
第7.4.2节 内存模型
第7.4.3节 函数定义与函数调用
第7.4.4节 数据项的访问
第7.4.5节 c16.mac:16位C接口的帮助宏
第7.5节 Borland Pascal程序接口
第7.5.1节 Pascal调用转换
第7.5.2节 Borland Pascal段命名限制
第7.5.3节 在Pascal程序中用c16.mac
第8章 写32位代码(Unix,Win32,DJGPP)
第8.1节 32位C语言接口
第8.1.1节 外部符号名
第8.1.2节 函数定义与函数调用
第8.1.3节 数据项的访问
第8.1.4节 c32.mac:32位C接口的帮助宏
第8.2节 写NetBSD/FreeBSD/OpenBSD和Linux/ELF共享库
第8.2.1节 包含GOT地址
第8.2.2节 寻找程序本地数据项
第8.2.3节 寻找外部和通用数据项
第8.2.4节 导出符号到库用户
第8.2.5节 在库外部调用过程
第8.2.6节 生成库文件

第9章16位与32位混和编程
第9.1节 混和尺寸的跳转
第9.2节 不同尺寸段间的寻址
第9.3节 其它的混和尺寸指令

第10章 问题解答
第10.1节 常见问题
第10.1.1节 NASM 生成无效的代码
第10.1.2节 我程序中跳转出界了
第10.1.3节 ORG不能工作了
第10.1.4节 TIMES 不能工作了
第10.2节 Bugs程序中的问题

--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:06:03

--

第1章 简介

1.1 什么是NASM?
网际汇编器,NASM是一个模块化和便携性的80x86汇编编译器。它支持多种目标格式包含Linux的a.out
,ELF,NetBSD/FreeBSD,COFF,Microsoft的16位OBJ和Win32。 它输出平坦模式的二进制文件。
它在语法设计上简单且容易理解,和Intel的相似但没有那么复杂。 它支持Pentium,P6和MMX操作码,
并且宏操作方面兼容。


1.1.1 为什么不用其它编译器?
网际编译器是在基于comp.lang.asm.x86(也可能是alt.lang.asm我记不清了)上的思想成长
起来的。它在本质上不象周围很好的自由x86系列编译器, 并且应有人写一个。
a86是比较好的, 但并不免费,通常你不能得到任何32位兼容的除非你付费。 它只支持DOS。
gas是免费的, 并且适应于DOS和Unix, 但它不是很好用, 由于它被设计成相对于gcc的后端,
所以必须给它输入一些正确的密码。 导致它的错误检查是很小。 因此, 从这方面来看待和
真正写些代码来说,它的语法是很难懂的。 另外你不能用它写16位代码(正常情况下)。
as86是Linux专用的, 并且有很多文档(至少对我来说)。
MASM不是很好用, 它太贵了, 并且只能在DOS下运行。
TASM好一些, 但仍在和MASM相兼容, 意味着将会有不计其数的指令的官文。 它的语法本质上
和MASM相同。 但它也很贵。 只在DOS下运行。
因此, 这里为了编码的快乐,用NASM。现在它仍然处理试验阶段-我不能保证它这些编译器好,但请向我
们报告程序中的问题,修改意见及帮助信息, 和任何你现有的信息。 (感谢很多人会这样做, 你知道
你也会) , 我们将不断的发展它。

1.1.2 软件许可协议
请查看许可协议文件,做为NASM描述档案的一部分提供, 你可以在许可协议下使用NASM。

1.2 联系方式
当前版本的NASM(从0.98)是由H.Peter Anvin([email]hpa@zytor.com[/email])维护的。
如果你想报告任何程序问题, 请先读一下第10.2节.
NASM有一个网页为:htt//www.cryogen.com/Nasm.
原始作者可邮寄电子信箱: [email]jules@earthcorp.com[/email] 和[email]anakin@pobox.com[/email]

最新版本的NASM已经上传到ftp.kernel.org,sunsite.unc.edu,ftp.simtel.net和ftp.coast.net.
通告将发布在 comp.lang.asm.x86,alt.lang.asm,comp.os.linux.announce和
comp.archives.msdos.announce(最后一个将自动传到ftp.simtel.net上)
如果你没有新闻组可以访问,或者更喜欢用电子邮件进行交流,你可以发送一行包含
subscribe nasm-announce的内容的邮件到[email]majordomo@linux.kernel.org[/email].
如果你想了解NASM beta版的有关信息,请发送一封含有subscribe nasm-beta信息的电子邮件到
[url]http://www.pop417.com/bbs/mailtmajordomo@linux.kernel.org.[/url]

1.3安装


1.3.1 在MS_DOS 或Windows下安装NASM
当你得到NASM的DOS版本的文件时,nasmXXX.zip(XXX表示NASM的版本号),将它解压到当前目录下
(例如:C:\\NASM)
这个文件包含4个执行文件:NASM的执行文件为:nasm.exe和nasmw.exe和NDISASM的执行文件:
ndisasm.exe,ndisasmw.exe.这个文件夹里文件名后有w为一个win32可执行文件,被设计在windows95
或windows NT下运行,另外的是16位的DOS执行程序。
NASM文件要运行它的自运行文件,因此拷贝(至少)nasm.exe和nasmw.exe的一个到你的目录下,或
选择一个编辑autoexec.bat文件将nasm的路径加到你的PATH目录上。 (如果你要节省空间可以删除它);
然而,你可以保留这个文件或测试程序。
如果你下载了DOS的源码文件包, nasmXXXs.zip,nasm目录将包含完事的NASM源代码,你可以选择一个
MAKEFILE(推荐)来重新编译NASM,README文件列出了这些MAKEFILE之间的不同和用什么编译程序编译
的。 注意源文件:insnsa.c,insnsd.c,insnsi.h和insnsn.c是自动从Perl角本文件:
主指令列表文件insns.dat生成的。
文件macros.c是通过另一个Perl角本从standard.mac生成的。
虽然NASM 0.98的发布包含这些生成文件,但如果你改变了insns.dat,standard.mac或相关文档,
你还是需要重新编译它。(因此你需要一个Perl解释器),也许以后的源码发布文档根本就不包含
这些文件。Perl在不同平台(包含DOS和Windows)的输出变化,在[url]http://www.cpan.org/[/url]可以找到。

1.3.2 在Unix下安装NASM
一旦你得到NASM的Unix源码文件:nasm-X.XX.tar.gz(这里X.XX表示档案中包含NASM的版本号),你
就可以把它解压到如:/usr/local/src的目录下,这个档案一旦解压会生成它自己的子目录:nasm-X.XX.
NASM是一个自配置压缩包:你解压后,用cd命令到它解压的目录下,然后用type ./configure命令,
这个shell角本将会找到最好的c编译器来编译NASM并建立相应的Makefiles文件。
一旦NASM自动配置后,你就可以用命令type make来编译nasm和ndisasm二进制文件,然后用命令install
来将它们安装到/usr/loacl/bin下,将nasm.1和ndisasm.1的帮助文件安装到/usr/local/man/man1目录
下。对于有选择的,你可以给出如--prefix来配置角本(关于安装的详细信息见INSTALL文件),或者安装
你自己的程序。
NASM也提供一些关于RDOFF自定义目标文件格式的工具,这些在NASM压缩包的rdoff子目录下,如果你想用
他们的话。
如果NASM自配置失败的话,你可以用Unix的失败后退makefile文件Makefile.unx来编译它。对Makefile
文件拷贝或重命名文件, 并试关重新编译。在rdoff子目录下也有一个Makefile.unx文件。



--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:09:55

--

第二章 运行NASM

2.1NASM命令行参数
为了汇编一个文件,你必须用一个命令行来实现:
nasm -f [-o ]
例子:
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的.你应该用use -f aout来代替(Linux a.out系统被认为淘汰了,
现在很小见)
象一些Unix编译器,NASM是默认的除非它出错;你瘵不能看到它输出任何信息,除非它显示错误信息。


2.1.1 -o参数 指定输出文件名
NASM正常情况下将为你指定输出文件名;它对目标文件格式是独立的。对于微软的目标格式文件(obj和
win32),它将从你的源文件移走asm扩展名(或者你用-NASM来忽略)并加上obj扩展名.对于Unix目标文件格式

(aout.coff,elf和as86),它将为rdf加上.o,它将用.rdf,而对于二进制文件它只是简单的移去扩展名.
所以myfile.asm文件将生成名为myfile的输出文件.如果输出文件存在,NASM将覆盖它,除非它有相同名字
的输入文件, 这种情况它将组出一个警告并用nasm.out来代替输出文件名。
这种替换行为是不允许的,NASM提供-o命令行参数来允许你指定你想要的输出文件名。
你可以在-o后边加空格或不加空格,然后在加文件名,如:
nasm -f bin program.asm -o program.com
nasm -f bin driver.asm -odriver.sys


2.1.2 -f参数:指定输出文件格式
如果你不用-f参数指定文件格式,NASM将为你指定一种格式。在NASM的发布版本中,默认的为bin;
如果你编译你自己的NASM拷贝,你可以在编译时重新定义OF_DEFAULT并且选择你想默认的文件名。
象 -o参数一样,在-f 和输出文件格式之间的空格是可选的。所以-f elf 和-felf都是有效。
一个完整的输出文件格式列表可能过命令nasm -h来得到。


2.1.3 -l参数:生成列表文件
如果你用NASM的-l参数,后面加文件名(通常中间加),NASM将生成一个源码列表文件。生成的代码和
地址被列在左边,还有真正的源码,并带有多行宏的扩展。(除了指定在要求在源码列表不扩展:见
第4.2.9节)在右边。例如nasm -f elf myfile.asm -l myfile.lst

2.1.4 -E参数:将错误信息输出到文件中
在MS-DOS下将程序的错误信息输入到一个文件中是比较困难(虽然这有方法),因此NASM通常将警告信息
和错误信息显示在屏幕上,如果你想捕获这些信息并存入到编辑器中将是比较困难的事.
NASM因此提供了-E参数然后加上文件名将会使错误输出到文件中而不是标准错误设备上。因此你可以通过
命令typing nasm -E myfile.err -f obj myfile.asm 重新将错误信息输出到文件中。

2.1.5 -s参数:将错误输出到标准输出上
-s参数将错误信息重新定位到标准输出面不是标准错误上,因此它能在MS-DOS下运行。为了汇编文件
myfile.asm并且将它通过管道输出到其它程序中,你可以通命令:
nasm -s -f obj myfile.asm |more
关于-E参数的详细信息请看第2.1.4节

2.1.6 -i参数:头文件设置路径
当NASM在源文件中查看%include 定向符号时(详细信息见第4.5节),它将搜索在命令行中指定的目录
而不只是当前目录中文件。你也可以从一个宏库文件中包含文件,例如,用命令:
nasm -ic:\\macrolib\\ -f obj myfile.asm

(通常在-i和路径名中的空格是可选的)
NASM ,在源码级兼容方面比较感兴趣,不用理解不同操作系统间文件名的转换。你提供的字符串做为-i参数
将写到头文件的名字。因此在上面的例子中加反斜杠是需要的。在Unix下,在前面加上一个斜杠也是需要的。
(你可以利用这点,如果你真想做的话,那么参数-ifoo将引起%include "bar.i"来搜索文件foobar.i...)

如果你想要定义一个标准头文件搜索路径,在Unix系统上象/usr/include,你应该在NASM的环境变量中用
一个或多个-i定向符。(见第2.1.13节)
为了与许多的C编译器的Makefile相兼容,这个参数也可以指定为-I。


2.1.7 -p参数:预包含头文件
NASM允许你通过-p参数指定文件预包含到你的源文件中,所以命令:
nasm myfile.asm -p myinc.inc
和运行nasm myfile.asm 然后在文件的开始置换%include "myinc.in"等价。
和-I,-D,-U参数一样,这个操作也可以写成-P。

2.1.8 -d 参数:预定义宏
-p参数给出了一个在源文件开始处可选择%include 定向符的方法,-d参数给出了可选择%define定向符的
方法。你可以将代码 nasm myfile.asm -dFOO=100写成%define FOO 100D 在文件开始时,你可以关掉宏
的值如:-dFOO 等价与%define FOO。这个定向符的表格对汇编时的参数选择有用, 如%ifdef
例如-dDEBUG。
为了和许多C编译器的Makefile文件兼容,这个参数可以写成-D.


2.1.9 -u参数:取消一个宏的定义
-u参数在已经预定义一个宏的情况下取消一个宏的定义。并且自动取消前面命令行中的-p或-d参数。
例如:命令行参数:nasm myfile.asm -dFOO=100 -uFOO

的作用是在程序中取消以前定义的FOO宏。这对于在Makefile中重载参数比较有用。
为了许多C编译器的Makefile兼容,这个参数可以指定为-U。


2.1.10 -e参数:只进行预处理
NASM 允许在它运行时进行预处理到某一点。用-e参数(不需参数)将会使NASM在它的输入文件中进行预处理,
扩展所有宏参数,移去注释和预处理符,并且在标准输出上显示结果文件(也可以用-o参数将结果存到
文件中)。
这个参数不能应用于那些对独立符号值进行预处理的操作如:%assign tablesize($-tablestart)

将在只进行预处理模式中引起一个错误。


2.1.11 -a参数:根本不进行预处理
如果NASM被做为一个编译器的后端使用,为了节约时间和提高速度,它可能假设编译器已经进行了预处理,
而完全不需要预处理。-a参数不需要参数,NASM将肢用一个什么都不做的stub预处理来替换这个参数。


2.1.12 -w参数:允许/禁止汇编警告
NASM在汇编码方案的过程中将检测很多条件,来提醒用户,但是一个错误不致于生成一个文件是这些条件
做为链接错误来报错并且在消息的前面有‘warning’这个词,Warning 不会阻止NASM生成文件并返回给系统
一个成功的状态。
有些情况可以忽略:它们只是想提醒用户。因而NASM允许用户用-w命令行参数来允许可禁止指定类别的
警告。警告类别可以用名字来描述,例如:orphan-labels,你可以用-w+orphan-labels来允许这个级别的
警告用-w-orphan-labels来禁止它.
可用的警告信息有:
macro-params 包含所有错误号参数执行的多行宏警告信息.这个警告类别默认是允许的;关于如
何禁止它的例子见第4.2.1节.
orphan-labels 包含警告哪些没有指令但用冒号定义的标号的源码行.NASM默认情况下没有对这种
条件进行规定;如果你想进一步了解它见第3.1节的例子.
number-overflow 包含不适用于32位的数字信息(例如:打入太多个f而产生0x7ffffffff的错误)
这个警告默认情况是允许的.


2.1.13 NASM的环境变量
如果你定义了一个叫NASM的环境变量,程序将会把它解释为为一个外部命令行参数,在真正的命令行以前
进行处理.你可以用这个来为头文件定义标准的搜索路径.通过在NASM变量中用-i操作.
这个变量的值用空格分开,以致于-s -ic:\\nasmlib 将做为2个单独的操作.然而,这意味着变量
-dNAME="my name"不会做你想要做的事.因为它将在空格处分开并且NASM命令行处理器将会得到两个无意义
的字 -dNAME="my and name".
为了可以这样做,NASM提供了一个操作,如果你在变量的前面字符加一个不为负号的字符,NASM将会这个符号
做为分隔符,所以设置变量 !-s!-ic:\\nasmlib 等价于-s-ic:\\nasmlib,但!-dNAME="my name"将会工作.


2.2 MASM用户快速入门
如果你用MASM写过程序或用与MASM相兼容的TASM或a86,这节试着说明MASM与NASM在语法上的主要区别.
如果你没有用过MASM,哪么请跳过这一节.


2.2.1 NASM是大小写区分的
一个简单的区别是NASM是区分大小写的,你调用标号foo,Foo或FOO是不一样的。如果你编译过DOS或OS/2的
.OBJ文件,你可以执行UPERCASE 指令(文档见第6.2节)来保证向其它模块输出的所有符号都为大写。
但在这里NASM只在标号间对它们进行区分。


2.2.2 NASM 需要用方括号引用内存
NASM在语法上被设计为尽量简单。一个实用的设计目标为用户看一行NASM代码时就告诉它产生的操作码是
什么。你不能在MASM中这样做:如果你定义如下符号
foo equ 1
bar dw 2
那么下面两行代码
mov ax,foo
mov ax,bar
将产生完全不同的操作码,尽管他们有相同的语法。NASM避免这种由有相同语法而产生不同情况的内存
引用。这个规则不难实现:对于任何访问内存内容的地方用方括号,而任何访问变量地址的则不用。
所以一条指令:mov ax,foo将会总是参考编译时的内容,无论它是一个EQU还是一个变量的地址;
访问一个变量的内容时,你必须用mov ax,[bar]
这也意味着NASM不需要MASM的OFFSET关键字,所以MASM的mov ax,offset bar与NASM的mov ax,bar意义
相同。如果你试着在NASM得到大量的MASM代码,你将会用%idefine offset来生成物大量的OFFSET预处理
关键字做为无意义的工作。
这会在a86中产生一些迷惑,用一个冒号结束一个标号的定义和一个变量区分引起a86采用NASM形式的语义。
所以在a86中,mov ax,var有另外的意义决定于var是否被定义成变量;dw 0(一个标号)或者 var dw 0(
一个WORD尺寸的变量)。NASM比较起来是很简单的:任何符号都是一个标号。
NASM在简单性方面很出色,也不支持象MASM复杂的语法格式和它的拷贝,如:mov ax,[table+bx],同样的
mov ax,es:[di]是错的而mov ax,[es:di]是对的。


2.2.3 NASM不存储变量类型
NASM在设计时,就选择了不记忆你定义的变量类型。然而看到var dw 0 MASM将记下你定义了一个字长的变量,
然后在指令mov var,2前 向这个尺寸填入一个值,而NASM则故意不记这个符号除非它开始时,所以你必须用
这样的指令:mov word [var],2.
由于这个原因,NASM不支持LODS,MOVS,STOS,SCAS,CMPS,INS或OUTS指令,但支持表格处理如:LODSB,MOVSW和
SCASD,这些指令明确指定了处理字串的单位。


2.2.4 NASM不用ASSUME.
做为NASM简单性的一部分,它不支持ASSUME关键字,NASM不保留你将什么值放入段寄存器的动作.并且也
永远不会自动生成一个段重载前缀。


2.2.5 NASM不支持内存模型
NASM不支持任何提供16位内存模型的操作符。程序员必须保留哪个函数为远调用哪个函数为近调用。
对于放入RET指令的正确表格也是负责的。(RETN或RETF;NASM接受RET做为RETN表格的一个替换);另外,
程序员也必须自己处理CALL FAR指令对于当外部调用函数时,并且也必须保留外部变量是far 还是near的
信息。


2.2.6 浮点数的区别
NASM 用不同的名字引用浮点寄存器;MASM叫它们为ST(0),ST(1),a86叫它们0,1;NASM叫它们st0,st1;


2.2.7 其它区别
由于历史原因,NASM用关键字TWORD,而MASM和其它编译器用TBYTE。
NASM同MASM一样不定义未初始化的内存;为了读一个64字节的保留内容。MASM程序员用stack db 64 dup(?),
NASM则用stack resb 64,为了有限的兼容,NASM对待问号做为一个有效字符,你可以用? equ 0然后写
dw ?将会有效。然而DUP是不被支持的。
除了以上说明这些,宏和定向符完全和MASM不同,详细信息请见第四章和第五章。



--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:14:39

--

第三章 NASM语言

3.1 NASM源码行的分布
象许多编译器一样,每个NASM的源码行(除非它是一个宏,一个预处理符或一个汇编定向符请见第四章和
第五章)都包含4个域
标号: 指令 操作符 ;注释
通常,这些域的一些为可选的:一个标号,一个指令和一个注释是可选的,当然,在指令域不存在时,操作符域
是必需的.
NASM在一行中是不限制空格的个数的:标号前可以有空格,指令前可以没有空格,标号后的冒号是可选的.(
注意,这意味着如果你愿意用一行来写lodsb,然后用lodab,那么这行只定义了一个标号而且是一个什么也
不做的有效代码行.运行用命令行-w+orphan-labels运行NASM,编译器将会提示你:一个没有冒号的标号行被
定义.)
标号里的有效字符为:字符,数字,_,$,#,@,~,.和?.定义标号的第一个字符必须为字母.,_和?,(详细信息
见第3.8节).一个标号如果带前缀$时,表示它将做为一个标号来处理而是一个保留字;因此,如果你用一个叫做
eax的符号来链接其它的模块,你应该在NASM的代码中用$eax来区别其它寄存器.
这个指令域可以包含什么机器指令:Pentium和P6的指令集,FPU指令,MMX指令和其它未公开的指令.这个指令集
通常也可以用前缀:LOCK,REP,REPE/REPZ或REPNE/REPNZ.编译器用前缀A16,A32,O16和O32来表示显式的地址
尺寸和操作符尺寸,其中第9章有关于这方面的一个例子.你也可以用段寄存器的名字做为一条指令的前缀:
coding 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-style语
法,这种语法中的寄存器名字前必须加a %sign),或有效地址(见第3.3节),常量(见3.4节)或表达式(
见3.5节)。
对于浮点数指令,NASM则有一个范围很广的语法:你可以象MASM一样用两个操作符形式,你也可以用NASM
本身的许多单操作符指令。所有支持的指令格式见附见A,例如你可以写:
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机器指令,但由于它们易于理解,所以经常在指令域中使用。当前的
伪操作符指令有:DB,DW,DD,DQ和DT,与其它指令一起用闹噶钣校篟ESB,RESW,RESD,RESQ和REST,
INCBIN命令,EQU命令,TIMES前缀。
3.2.1 DB和有关指令:定义初始化数据
DB,DW,DD,DQ和DT在MASM中用的很多,它们是用来定义输出文件的初始化数据了解的,它们使用的方式很
多:
db 0x55 ;只定义一个字节0x55
db 0x55,0x56,0x57 ;成功定义三个字节
db \&#39;a\&#39;,0x55 ;定义字符常量
db \&#39;hello\&#39;,13,10,\&#39;$\&#39; ;这是字串常量
dw 0x1234 ;0x34 0x12
dw \&#39;a\&#39; ;0x41 0x00(它只是一个数字)
dw \&#39;ab\&#39; ;0x41 0x42(字符常量)
dw \&#39;abc\&#39; ;0x41 0x42 0x43 0x00(字串)
dd 0x12345678 ;0x78 0x56 0x34 0x12
dd 1.234567e20 ;浮点数常量
dq 1.234567e20 ;双精度浮点数
dt 1.234567e20 ;扩展的双精度浮点数

DQ和DT不能接受数字常量和字符串常量做为操作数。
3.2.2RESB及相关符号:定义未初始化的数据
RESB,RESW,RESD,RESQ和REST被用在一个模块的BSS段:它们定义未初始化的存储空间。这些操作符中的
每一个都可以带一个操作数,这个操作数可以是一些字节,字,或双字来保存空间。这些在第2.2.7节中
描述。NASM不支持MASM/TASM用DW来定义未初始化的空间或类似的语法:RESB伪操作数是一个临界的表达式。
见第3.7节说明。
例如:
buffer: resb 64 ;保留64个字节
wordvar: resw 1 ;保留一个字
realarry resq 10 ;10个实数列表

3.2.3 INCBIN:包含外部的二进制文件
INCBIN是从旧的Amiga汇编器DevPac引用来的:它包含一个二进制文件verbatim到一个输出文件。这点对于
将一图象与声音数据直接加到一个关于游戏的程序是十分方便的。它可以有以下三种表示方式:
incbin "file.dat" ;包含整个文件
incbin "file.dat",1024 ;跳过前1024个字节
incbin "file.dat",1024,512 ;跳过前1024个字节,并且最多只包含512个字节

3.2.4 EQU:定义常量
EQU字义一个给定的符号为常数:当EQU被用时,源码行必须包含一个标号。EQU的用法是给一个数定义一个
标号名字。这个定义是固定的,不能在以后的代码改变它。例如:
message db \&#39;hello,world\&#39;
msglen equ $-message

定义msglen为常数12.msglen不能在以后重新定义。这也不是一个预处理定义:msglen是当时就被赋值的,
用$符号(见第三3.5节关于$的说明)定义时,意思是说它的值为后面字串的长度。要注意的是EQU也是一个
临界表达式。(说明见3.7)。


3.2.5 TIMES:重复指令和数据
TIMES前缀将使指定的指令多次进行编译。这个指令等价于在MASM及其兼容的编译器中的DUP指令的用法。
在你的代码中可以这样写:
buffer: db \&#39;hello,world\&#39;
times 64-$+buffer db \&#39; \&#39;
以上指令将会使用一个存储空间,这个空间会存储缓冲区上限到64时的总长度。最后,TIMES可以和普通指
令用, 你可以用以下指令来执行一个不用回退的动作:
times 100 movsb

要注意的是,指令times 100 resb 1和 resb 100之间是没有什么区别的,准确的说后者在编译器的内部运行
时将比前者快100倍左右。
TIMES指令与EQU,RESB及其它相关指令一样,是一个临界的表达式(见第3.7节说明)
注意,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] ;象汇编语句:[ebx*4+ebx]一样
mov eax,[label1*2-label2];等价于[label1+(label1-label2)]

有些有效地址格式有很多种形式,NASM在这种情况将会生成最小的一种格式,这将不同于32位的有效地址
[eax*2+0]和[eax+eax],NASM通常对后者将会用4 个字节来存储一个0偏移。
NASM有一个提示机制来使[eax+ebx]和[ebx+eax]来生成不同的操作数;由于[esi+ebp]和[ebp+esi]用不同的
默认值寄存器。
然而,你可以用关键字BYTE,WORD,DWORD和NOSPLIT来限制NASM在一个特殊的表格中生成一个有效地址。
如果你想用双字偏移来编译[eax+3]来代替NASM通常的一个字节, 你可以用[dword eax+3].同样,你可以
用[byte eax+offset]来限制NASM用一个字节偏移来编译一个第一遍没有编译的小值。(第3.7节有这样的
一个例子)[byte eax]将会用一个人偏移来编译[eax+0],而[dword eax]将用一个双字节的0偏移。
正常格式的[eax]是没有偏移的这个域的。
同样的,NASM由于允许偏移域和空间被存储,所以会将[eax*2]分成[eax+eax];事实上,它也会将[eax*2+
offset]分成[eax+eax+offset]。你也可以NOSPLIT关键字来阻止这种情况产生:[nosplit eax*2]将会
变成[eax*2+0]。


3.4 常量
NASM将常量分成4种类型:数字,字符,字串和浮点数。


3.4.1 数字常量
数字常量是简单的数字。NASM允许你指定不同进制来定义数字常量。你可以用后缀H,Q,B来分别表示十六
进制,八进制,二进制,或者你也可以用前缀0x来表示C形式的十六进制,或者你也可以用前缀$来表示
Pascal形式的十六进制。注意,前缀$有两种用法(见第3.1节),所以一个十六进制用前缀$来表示时,
必须用一个数字做为第一个字母。
例如:
mov ax,100 ;十进制
mov ax,0a2h ;十六进制
mov ax,$0a2 ;还是十六进制,0是需要的
mov ax,0xa2 ;还是十六进制
mov ax,777q ;十进制
mov ax,10010011b ;二进制

3.4.2 字符常量
一个字符常量为一个由单引号或以引号包含的最多面手个字符的形式。这种引用方式对NASM没有区别,除非
有后的常量用单引号引用允许在里面用双引号,反之亦然。
多于一个字符的字符常量将会以little-endian来排列,例如:
mov eax,\&#39;abcd\&#39;

那么常量将不是0x61626364,而是0x64636261,所以如果你想将一个值存到内存中,它将读成abcd而不是
dcba。这也是字符常量被Pentium\&#39;sR CPUID指令接受的原因。


3.4.3 字串常量
字串常量只能被命令为DB系列和INCBIN伪操作符处理,
一个字串常量看起来更象一个字符常量,只是长度不同。它的处理方式为根据实际情况将最大尺寸
的字符常量连接而成。所以下而的例子是等价的:
db \&#39;hello\&#39; ;字串常量
db \&#39;h\&#39;,\&#39;e\&#39;,\&#39;l\&#39;,\&#39;o\&#39; ;等价的字符常量

下面的也是相互等价的:
dd \&#39;ninechars\&#39; ;双字字串常量
dd \&#39;nine\&#39;,\&#39;char\&#39;,\&#39;s\&#39;;为三个双字常量
db \&#39;ninechars\&#39;,0,0,0;看起来更合理些

注意当用db做为操作数时,一个象‘ab’这样的常量做为字串常量时将会尽量变成一个短的字符常量,
因为db \&#39;ab\&#39;将简单和\&#39;db \&#39;a\&#39;等价。同样,三个字符和四个字符的常量用dw来处理时是一样的。


3.4.4 浮点数常量
浮点数只能用DD,DQ和DT伪操作来处理。他们将在一个传统的格式表达:数字,数值,然后是一系列可选项
的数字然后是一个以指数E结尾。所以NASM将会区分出dd 1 为定义一个整数常量而dd 1.0为定义一个浮点数
常量。
例子:
dd 1.2 ;一个简单例子
dd 1.e10 ;10,000,000,000
dd 1.e+10 ;与1.e10相同
dd 1.e-10 ;0.000 000 000 1
dd 3.141592653589793238462;pi

NASM不能对浮点数常量进行编译时算术运算。这是因为NASM被设计成便携的,虽然它经常生成可以在x86上
运行的代码,汇编程序将用ANSI C的编译器运行在任何系统上。然而,编译器将不能保证Intel的数字格式
上浮点数的兼容性,所以NASM只能处理它自己的浮点数运算,使汇编器由于一点好处而增加尺寸。

3.5 表达式
NASM里的表达式与C语言中的相似。
NASM不能保证编译时表达式的整数尺寸:因此NASM能够很轻松的在64位系统上编译并运行,
不能假设表达式等同与32位寄存器而故意使整型溢出。这样它或许不能工作。NASM能保证ANSI C做到的都
能做到:你到少在32位上工作。
NASM支持两种特殊的表达式, 允许计算当前汇编的位置:$和$$,$为到表达式行开始的位置且包含表达
式,所以你可以用JMP $来处理一个无限循环的操作。$$当前段的位置,所以你可以用($-$$)得到当前位置到
当前段有多远的。

按优等权的等级,将算术操作符列在下面

3.5.1 |:或操作
|操作给出了一个或逻辑运算,和OR机器指令一样。OR是NASM中算术运算级别最低的。


3.5.2 ^:异或运算
^提供一个异或运算操作。

3.5.3 & 与运算操作
&提供一个与运算操作。

3.5.4 <<和>>:位移动操作
<<给出了一个左移的操作,与C语言一样,所以5<<3等价与5 乘除或49。>>给出了一个位右移操作;在NASM中
移动操作经常是无符号的,所以从左边开始移位相当填入0而不是最高位的一个符号扩展。

3.5.5 +和-:加与减运算
+和-运算完全为最常见的加减运算。

3.5.6 *,/,//,%和%%:乘法与除法运算
* 是乘法运算符,/和//都是除法运算符:/是无符号除法运算,//是有符号除法运算。同样,%和%%提供了
无符号和有符号的取模运算。
NASM象ANSI C一样不能保证有答号取模的精度。
因此%在宏操作中用的更广泛一些,你应该尽量保证无论什么地方有符号与无符号取模后面都有空格。

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。
这样可能会变得复杂:由于16位段与组可能有叠加,你可能想用不同的段基址来引用一些符号。NASM允许你
这样做,用WRT(With Reference To)关键字。所你可以这样做:
mov ax,weird_seg ;weird_seg是一个段基址
mov ea,ax
mov bx,symbol wrt weird_seg

取ES:BX的方法不同,但实现的功能相同,指针都指向符号symbol。

NASM支持一个远调用(内部段)及一个段:偏移形式的跳转。这里段和偏移都是立即数。所以进行一个远
调用。你也可以用以下代码:
call (seg procedure):procedure
call weird_seg:(procedure wrt weird_seg)

(为了更详细说明括号里的为注释,可以不要).
NASM支持远调用的语法,和上面第一种用法相同。JMP和CALL工作方法相同。
在数据段里为了定义一个远指针,你必须这样写:
dw symbol,seg symbol

虽然你可以经常构造一些宏观世界处理,但NASM支持象上面的不方便的方式。

EvilOctal 2005-10-22 02:41

3.7 临界表达式
NASM的一个局限性在于它是一个两遍的编译器;不象TASM或其它的编译器,
它只做两遍编译。因而它不能应付哪些要二遍以上的复杂源码文件。
第一遍编译用来检查被编译数据和代码的尺寸,而第二遍编译用来生成物所有代码,已知的符号地址和代
码引用地址。所以NASM不能处理的一件事情是代码的尺寸由一个在定义在代码后的决定的情况。例如:
times (label-$) db 0
label: db \&#39;Where am I?\&#39;

在这个例子中,TIME后的参数根本什么都不等于。NASM将拒绝这种情况由于它不能确TIMES行的尺寸。下面的
代码也是不对的:
times (label-$+1) db 0
label: db \&#39;Now where am I?\&#39;

这里对于TIMES后参数的任何值的定义都是错的!NASM所拒绝的这些例子及相关表达式被称为临界表达式。也
就是说明一个表达式的值必须在第一遍编译时就确定,并且只依赖于它前面的符号定义。对于TIMES前缀是一
个临界表达式;同样对于RESB系列的参数伪操作数的参数也是临界表达式。
临界表达式可以使上下文保持好:考虑以下代码:
mov ax,symbol1
symbol1 equ symbol2
symbol2:
在第一遍编译时,NASM不能决定symbol1的值,由于定义symbol1的symbol2没有被NASM找到。第二遍编译时,
它遇到了mov ax,symbol1,它由于仍然不知道symbol1的值所以仍然不能生成代码。当它处理下一行时,它
找到EQU并且得到解决symbol1的值,但这已经太晚了。
NASM通过定义一个EQU的右边临界表达式来解决这个问题。这样在第一遍编译时symbol1将会被拒绝。
这有一些相关的例子:
mov eax,[ebx+offset]
offest equ 10

NASM在第一遍编译时,必须计算出指令:mov eax,[ebx+offset]的尺寸,虽然不知道offset 的值。它无法
知道offset是一个1字节的小值还是短整型格式有效地址的编码。它所能知道的就是在第一遍编译中,offset
应为一个代码 的一个符号,它可能用4个字节来填充。所以它为了适应4字节地址部分来记算指令的尺寸。
在第二遍编译中,为了维持这个判断,它要被迫使指令变得很大,所以这种方式产生的代码将不再是以前
哪么小。这种问题可以通过在offset前定义它或用[byte ebx+offset]来限制有效地址的尺寸。

3.8 本地标号
NASM对标号开始的部分进行了特殊处理,一个在单周期开始的标号为一个本地标号,也就是说它与以前的非
本地标号有关。所以例如:
label1 ;一些代码
.loop ;一些代码
jne .loop
ret
label2 ;一些代码
.loop ;更多的代码
jne .loop
ret

在上面的代码中,每个JNE都跳到它前面的行中,这是因为两个.loop的定义被前面的非本地标号分开了。
本地标号的处理方式是从旧Amiga汇编器DevPac中借鉴来的。然而NASM对其进一步发展了,允许访问其它代
码中的本地标号。这意味着在前面非本地标号中定义标号是可以的。上面的第一个.loop的定义是定义一个
符号label1.loop,第二个定义是定认一个符号label2.loop所以你可以这样写代码:
label3 ;一些代码
;更多代码
jmp label1.loop

这样做是可以的-在宏中,例如可以定义一个在任何地方被访问的标号但不能被正常本地标号机制所打扰。
这样的标号不能是非本地标号,因为它将被除数后来本地标号的定义,引用所打扰;它也不能是本地的,因
为定义它的宏不知道这个标号的全名字。NASM因此引用了第三种标号,它只在宏定义中有用;如果一个标号
用一个前缀:..@,那么它在本地标号机制中什么都不做。所以你可以写:
label1: ;一个非本地标号
.local: ;这是一个标号label1.local
..@foo:;这是第三种标号
label2: ;另一个非本地宏
.local: ;这是一个标号label2.local
jmp ..@foo ;这将向前跳三行。

NASM也兼容在双周期定义其它的指定符号例如:..start用来指定obj输出格式的入口点。(见第6.2.6节)。

--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:17:34

--

第四章 NASM的预处理

NASM包含一个功能强大的宏处理器,它支持条件汇编,多级文件包含,两种宏格式(单行与多行)以及对外部宏的
\&#39;context stack\&#39;机制.预处理操作用一个%符号做为开始.


4.1 单行宏

4.1.1 常用方法:%define
单行宏用%define预处理定向符来定义.这个定义方式与C语言有些相似;所以你可以这样:

%define ctrl 0x1F &
%define param(a,b) ((a)+(a)*(b))
mov byte [param(2,ebx)],ctrl \&#39;D\&#39;

这将扩展为:
mov byte [(2)+(2)*(ebx)], 0x1F& \&#39;D\&#39;

当一个单行宏的扩展中包含另一个宏时,另一个宏的扩展将在调用时展开,而不是在定义时.
所以下面的代码:
%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的意思为不敏感)可以
在宏定义时不区分大小写.所以%idefine foo bar 对foo,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.4.1节说明)。
你也可以用NASM的命令行参数‘-d\&#39;来重新定义一个单行宏。见第2.1.8节说明。
当定义一个单行宏时,你可以用%+伪操作符来连接字串,例如:
%define _myfunc _otherfunc
%define cextern(x) _ %+ x
cextern (myfunc)
在第一次调用展开时,第三行将变成"_myfunc",在后面的调用时它将会展开为"_otherfunc"


4.1.2 立即执行的单行宏:%xdefine
%define机制描述了前面提到的用后约束方式来定义单行宏,也就是说当你执行宏时将参考其它宏来进行
展开。所以你如果当时就改变这个宏时,宏展开的值也会变化。在它们不是相象的值时,这种方式会很有
用。如:
%assign ofs 0
%macro arg 1
%xdefine %1 dword [esp+ofs]
%assign ofs ofs+4
%endmacro
arg a
arg b

在上面的例子中,我们如果用%define来代替%xdefine那么宏a和b都会展开成dword [esp+8],这是错的。而我
们用%xdefine宏来在它定义时展开的话,那么a将会展开为dword [esp+ofs],而b将会展开为dword [esp+4]。
%xdefine宏是大小写敏感的,象%idefine与%define一样,%ixdefine可以使大小写不区分。


4.1.3 取消宏定义:%undef
单行宏可以用%undef命令取消,例如:
%define foo bar
%undef foo
mov eax,foo
将会扩展成mov eax, foo,这是由于用%undef的foo宏已经不存在了。
也可以用命令行参数\&#39;-u\&#39;来将预定义的宏取消。见第2.1.9节说明。


4.1.4 预处理变量:%assign
可以用%assign来将单行宏定义为一个变量(和%idefine与%define一样,%iassign不区分大小写)。
%assign被用来给无参数的宏定义一个数字,这个值可以在表达式中指定,在%assign被预处理时,这个值将
被计算。
象%define一样,宏%assign也是可以重定义的。
例如:
%assign i i+1
将增加宏的值。
%assign对于控制%rep预处理符循环的结束很有用。
见第4.4节的例子。另一个关于%assign的用法见第7.4节和第8.1节。
用%assign的表达式是一个临界的表达式(见第3.7节),并且表达式必须是一个纯数字(而不是一个代码或数
字的地址重定位引用,或一个寄存器的执行)。


4.2 多行宏:%macro
多行宏与MASM和TASM中的宏类型有些相似:一个NASM中的多行宏象下面这样:
%macro prologue 1
push ebp
mov ebp,esp
sub esp,%1
%endmacro

这里定义了一个C样式的宏prologue:所以你可以象下面这样用call来调用这个宏:
myfunc: prologue 12
它会扩展成三行:
myfunc: push ebp
mov ebp,esp
sub esp,12
宏%macro后面的数字1定义了prologue宏将要接受的参数的个数。宏里面的符号%1是对调用宏时第一个参数的引
用。当宏要引用多个参数时,它会用%2,%3来表示。
多行宏象单行宏一样是区分大小写的,除非你用%imacro来定义多行宏。
如果你要将逗号做为多行宏中参数的一部分时,你可以将整个参数放到一个括号中,如下所示:
%macro silly 2
%2: db %1
%endmacro
silly \&#39;a\&#39;,letter_a ;letter_a:db \&#39;a\&#39;
silly \&#39;ab\&#39;,string_ab ;string_ab:db \&#39;ab\&#39;
silly {13,10},crlf ;crlf :db 13,10


4.2.1 重载多行宏
与单行宏一样,多行宏也可以用相同名字不同参数的宏在后面进行重载。这也包含一个参数都一带的宏,所以
你可以定义:
%macro prologue 0
push ebp
mov ebp,esp
%endmacro
定义一个重载的宏将不会分配本地的堆栈空间。然而有时,你可能想用\&#39;overlod\&#39;重载一个机器指令;如:
%macro push 2
push %1
push %2
%macro
这样你的代码会变成:
push ebx ;这行不是宏调用
push eax,ecx ;这行是
通常,NASM对上同两行会出警告信息,因为push被定义一个宏,然后会用一个没有定义的参数来执行宏。这将
会生成正确的代码,但是编译器将给出一个警告。这个警告可以用命令行参数-w-macro参数来禁止。(见第
2.1.12节)。


4.2.2 本地宏标号
NASM允许你将一个多行宏定义为一个本地宏:所以多次调用相同的宏每次将会用不同的标号。你可以用前缀
%%来达到这个目的。你可以象下面的代码构造一个指令实现如果Z标志位为0则指行RET指令:
%macro retz 0
jnz %%skip
ret
%%skip:
%endmacro
你可以多次调用这个宏,在你调用这个宏时NASM将会生成一个\&#39;real\&#39;的名字来代替符号%%skip。
当用数字2345来调用每个宏时,NASM会构造一个..@2345.skip的表格。前缀符号..@是一种用来区分本地标号的
格式机制,这方面的内容见第3.8节。你应该避免用这种格式定义自己的宏(..@前缀然后是一个数字加另一个
周期)。这种情况的宏将与本地宏符号相冲突。


4.2.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
%endmacro
上面的代码将这样调用宏:第一逗号前的文本,[filehandle]被用来做为第一个宏的参数并且当%1被引用时扩展,
而所有后面的文本都将放到%2中来代替db所面的内容。
NASM中定义宏的这种扩充形式为在%macro所在行后面加参数个数再加+来实现。
如果你定义了一个扩充宏,你将会有效的告诉NASM它就将宏后面参数的个数从指定的数值扩充到无穷个;例
如,NASM将会根据后面参数个数为2,3,4或更多时来函来相应处理调用writefile。NASM将会在重载时不用这
种情况,即不允许你定义其它带4个参数的writefile。
当然,上面的宏观世界可以做为一个非扩充宏来使用,在这种情况下,它将象下面这样调用 :
writefile [filehandle],{"hello,world",13,10}
NASM支持将逗号入到宏参数中的机制,因此你应该选择一种你常用的宏定义机制。
写上面宏更好的解释见第5.2.1节。


4.2.4 默认的宏参数
NASM允许你用一范围的参数个数来定义一人多行宏。如果你这样做,你必须指定一个默认的参数。
例:
%macro die 0-1 "Painful program death has occurred."
writefile 2,%1
mov ax,0x4c01
int 0x21
%endmacro

这个宏在调用时会显示一条明显的错误信息(关于writefile的宏见第 4.2.3节):在退出前将把错误信息写到输出
设备上,它的调用没有参数,它是在宏定义中支持的一个默认的错误信息。通常你可以给这种类型的宏提供一
参数的最大个数与最小个数。最小参数在设用宏时用,你也可以指定一个默认的可选的参数。所以一个宏可以
用以下方式定义:
%macro foobar 1-3 eax,[ebx+2]
它可以用1到3个参数调,%1总是被调用,%2可以不指定而用默认值eax,如果不指定%3,则用默认值[ebx+2].你也
可以在宏定义中省略默认参数的定义,这种情况下,将默认值将是空格.一个能带可变参数的宏是比较有用的,因为
%0将被用(见第4.2.5节).来检测在宏调用是有多少个参数被用.
这种默认的机制可以与贪婪-参数机制一起使用;所以这种宏的模式将会变得用途很广,如下:
%macro die 0-1+"Painul program death has occurred.",13,10
通过用*符号,宏的最大参数个数将为无限的.当然,在这个例子里提供一个完整的默认参数集合是不可能的.关于
具体用法见第4.2.6节.


4.2.5 %0:宏参数记数器
对于一个可变参数的宏,参数%0的引用将返回一个包令调用宏时参数个数的数值型常量.这个用法与%rep相同(见
第4.4节)在第4.2.6节中给出一个相关的例子.


4.2.6 %rotate:对宏参数进行循环操作
Unix的shell程序员对shell中的移动命令应该比较落后了解,它允许一个shell角本中的参数(做为$1,$2引用)
向左移位,所以参数$2将代替$1,而最前面的$1将根本不再需要。
NASM提供了一个类似的机制,用%rotate格式。正如它的名字一样,它不象Unix中的shift命令会把最左边的参数
丢掉:而是移到最右边,反过来也一样。
%rotate 参数带一个数值参数来执行(可以是表达式)。这个宏参数指定的将右边的参数向左边移动多少个位
置。如果%rotate的参数为负,那宏的其余参数将向右边做循环移位操作。
所以下边一对宏可以存储和恢复一组寄存器的值:
%macro multipush 1-*
%rep %0
push %1
%rotate 1
%endrep
%endmacro
这个宏将用PUSH指令将参数从左到右依次调用。它首先用PUSH调用第一个参数%1,然后执行%rotate将所有参
数缶左移一位,即第二个参数变为%1。重复这个过程多次,(通过用%rep的参数%0完成)最后使每一个参数
都得到调用。
注意符*是一个最大宏参数个数的表示,也就是说在没有上限参数个数的情况下,你可以用这种多级压栈。
用这种宏对于做一个POP等价的宏而不需要指定参数是很方便的。最理想的是,你可以写一个多级压栈,将上
面的代码剪切和拷贝,然后将名字改为multipop,然后注意将出栈的寄存器以与它们相反的次序放在宏后面。
这个代码如下:
%macro multipop 1-*
%rep %0
%rotate -1
pop %1
%endrep
%endmacro
这个宏开始调用时将它所带的参数向右移动一位,而最后一个参数将出现在%1位置。然后开始出栈,然后再
开始右移,第二个倒数第二个参数将变成%1。这些参数将按与前面相反的次序进行迭代。


4.2.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来分隔第一个参数(给出的宏参数的个数)与第二个参数(将要连接的
文本)。这种连接可应用其它内连目标格式的预处理。如宏本地标识符(第4.2.2节)和相关本地标识(第4.6.
2节)。上面的情况中,用符号%和后面的文本放在括号里在语法上是有歧义的:%{%foo}bar将文本bar接到真正
的标识符:%%foo后面。(由于NASM用本地标识宏的真实名字的用法,意味着%{%foo}bar 和%%foobar都能扩
展成相同的形式,相反也是兼容)。


4.2.8 条件扩展宏参数
NASM通过给包含条件扩展代码来处理宏参数。开始,你可以用宏的参数%1来代替语法%+1,这使NASM处理
一个包含条件扩展的宏参数。并且如果宏被一个无效的条件扩展代码调用时,则会引起预处理器输出一个
错误提示信息。
对于通用的用法,你可以用一个%-1宏来使NASM来做一个相反的条件扩展。所以在第4.2.2节定义 的retz宏将
用下面一个更通用的条件来代替。
%macro retc 1
j%-1 %%skip
ret
%%skip
%endmacro
这样当用retc ne调用宏时会引起一个与JE一样的操作,或用retc po来调用宏时则会生成一个JPE调用。%+1参数
的引用用于象指令:CXZ,ECXZ等条件扩展;然而%-1则会对上面的情况输出一个错误信息,由于没有相反
的条件代码存在。


4.2.9 禁止列表扩展
当NASM从你的程序中产生一个列表文件时,它将生成一个扩展的多行宏来表示当调用宏时的内容并列出每个
表达式的内容。它允许你查看扩展宏生成时代码;然而有些宏将会使列表不清楚。NASM在这里提供了.nolist限
定符,使你可以包含一个宏定义使在列表文件中禁止扩展宏操作。.nolist限定符可以直接跟在参数个数后面。
象:
%macro foo 1.nolist

%macro bar 1-5+.nolist a,b,c,d,e,f,g,h


--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:20:59

--

4.3条件汇编
同C预处理器一样,NASM允许对源文件的段中的某一段进行汇编。通用的语法为:
%if<条件>
;如果条件为真时则出现的代码
%elif<条件2>
;如果条件不为真而条件2为真则处理
%else
;如果条件和条件2都假时则处理
%endif
如果%else子句象%elif子句一样为可选的,你可以在后面用很多的%elif子句。


4.3.1 %ifdef:测试单行宏的存在
在一个条件汇编块的开始用%ifdef MACRO来汇编子代码,如果把MACRO没有定义 ,则会执行%elif和%else之
间的代码块。
例如:
不调试一个程序时,你可以用下面代码:
;执行一些函数
%ifdef DEBUG
writefile 2,"Function performed succesfuly",13,10
%endif
;离开并做一些其它工作
如果你用命令行参数-dDEBUG则会生成一条调试信息,而移去这个参数时,将会生成这个程序的release版本。
你可以用宏%ifndef来代替%ifdef来测试一个宏的不存在。你也可以用宏%elifdef和%elifndef来测试%elif块中的宏
定义。


4.3.2 %ifctx:测试上下相关块
条件汇编结构%ifctx ctxname将引起预处理器的上下相关堆栈用ctxname命名时处理if汇编了块。
和%ifdef 一样,%elif的相反格式为%ifnctx,%elifctx和%elifnctx都是被支持的。
关于上下相关堆栈见第4.6节。第4.6.5节有一个关于%ifctx的例子


4.3.3 %if:测试数字表达式的任意性
条件汇编结构%if expr将会在数字表达式expr为非0时,引起执行if汇编子块。这个特点的一个例子为:决定什么
时候退出%rep预处理循环。(见第3.7节)
%if 扩展了常用的NASM表达式语法,从而提供了一种在正常表达式中无法处理的相关操作符。符号:=,<,
>,<=,>=和<>测试相等,小于,大于,小于等于,大于等于和不等于。C格式==和!=做为=和<>被支持。另外低级
操作符&&,^^和||用AND,XOR,OR来表示也是有效的。它们象C逻辑符一样(虽然C没有逻辑异或),返回0或1,对于
非0值返回1(如果对于^^,输入为0和1则返回1)。这类操作符返回1为真,返回0为假。


4.3.4 %ifidn和%ifidni:测试外部文本标记
结构%ifidn text1 , text2在if text1和text2成立时将引起一个if代码块的执行。在单行扩展后是相同的段文本。空格间
区别不计算在内。%ifidni与%ifidn相拟,但是大小写区分的。
如下面的宏观世界将一个寄存器或数字压栈,并且允许你将ip做为一个真正的寄存器。
%macro pushparam 1
%ifidni %1,ip
cal %%label
%%label:
%else
push %1
%endif
%endmacro
象%if结构一样,%ifidn和%elifidn配对,%ifnidn的作用和%elifnidn正好相反,同样,%ifidni有%elifidni,%ifnidni和
%elifnidni。


4.3.5 %ifid,%ifnum,%ifstr:测试标记类型
有些宏根据参数是否为数字,字串,标识符来执行不同中的功能。例如:一个字串输出宏要区分参数是一个
字串常量还是一个存在串的指针。
这种条件下可用结构%ifid来判断(可以有空格)后面的参数如果存在并且为标识符则运行if代码块。%ifnum也
类似,但是测试的是一个数字常数,而%ifstr则是测试一个字串。
例如:在第4.2.3节定义的写文件宏可以用%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宏可以用下面两种方法调用。
在第一种方法中,strpointer做为已经存在字串的地址被用,而length则为它的长度;在第二种调用中,一个字串
被传给一个宏,它将会自己计算出地址和长度。
注意在%ifstr中%if的用法:这里检测了宏是否带两个参数(由于字串是一个单独的字串常量,与db %2相同)或
更多的情况(在这种情况里,前两个数将被放到%3中,然后用db %2,%3被需要处理)。
%elifxxx,%ifnxx和%elifnxxx的用法适用于%ifid,%ifnum和%ifstr。


4.3.6 %error:报告用户定义的错误
预处理器定向符%error将引起NASM在代码中定义的错误发生时报告一个错误。所以其它用户想汇编你的源文件
时,你可以保证代们定义正确的宏。如下:
%ifdef SOME_MACRO
;一些设置
%elifdef SOM_OTHER_MACRO
;做一些其它设置
%else
%error Neither SOME_MACRO nor SOME_OTHER_MACRO was defined
%endif
这样当用户在不符合你代码要求的情况下汇编则会给出相应警告提示, 这比等待机器死机或出现未知错误要
好。


4.4预处理循环:%rep
NASM的TIMES前缀,虽然有用,但不能多次执行一个多行扩展的宏,由于它是在宏扩展后才被除数NASM处理
的。NASM提供了另一种循环格式,这是预处理级的:rep。
定向符%rep和 %endp(%rep可以带一个数字参数或表达式,而 %endp则无参数)用来包含一个chunk代码。
然后预处理器就可以反复的执行这个宏多次了。
%assing i 0
%rep 64
inc word [table+2*i]
%assign i i+1
%endrep
这将会生成一个64个INC指令,在[table]到[table+126]的内存中则为按字增长的数值。
对于一些复杂的终止条件可从重复循环中跳出哑,你可以用%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.5 包含其它头文件
又是一个与C预处理相拟的语法,NASM预处理器允许你将其它源文件包含到你自己的代码中, 这可以通过
定向符%include 来实现:
%include "macros.mac"
这将把文件macro.mac的内容包含到源文件中%include 定向符的位置上。

所包含的文件会被在当前目录查找 (你运行NASM的目录,或NASM的执行目录,及源文件目录),也可用NASM
的命令行参数-i 参数来指定。
标准C中的为避免一个文件被包含多次的操作在NASM中也实用:如果文件macro.mac有以下形式:
%ifndef MACROS_MAC
%define MACOROS_MAC
;现在定义一些宏
%endif
当包含一个文件多次时,将不会引起错误,这是因为第二次运行时将不会做任何事情。你也可以用NASM的命令
行参数-p(见第2.1.7节)来明确表示当一个文件没有用%include 定向符包含时包含文件。


4.6 上下相关堆栈
对于一个定义宏为本地的标号用处不是很强大:有你可能想要共用几个宏的调用。一个REPEAT...UNTIL循环的
例子REPEAT表达式需要对UNTIL宏定义的标号的引用。然而你可能想用宏来嵌套循环。
NASM提供了一个上下相关堆栈的操作。预处理器将维护一个上下相关堆栈,每一个元素都是用一个名字来标
记。你可以用%push定义符来增加一个相关堆栈,也可以用%pop来移去。你可以在相关堆栈中定义一个本地
的标号。


4.6.1 %push 和%pop:创建和移除相关块
%push定向符用来创建一个新的相关块并将它放到相关堆栈的上面,%push需要一个参数,来为相关块命名。如
%push foobar
这个指令在堆栈上创建了一个叫foobar的相关块。你可以将几个相关块用一个名字:它们仍然可以区分开。
定向符%pop不需要参数,移除最上的相关块并消除它及相关的标号。


4.6.2 上下相关标号
与%%foo定义一个本地特殊宏的标号一样,%$foo被用来定义一个本地相关堆栈上的一个相关块。所以下面用
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
这段代码将在string中4个字节为单元的进行扫描,直到找到与AL中相同的字节。
如果你想定义可访问堆栈中最上面的相关块的下一个相关块,可以用%$$foo或%$$$fooo来处理再下一个


4.6.3 相关本地单行宏
NASM允许你定义一个单行宏来对指定相关块操作:
%define %$localmac 3
定义一个单行宏%$localmac到堆栈的上面相关块,当然在后面的%push命令中,它仍然可用 名字%$$localmac
访问。


4.6.4 %repl:重命名一个相关块
如果你需要改变堆栈上面的一个相关块的名字(例如它用%ifctx区分),你可以在一个%push后用一个%pop;但
这将会消除所有关地这个相关块的本地标号。
NASM提供了一个定向符%repl来给一个相关块改名,而不用涉及到有关的宏和标号,所以你可以象下面这样:
%pop
%push newname
而用无消除的版本:%repl newname。


4.6.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 \&#39;if\&#39; before \&#39;else\&#39;"
%endif
%endmacro

%macro endif 0
%ifctx if
%$ifnot:
%pop
%elifctx else
%$ifend:
%pop
%else
%error "expected \&#39;if\&#39; or \&#39;else\&#39; before \&#39;endif\&#39;"
%endif
%endmacro
这个代码是这节给出的比较健壮的REPEAT和UNTIL宏,由于它用了条件汇编来检查宏运行的正确性(例如在
if 前没有调用endif)并且如果有问题用一个%error来处理。
另外,endif宏也能区分是直接在if后还是在else后的情况。这里用条件汇编将定是相关块是在堆栈上面还是不是
在上面的判断。
而else宏必须保存堆栈中的相关块,以使%$ifnot象if宏被endif宏定义一样而被引用 ,但这必须改变相关块的名字
使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块宏的处理比较好,这就是说将一个相关块压入堆栈用inner 描述,而它上面的一个则描述outer if;
因而else和else if 经常和最后一个未匹配的if或else组合。


4.7标准宏
NASM定义了一些标准宏,它们在源文件开始处理时就已经定义了。如果你需要程序不用预定义的宏汇编,可以
使用定向符%clear来清除预处理器的操作。
许多用户级的汇编定向符(见第5章)通常做为执行基本操作定向符的宏来使用;这在第5章中有说明。其它的
标准宏将在下面说明。


4.7.1 _NASM_MAJOR_和_NASM_MINOR_;NASM版本
单行宏_NASM_MAJOR_和_NASM_MINOR_将扩展成NASM的版本号。所以,在NASM0.96的宏中,_NASM_MAJOR_
将为0则_NASM_MINOR_将为96


4.7.2 _FILE_和_LINE:文件名和行数
象C语言的预处理一样,NASM允许用户发现文件名与包含当前行的行号。宏_FILE_将扩展成一个当前输入文件
的字串(可以用%include 定向符改变)而_LINE_则定义了在当前文件中包含当前行号的数值常量。这些宏对
调试一个宏信息时有用,在一个宏定义(单行宏或多行宏)中调用 一个_LINE_宏时,将会返回一个行号,而不
是定义。所以它可以检测一个代码块是否崩溃,如下面的例子将一个行号宏写入EAX中并输出类似\&#39;line 155:still
here\&#39;的信息。
%macro notdeadyet 0
push eax
mov eax,_LINE_
call stillhere
pop eax
%endmacro
然后你调用notdeadyet时就可以知道代码是在哪里死掉的了。


4.7.3 STRUC和ENDSTRUC:定义结构类型
NASM的内部并不允许定义数据结构,而预处理器则将它们做为一个宏的集合来执行。宏STRUC和ENDSTRUC用
来定义一个数据类型。
STRUC带一个参数,用来命名定义的数据类型。这个名字用一个带0值的字符定义,后面加上_size后缀再定义
一个EQU的值来给出此结构的大小。一旦STRUC定义成功,你就可以用RESB相关的伪指令来定义所需的域,
最后用ENDSTRUC来完成整个类型的定义。
上面的例子将定义一个叫mytype的宏,其中包含一个长字,一个字和一个字节及一个字串:
struc mytype
mt_long: resd 1
mt_word: resw 1
mt_byte: resb 1
mt_str: resb 32
endstruc
上面的代码定义了6个符号:mt_long从0开始(从类型mytype开始到长字域的偏移)。mt_word为4,mt_byte为 6
mt_str为 7,my_type_size为 39,而mytype本身则为0。将类型名字定义为0的原因是对本地标识机制的响应:如果
你的结构成员中有与另外的结构有相同的名字,你可以用以下代码改写上的的结构:
struc mytype
.long: resd 1
.word: resw 1
.byte: resb 1
.str: resb 32
endstruc
这样定义后结构的成员将变为 :mytype.long,mytype.word,mytype.byte和mytype.str。
NASM内部不对结构支持也不支持你定义的任何结构成员的格式与引用(本地标识符除外),所以指定mov
ax,[mystruc.mt_word]是无效的。mt_word将象其它常量一样处理,所以正确的代码指令应为:mov ax,[mystruc+
mt_word]或mov ax,[mystruc+mytype.word]。


4.7.4 ISTRUC,AT和IEND:定义一个结构的实例
当定义完一个结构后,下一步你将为你的结构定义一个实例。NASM提供了相对简单的ISTRUC机制。定义类型
mytype的一个例子如下:
mystruc: istruc mytype
at mt_long,dd 123456
at mt_word,dw 1024
at mt_byte,db \&#39;x\&#39;
at mt_str,db \&#39;hello,world\&#39;,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 \&#39;hello,world\&#39;
db 13,10,0


4.7.5 ALIGN和ALIGNB:数据对齐
ALIGN和ALIGNB宏可以将数据或代码按照字,长字,段或其它边界形式对齐。(有些汇编器用EVEN调用)。
ALIGN和ALIGNB宏的语法为:
align 4;4字节对齐
align 16;16字节对齐
align 8,db 0;定义8个0,相当于NOP
align 4,resb 1;在BSS中4字节对齐
alignb 4;与上一行相等
上面的宏的第一个参数必须为2的指数幂;它也会计算当前值到所要对齐的字节需要的字节数。然后用TIMES前
缀放在它的第二个参数来执行这个对齐操作。
如果第二个参数不指定时,则ALIGN的默认值为NOP,而ALIGNB的默认值为RESB 1。所以第二个参数如果指定
时两个宏将相同。正常情况下,你可以将ALIGN放到代码和数据段中,而ALIGNB放到BSS段中,除了特殊指定
否则不需要第二个参数。
ALIGN和ALIGNB是一个相对简单的宏,它们一不进行错误检查:它不检查它们的第一个参数是否为2的指数幂或
它们的第二个参数是否为大于一字节的代码。在这种情况下,它将会继续错误的做下去。
ALIGNB(或ALIGN带两个参数RESB 1)将会用下面的定义形式:
struc mytype2
my_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检查段是对齐。

--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:22:43

--

第五章 汇编定向符


NASM,虽然避免象MASM和TASM的官方编译器但也被迫支持一些宏。下面将描述这些内容。
NASM的定向符有两种类型:用户级的用户定向符和原始级的原始定向符。典型的说,每一个定向符都有一个用户级的和原始级的。在通常情部优下,我们推荐用户使用用户级的,而宏调用原始级的。
原始级的定向符将用方括号包含;用户级的则无所谓。
在这章描述的定向符中,每个目标文件格式都能可选的支持外部的定向符,为的是在文件格式中控制得的更
容易些。这些指定格式的格式定向符将在第六章描述。


5.1 BITS:指定目标处理模式
BITS定向符指定了NASM生成的代码是16位处理模式还是32位处理模式的。这个语法分别是BITS16或BIT32。
在最许多情况下,你不必明确的用BITS。被设计用在32位系统中的aout,coff,elf和win32目标格式,会使NASM来
选择32位的默认模式。obj目标格式允许你为每一个段指定USE16或USE32,NASM将根据它的操作模式为设置。
所以人用BITS定向符将不再需要。
用BITS定向符的主要原因是在一个平坦模式的二进制文件中写一个32位;这是由于二进制的输出格式默认为16
位模式来为写成DOS和COM的程序准备,DOS的.SYS设备驱动程序 和系统引导程序。
你在一个16位的DOS程序为了用32位指令而用BITS32为指定生成模式。如果你这样做编译器将会由于生成了
一个在16位机器上运行32位代码而生成错误的代码。
当NASM在BITS16下的状态时,用32位的数据用0x66加前缀,而用0x67前缀则表示一个32位的地址。在32位的
状态时,正好是相反:32位指令不需要前缀,16位数据需要0x66而16位地址则需要0x67。
BITS有一个等价的原始格式,[BITS 16]和[BITS 32]。用户级格式是一个不能调用原始格式的宏。


5.2 SECTION或SEGMENT:改变和定义段
SECTION定向符(SEGMENT是一个等价形式)来改变输出文件的段。在一些目标文件格式,一个数的数量和名
字是可以修改;可以按照用户的需要来构造宏。如果你想切换到一个来存在段,SECTION将会给出一条错误信息
或定义一个新段。
Unix目标格式或二进制目标格式,为代码,数据和非初始化数据段提供.text,.data和.bss段名字。相对来说,目标
格式不能识别指定的目标段名字并会去掉段名字的引导阶段。


5.2.1 _SECT_宏
SECTION定向符与它的原始格式的区别为不常用在用户级的格式。原始格式[SECTION xyz],可以简单的将目标段
切换到一个给定的段。用户格式SECTION xyz,首先定义了一个单行宏_SECT_来成为一个原始宏[SECTION]定向
能执行的方式 ,所以用户定向符:
SECTION .text
将扩展成
%define _SECT_[SECTION.text]
[SECTION.text]
用户可以发现用它们自己的宏来使用可能很有用。例如,在第4.2.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_切换回先前执行的段。在前一个版本的宏中避免跳过数据段
为了包含一个JMP指令,在某些方面个复杂的OBJ格式模块中用户可以在不同的代码段中进行汇编。


5.3 ABSOLUTE:定义绝对标号
ABSOLUTE定向符是SECTION的一个变化格式:它对一个不直接在物理段的子代码在给定的绝对地址开始。你只
能在RESB相关指令中使用。ABSOUTE用法如下:
absolute 0x1a
kbuf_chr resw 1
kbuf_free resw 1
kbuf resw 16
这个例子描述了一个PC BIOS数据区的段,在段地址为0x40:定义了kbuf_chr为0x1a,kbuf_free为0x1c和kbuf为0x1e。
ABSOLUTE的用户级格式象SECTION一样,在它被执行时被定义为_SECT_。
STRUC和ENDSTRUC可以用ABSOLUE(也可以用_SEC_)宏定义。ABSOLUTE不一定带一个绝对的参数:它可以
带一个表达式(一个临界表达式:见第3.7节)也可以是一个段中的值。例如:一个TSR象运行时的BSS一样重
用它的setup代码:
org 100h ;它是一个.COM程序
jmp setup ;跳到后的设置代码
;TSR中的驻留部分
setup: ;现在写一个安装TSR的代码
absolute setup
runtimevar1 resw 1
runtimevar2 resd 20
tsr_end:
在安装代码上定义了一些变量,所以在安装运行完后,它将为运行TSR存储空间来重新放数据。符号\&#39;tsr_end\&#39;
用来计算TSR的驻留部分的总尺寸。


5.4 EXTERN:从其它模块中引入符号
EXTERN与MASM的定向符EXTRN和C语言的关键字extern相似:它被用来引用一个不在模块中定义的符号,但
在其它模块中定义 了它并且被这个模块引用。不是每一种文件格式都支持引用的变量:bin格式就不支持。
EXTERN定向符可以带很多参数,每一个参数是一个符号的名字:
extern _printf
extern _sscanf,_fscanf
有一些目标文件格式提供了EXTERN定向符的外部特性。在所有情况下,外部特性用一个后缀的冒号跟在目标
文件指定文件的符号名。例如目标格式允许你来定义一个默认段的外部基位置 为一个定向符dgroup
extern _variable:wrt dgroup
EXTERN的原始格式与它的用户格式的区分为一次只能带一个参数:多个参数的支持在预处理器级上执行。
你可以用EXTERN多次定义相同的变量 :NASM将会忽略第二个及后面的重定义。你不能用EXTERN定义象
EXTERN的变量 。


5.5 GLOBAL:导出符号到其它模块
GLOBAL是EXTERN的结束标志:如果一个模块用EXTERN定义了一个符号并引用它,为了避免连接器的错误,其
它的模块必须定义这个标号并用GLOBAL来定义。一些编译器用PUBLIC来支持这个操作。
GLOBAL定向符必须在符的定义前出现。
GLOBAL的用法与EXTERN的用法相同,但它必须用在与GLOBAL定向符相同的模块中,例如:
global _main
_main: ;一些代码
GLOBAL,象EXTERN一样允许目标格式定义一个自己的扩展用一个冒号区分,elf目标格式例如 ,可以允许你
指定全局数据项是functions或data
global hashlookup:function,hashtable:data
象EXTERN一样,GLOBAL原始格式与用户级区别为它一次只用一个参数。


5.6 COMMON:定义常用数据区
COMMON定向符用来定义一个常用的变量,常用变量可以是一个非初始化数据段的全局变量,如下
common intvar 4
与下面功能相似
global intvar
section .bss
intvar resd 1
不同的是如果多个模块定义了相同的变量,那么在连接时这些变量将合并,在所有模块对这人变量的引用 会
在内丰中一个指定的位置。

象GLOBAL和EXTERN,COMMON支持目标格式指定的扩展,例如obj格式允许常用变量为NEAR或FAR而elf格式
则允许你指定一个常用变量的对齐格式:
common commvar 4:near ;在OBJ文件中
ommon intarray 100:4 ;在ELF中:4个字节对齐
象EXTERN和GLOBAL一样,COMMON的原始格式与用户级格式的区别为它一次只能带一个参数。



--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:27:43

--

第六章 输出格式


NASM是一个方便的编译器,用来 设计在任何ASNI的C支持的平台中编译,并生成物一个在不同Intelx86操作系统上
输出.对于这个原因,它可以有很多输出格式,用NASM的命令行参数-f可以选择所要的格式.对每种格式,都有它自已
的基于NASM的扩展语法,本章将进行详细说明.
在第2.1.1节中,NAM将在输入文件名的基础上选择一个默认的输出文件名和输出文件格式。这种方法是将输入
文件名的扩展名(.asm,.s或其它你用的名字)移去然后加上输出格式定义的扩展名。这个扩展将在下面给出。


6.1 bin:Flat-Form二进制格式输出
bin格式不能生成目标文件:它只产生你写的代码。这种\&#39;\&#39;纯二进制\&#39;\&#39;文件用在MS-DOS的.COM 和.SYS二进制文件
中。纯二进制文件主要用于对开发操作系统和引导程序的开发。bin支持三种标准的段名字:.text,.data和.bss。
NASM首先输出.text段的内容,然后是四字节对齐的.data段内容。如果你用SECTION定向符指定,你写的代码将
默认的放到.text段中。16位模式的bin格式(见第5.1节)为了在系统中用一个32位代码,你需要指定一个BIT32定向
符。bin没有默认的扩展格式:它只是去除源文件的扩展名,如NASM对binprog.asm汇编时将会生成二进制文件
binprog。


6.1.1 ORG:二进制格式的起点
bin格式提供了一个附加的定向符,它在第5章列出:ORG。ORG定向符的功能是在NASM将程序装入内存中时,
指定的一个起始位置。例如,下面的代码将生成长字0xc00000104:
org 0x100
dd label
label:
不象与MASM相关编译器提供的ORG定向符一样允许你在目标文件中跳转并重写你生成过的代码,NASM的ORG
定向符的只是确定初始值。它指定一个偏移值用来加在文件中所有的内部地址上;它不允许MASM版本所做的
任何欺骗行为。更多的信息见第10.1.3节。


6.1.2 bin扩展到SECTION定向符
bin输出格式允许你将SECTION(或SEGMENT)定向符来为一个需要对齐的段进行处理。这可以在段定义行后加
ALIGN来实现:
section .data align=16
这将会切换到section .data并指定它必须为16字节对齐的。ALIGN指定了段开始地址的最低多少位必须为0。
这个值必须为二个指数幂。


6.2 obj:微软的OMF目标文件格式
obj文件格式(NASM不叫的omf的原因是因为一些历史原因)是由MASM和TASM的一种输出格式,它主要用在16位
的DOS连接器生成.EXE文件。它也是OS/2的一种格式。obj提供一个默认的输出文件扩展名:.obj。
虽然obj不全是一个16位的格式,NASM还是支持32位扩展模式。通常32位obj格式在Borland的Win32编译器中使
用,来代替微软的新的win32目标格式。
obj格式不定义任何特殊的段名子:你可以定义你想要的名子。obj格式中典型的段名字CODE,DATA和BSS。
如果你在指定SEGMENT定向符前在源文件中包含代码,NASM将为你生成一个叫_NASMDEFSEG的段。当你在
obj文件中定义一个段时,NASM也会定义一个段名字,以使你可以访问段的地址。如下所示:
segment data
dvar: dw 1234
segment code
function: mov ax,data ;给出一个数据的段地址
mov ds,ax ;将它移到DS中
inc word [dvar] ;现在这个引用将操作
ret
obj格式也允许使用SEG和WRT操作符,所以你可以这样写代码:
extern foo
mov ax,seg foo ;给出foo的段
mov ds,ax
mov ax,data ;另一个段
mov es,ax
mov ax,[ds:foo] ;这将访问\&#39;\&#39;foo\&#39;\&#39;
mov [es:foo wrt data],bx ;这样引用


6.2.1 obj的SEGMENT定向符扩展
obj输出格式将定向符SEGMENT(或SECTION)进行扩展使你可以对段的属性进行更多的定义。这个操作可以通
过在段定义行的后面加扩展限定符来实现。如下:
segment code private align=16
定义一个段代码并定义它为一个原始格式的段并且必须为16字节对齐的。
下面为可用的定向符:
PRIVATE,PUBLIC,COMMON和STACK将指令段的综合特性。PRIVATE段将不能用连接器和其它段连接;
PUBLIC和STACK段将在连接时间进行连接;COMMON段将每一个段相互覆盖的连接而不是end-to-end连接。
ALIGN的用法象上面一样,指定段的开始位置最少多少位为0。给出的段值为2的指数幂可以从1-4096;实际上,
为1时只支持1,2,4,16,256到4096,如果为8时则可以是16,32,64和128到256等等。4096字节的偏移将是一个
PharLap扩展并且不被所有连接器支持。
CLASS将用来指定段的类别;这个特性将指示连接器将相同类型的段在输出文件中放一起。这个类型的名字可
以是任何词:如CLASS=CODE。
OVERLAY象CLASS一样带任何一个参数,并对overlay相兼容的连接器提供overlay信息。段可以用USE16和USE32
定义,它对在目标格式中对选择进行记录,并且当段分别是16位和32位时确保NASM能用默认的汇编。
当写OS/2目标文件时,你可以用FLAT来定义一个32位的段,用来为一个指定的FLAT的组定义一个段基址,如果组滑有定义时也定义组。obj文件格式允许段来定义一个预定义绝对段地址,虽然没有连接器用这个特性;但
NASM允许你用SEGMENT SCREEN ABSOLUTE=0XB800来定义一个段。ABSOLUTE和ALIGN关键字将是相互排斥的。NASM的默认段属性为PUBLIC,ALIGN=1,无类型,无重叠和USE16。


6.2.2 GROUP:定义段的组
obj格式也允许段是可组的,所以一个段寄存器可以对组中的所有段进行引用 。NASM则提供GROUP定向符:
segment data
;一些数据
segment bss
;一些未初始化数据
group dgroup data bss
将定义一个名字为dgroup的组包含data和bss段。象SEGMENT一样,GROUP可以将组的名字定义为一个符号,所
以你可以用var wrt data或var wrt dgroup来在一个数据段中引用一个变量,这取决于呶一个段值为在你当前的段
寄存器。如果你引用一个var,而var是在一个段组中的一个段中被定义的,那么NASM将默认从段组的开始定义
var的偏移,而不是段的。SEG变量,将返回组基址而不是段基址。
NASM允许一个段成为多个组的一部分,但你这样做将会生成一个警告信息。在多个段组中的段里定义的变量
将默认与包含这个段的第一个段组相关联。一个组不一定包含任何段:你可以用WRT引用一个在组中不包含你
引用的变量。对于OS/2,则定义了一个特殊组FLAT,没有段在它里面。


6.2.3 UPPERCASE:在输出中禁止大小写
虽然NASM本身是大小敏感的,有一些OMF连接器不是;然而它对于NASM输出单向敏感是有用的。UPPERCAT的
格式定义符将对在目标格式文件中的所有段,组和符号名在它们写入前来变成大写。在一个源码文件中,NASM
仍然是大小写区别的;如果愿意要可以将整个文件变成大写。UPPERCAT单独用一行表示,并不带参数。


6.2.4 IMPORT:引入DLL符号
如果你用NASM写一个DLL的引入库,IMPORT格式定义符定义一个从DLL引用的符号。你可以象EXTERN一样,使
用IMPORT定向符。IMPORT定向符带两个参数,用空格来分隔,它们分别为你想引用的符号名和你相引入的库
名字:
import WSAStartup wsock32.dll
第三个可选的参数将给出一个在引入库中的名字,一旦你在代码中引用它时,则和这个符号相同。如:
import asyncsel wsock32.dll WSAAsyncSelect


6.2.5 EXPORT:引出DLL符号
如果你用NASM写了一个DLL,EXPORT格式描述符定义一个可以外部引用DLL的全局符号。你可以象定义GLOBAL
一样定义EXPORT定向符。EXPORT带一个参数用来给你引出的名字命名,它将在你的源文件中定义。第二个可
选参数(用空格与第一个参数分隔)给出了这个符号的外部名:你在程序中用DLL时想要程序知道的符号名。
如果这个名字和内部名相同,你可以不用第二个参数。
更多的参数将用来定义外部符的属性。这些参数象第二个参数,用空格分隔,如果更多的参数给出,外部名也
必须指定,它甚至可以和内部名一样。下面为可用属性:
resident表示外部符号是可以在系统启动时保存的。这对通过名字引入的符号通常有用。
nodata表示引出符号是一个不用初始化数据的函数。
parm=NNN,这里NNN是一个整数,设置参数的个数,这些参数是在32位和16位段间门调用的符号。
数值的属性表示一个符应该用一个数字(序号)导出,并给出一个预计的数字。例如:
export myfunc
export myfunc TheRealMoreFormalLookingFunctionName
export myfunc myfunc 1234 ;通过序号导出
export myfunc myfunc resident parm=23 nodata


6.2.6 ..start:定义程序的入口点
OMF连接器需要一个目标文件被连接到程序中的入口点,当程序运行时将会从这里执行。如果用NASM定义目标
文件的入口点,你可以用符号..start来指定。

EvilOctal 2005-10-22 02:42

6.2.7 obj对EXTERN定向符的扩展
如果你定义了一个外部符号:extern foo
然后 用mov ax,foo将会给你一个从段基址(定义foo所在的段)到foo的偏移。拟你可以象下面这样访问fo
mov ax,seg foo;给出段基址的引用
mov es,ax ;将它移至ES中
mov ax,[es:foo] ;给出\&#39;\&#39;foo\&#39;\&#39;的偏移
这是一个不很广泛的用法:如果你想对一个已知的段或组中的外部符号进行访问可以用dgroup。如果DS已经包
含了dgroup,你应该写成:mov ax,[foo wrt dgroup]
然而,这时你想访问fo将很难,NASM允许你在一个变格式的中定义foextern fowrt dgroup
这种格式使NASM假装在引用foo的段基址是在一个实际的dgroup中;所以seg foo将返回dgroup并写表达式
foo将与foo wrtr dgroup等价。默认的WRT机制鹏程万里你程序中任何相关的组或段中出员的外部符号。它也可以
用在common变量中:见第6.2.8节





--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:28:02

--

6.2.8 obj对COMMON定向符的扩展
obj格式允许common变量为一个near或far;NASM允许你指定一个变量的语法用法:
common nearvar 2:near ;\&#39;\&#39;nearvar\&#39;\&#39;是一个near common
common farar 10:far ;\&#39;\&#39;farvar\&#39;\&#39;是一个far
Far common变量可以大于64Kb的尺寸,OMF规定它们用一个给定的元素个数的尺寸定义。所以一个10字节的far common变量
将用10个一字节的元素定义,5个二字节,和2个五字节可一个10字节。一些OMF连接器需要这个尺寸,和变量
尺寸一样,为了在多个模块中解决common变量的定义和匹配。然而NASM认你必须指定你far common变量中
元素的指尺寸 。如下的语法:
common c_5by2 10:faq 5;二个五字节
common c_2by5 10:far 2;五个二字节
如果元素尺寸未指定则默认为1。当元素的尺寸指定时FAR关键字可是以不用,这是因为只有far common变量才
需要元素尺寸,如下定义:
common c_5by2 10:5;二个五字节
common c_2by5 10:2;五个二字节
附加的扩展为,obj中的COMMON定向符象EXTERN(见第6.2.7节)一样也支持默认的WRT操作。所以你可以写:
common foo 10:wrt dgroup
common bar 16:far 2:wrt data
common baz 24:wrt data:6


6.3 win32:微软的Win32目标文件格式
win332输出格式生成微软的Win32目标文件,主要为用象Visual C++等微软的连接器。而Borland的Win32编译器不
用这种格式,而是用obj格式代替(见第6.2节).
win32指提供的默认输出扩展名为.obj.注意虽然微软说Win32目标文件是COFF(通用目标文件格式)标准的,但微软
生成的目标文件夹与象DJGPP COFF连接器生成的不兼容性,相反也一样。这是由于PC相关重定位的语法上的
区别。为了产生DJGPP的COFF文件可以使用NASM的coff输出格式;相反,coff格式也不能生成Win32连接器产生的
目标文件格式。


6.3.1 win32对SECTION定向符的扩展
象obj格式一样,win32允许你在SECTION定向符行上指定附加的信息,如你定义段的类型与属性。段的类型和
属性在NASM自动生成时为.text,.data和.bss,但这可以通过下面的限定符修改:
code或等价text,定义了一个代码段。 这使段可以是只读和可执行的,但不可写,也表示连接器用一
个代码段来处理它。
data和bss定义了一个数据段,类似代码段。数据段是可读写的,但不能执行。data定义了一个初始化
了的数据段,而bss定义了一个非初始化的数据段。
info定义了一个信息段,不能通过连接器包含,但可以将信息传给连接器。如定义一个叫.drectve的信息
段使连接器将段的内容做为一个命令行操作:
align=,象obj一样处理段数据的对齐。你可以指定的最大值为64:Win32目标文件格式不能包含大于这个
段偏移值。如果参数不指定则默认为code段为16个字节,data段(BSS)为4字节,信息段为1字节(不对齐):
section .text code align=16
section .data data align=4
section .bss bss align=4
其它段的名字象.text一个默认对待。


6.4 coff:通用目标文件格式
coff输出用DJGPP连接器生成的COFF目标文件。coff提供了一个默认的扩展名:.o.
cof格式支持SECTION定向符的扩展同win32一样,除了不支持align限定符和info段类型。


6.5 elf:Linux ELF目标文件
elf输出格式生成ELF32(执行和连接格式)目标文件,在Linux下使用。elf提供了一个默认的扩展名:.o.


6.5.1 elf对SECTION的扩展
象obj格式一样,elf允许你在SECTION定向符后指定附加信息来对你定义的段的类型与属性。段类型与属性在
NASM自动生成时为.text,.data和.bss并能用下面的限定符修改:
alloc定义一个段在程序运行时就装入内存。noalloc定义则相反常用于一个信息段或注释段。
exec定义当程序运行时是可执行的。noexec定义不可以。
write定义 程序运行时是可以写的。onwrite定义不可写。
progbits定义一个段将显式的在目标文件中存储内容:一个原始代码可数据段,如nobits定义一个段
将不用显式的内容如BSS段。
align=象obj一样对齐数据。
NASM默认的设置为:
section .text progbits alloc exec nowrite align=16
section .data progbits alloc noexec write align=4
section .bss nobits alloc noexec write align=4
section other progbits alloc noexec nowrite align=1
(任何不为.text,.data和.bss的段都将象上面代码一样对待)


6.5.2 位置独立代码:elf指定符和WRT
ELF的指定格式包含足够的特性使位置独立代码生成(PIC),这使ELF可以弹性的共享其它 的库。这也就是说NASM
必须能在ELF目标文件中生成大量奇怪的重定位信息,如果这个源代码是用PIC写的。由于ELF不支持段基址的
引用,所以WRT操作符不能用它的正常用法;然而NASM的elf输出格式可以用不同格式的WRT用法,用PIC分隔
的重定位类型。elf定义了5种指定的符号包含PIC的重定位类型,你可以将它们放在WRT操作符的右边。它们是
..gotpc,..gotoff,..got,..plt和..sym。它们的功能 如下:
引用符号时用wrt标记全局偏移表的基址时,..gotpc将结束从当前段位置到全局偏移表的距离。
(_GOBAL_OFFSET_TABLE 是一个标准符号名用来引用GOT)。所以你需要将$$加到结果上来得到真实的GOT地址。
在你自己的段中引用一个位置时用wrt ..gotoff将会全出从GOT开始的位置到指定位置的距离,所以将地址 加到
GOT上将会给出一个你想要的真实地址 。
用wrt ..got对一个外部或全局符号的引用将会使连接器在构造GOT时包含符号的地址和给出GOT开始到入口的
距离 。所以你可以在GOT上加一个地址,从结果地址中取出再接到符号的地址上。
用wrt ..plt引用一个过程名将使连接器为符号构造一个可连接的过程表入口,并给出PLT入口的地址。你可以只
在上下相关环境中生成物一个PC相关的重定位信息(如一个CALL或JMP的目标),ELF对PLT的入口绝对引用 包含
不能重定位的类型。
用wrt ..sym对一个符号名引用时会使NASM写一个原始的重定位,而不是一个段开始的相关重定位并加上符号
偏移,它将写一个重定位记录来直接处理符号方面的问题。这个做法对于一个常见的动态连接是有必要的。
如何使用重定位类型的一个完全解释是在NASM第8.2节中的一个定共享库的例子。


6.5.3 elf对GLOBAL定向符的扩展
ELF目标文件可以包含关于一个全局符号更多信息:它们可以包含它们的尺寸和类型。这将很少进行调试,但
程序用一个共享库写时是需要的。NASM也支持对GLOBAL定向符的一些扩展,允许你指定这些特性,你可以指
定一个全局变量是一个函数或数据目标通过用冒号后缀和function或data(object与data相同),例如:
global hashlookup:function, hashtable:data
导出全局符号hashlookup是一个函数而hashtable为一个数据。你也可以用一个数学表达式指定与符号相关数据
的尺寸,如:
global hashtable:data (hashtable.end-hashtable)
hashtable:
db this,that,theother ;一些数据
.end:
这使NASM自动计算表的长度并将信息放到ELF表格中。当写一个共享库代码时定义全局符的类型和长度是有用
的,更多信息见第8.2.4节。


6.5.4 elf对COMMON定向符的扩展
ELF也允许你在common变量上指定对齐方式。这通过在变量 的名字和大不上气不接下气放一个数字(2的指数幂)
实现,中间用冒号来实现,如下面将使一个双字的数组4字节对齐。
common dwordarray 128:4
数组的整个尺寸 为128个字节,而它是4字节对齐的。
6.6 aout:Linux a.out目标格式
aout格式生成a.out目标文件,用在早期的Linux系统上.(与其它a.out目标文件在magic数字前四个字节上有区别,
有些a.out执行文件,如NetBSD的,支持位置独立代码,而Linux的则不能执行)a.out提供的默认扩展名为.o.
a.out是一种非常简单的目标格式,它不支持指定的定向符,指定的符号和SEG或WRT的用法及对任何标准定向
符的扩展。它只支持三种标准的段名字:.text,.data和.bss。


6.7 aoutb:NetBD/FreeBSD/OpenBSD a.out目标文件
aoutb格式生成a.out目标文件,这种格式用在各种自由的BSD Unix版本:NetBSD,FreeBSD和OpenBSD。对简单的
目标文件,这种目标格式和aout一样除了文件前面的4个字节magic数字不同。然而,aoutb格式象elf格式一样
支持位置独立的代码,所以你可以用它来写BSD的共享库。aoutb提供了一个默认的扩展名为.o.aoutb不支持指定
的定向符,符号,只支持三个标准的段名:.text,.data和.bss。然而它也和elf一样支持WRT的用法,提供位置独立
的代码重定位类型。更多这种属性的文件见第6.5.2节
aoutb象elf一样支持GLOBAL定向符:见第6.5.3节这方面的文档。


6.8 as86:Linux的as86目标文件
Linux的16位编译器as86有它自己的非标准的目标文件格式。虽然它的相关连接器ld86生成相似的a.out二进制输
出,但在as86和ld86间目标文件的用法的交流不是a.out本身。NASM支持这种格式,是因为它在这方面有用。
as86提供默认的扩展名为.o。as86是一种非常简单的目标格式(从NASM用户观点)。它除了支持三种标准的段名:
.text,.data和.bss外不支持其它的定向符,符号和SEG或WRT的用法。


6.9 rdf:重定位动态目标文件格式
rdf输出格式生成RDOFF目标文件。RDOFF(重定位动态目标文件格式)是一种home-grown目标格式,为了NASM自身
设计并影响编译器内部结构的文件格式。RDOFF不用在出名的操作系统上。哪些写它们自己操作系统的人用这
种RDOFF做为它们的目标格式化,它主要设计用来单独包含那些少量的官方文件头。Unix NASM档案和DOS档案
包含源代码和一个rdoff子目录用来设置RDOFF:一个RDF连接器,一个RDF静态库,一个RDF文件查看器及在
Linux下的装入和执行RDF的程序。rdf只支持标准的段名:.text,.data和.bss。


6.9.1 所需库:LIBRARY定向符
RDOFF包含一个机制为目标文件决定将一个给出库连接到模块上,在装入时或运行时。通常LIBRARY定向符带
一个参数做为模块的名字:library mylib.rdl


6.10 dbg:调试格式
dbg输出格式不在NASM的默认匹配中构造。如果你要从源码文件中构造自己的NASM执行文件,你可以在
outform.h或命令行定义OF_DBG来包含dbg输出格式。dbg格式并不输出目标文件;而是输出一个文本文件,它包
含了一个在NASM主程序和输出听到端模块转换的完整列表。它主要是为了帮助那些想写自己输出驱动程序的人
,在主程序根据不同需要生成驱动程序时能有一个清楚的思路。dbg格式的用法如下:
nasm -f dbg filename.asm
这将生成一个错误诊断文件:filename.dbg。但这对哪些不同目标格式的文件将不会工作太好,因为每一种格式都
定义了自己的宏(通常用用户级的定向符),而这些宏在dbg格式中可能没有定义。这里可以运行两次NASM,以用
一些私有的目标格式进行预处理:
nasm -e -f rdf -o rdfprog.i rdfprog.asm
nasm -a -f dbg rdfprog.i
这将预处理rdfprog.asm到rdfprogi里,保存选中的rdf目标格式为了确保RDF指定定向符可以正确的转成原始模
式。这种预处理的源码将用dbg格式生成最终的错误输出。
这种情况对于obj格式不适用,因为obj的SEGMENT和GROUP定向符号地于用符号定义段和组是有影响的;dbg则
不会这样做,所以程序将不编译。如果你想从目标源文件中得到一条dbg信息,则你必须定义你自己的符号
(如用EXTERN)。dbg可以接受任何段的名字,定向符,并将它们记录到它的输文件中?


--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:31:49

--

第七章 写16位代码(DOS,Windows3/3.1)

这章主要介绍写MS-DOS或Windows3.x下运行的16位代码是遇到的方法.主要说明如何将程序连接成.EXE或.COM
文件,如何写.SYS设备驱动程序,如何提供汇编语言和16位C编译器和Borland Pascal之间的接口.


7.1 生成.EXE文件
任何在DOS下运行的大型程序都在做为一个.EXE文件来构造:只有.EXE文件需要跨过64K段的内部结构.Windows
程序由于不支持.COM格式也必须构造成.EXE文件.通常你生成.EXE文件时用obj输出格式生成一个或多个obj文件.
然后用连接器将它们连到一起.然而,NASM也可以生成用于bin格式的简单的DOS.EXE文件(用DB和DW来构造.EXE
文件头).和一个支持这个的宏压缩包.感谢Yann Guidon对这个代码的描述.
NASM也支持本了的.EXE文件,做为将来版本的另一种输出格式.


7.1.1 用obj格式生成的.EXE文件
这一段描述了通过.OBJ文件连接成.EXE文件的方法.16位程序语言是用一个适合的的连接器压缩的;如果把你没有
话,这有一个叫VAL的自由连接器,来自x2ftp.oulu.,fi的LZH文档格式.一个LZH文档可以在ftp.simtel.net找到.这还有
一个叫FREELINK的\&#39;free\&#39;连接器(虽然它不带源码),可以从[url]www.pcorner.com[/url]上找到,另外还有DJ Delori写的djlink可
以在[url]www.delorie.com[/url]上找到.当将几个.OBJ文件连接进一个.EXE文件中时,你应该保证这些obj文件中的一个为程序
定义了开始.(用..start指定符定义,见第6.2.6节)如果没有模块定义起始点时,则连接器将不知道输出文件头中入口
给定的值;如果定义过多的起始点,连接器也不知道会用哪一个.这里给出呈个用NASM源码编译成.OBJ的文件和连
接器连接成.EXE文件的例子.这解释了定义一个堆栈,初始化段寄存器,定义开始点的基本规则,这个文件也包含在
NASM文档中的test子目录中,名字为objexe.asm:
segment code
..start: mov ax,data
mov ds,ax
mov ss,ax
mov sp,stacktop
初始化部分将DS设为数据段,并将SS和SP初始为提供堆栈的顶部.注意将一个指定移入SS时,将禁止中断,所以在
SS和SP间取数时和没有堆栈执行时将不会生成堆栈.指定符..start定义了这个代码的开始点,这也是说这个点将为
结果执行文件的输入点.
mov dx,hello
mov ah,09
int 0x21
上面的主程序:将一个欢迎信息指针将入DS:DX(hello是一个隐式相关的段数据,将在设置代码中送入DS中,所以指
针是有效的)并调用DOS的显示字串命令.
mov ax,0x4c00
int 0x21
这将用另一个DOS系统调用终止程序.
segment data
hell db \&#39;hello,world\&#39;,13,10,\&#39;$\&#39;
数据段包含了我们想要显示的字串.
segment stack stack
resb 64
stacktop:
上面的代码定义了一个包含64个字节非初始化的堆栈段,然后将指针移到它的顶端.堆栈段定向符定义了一个名
字为stack的堆栈,类型为STACK.对于正确运行程序后者不需要,但如果你的程序没有STACK类型的段则连接器将发
出一个警告信息.将上面的文件编译成.obj文件并连接成一个有效的.exe文件,运行这个文件时会显示\&#39;hello,world\&#39;
信息并退出.


7.1.2用bin格式生成.exe文件
.exe文件格式的简单在于:它可以通过写一个纯的二进制程序并粘在一个32字节头后面来构造一个.exe文件.这个
头可以简单的用NASM的DB,DW命令来生成,所以你可以用一个bin的输出格式来直接生成.exe文件.
在NASM的文档中的misc目录下有一个叫exebin.mac的宏文件.它定义了三个宏:EXE_begin,EXE_stack和EXE_end.
为了用上面的方法生成一个.EXE文件,你可以在你的源码文件中的开始处用%include包含一文件exebin.mac.然后
你就可以用宏EXE_begin(无参数)来生成文件头数据.为通用的bin格式写代码-你可以用三个标准段:.text,.data和
.bss.在文件尾你应该用EXE_end宏(也不带参数)来定义一些标记段尺寸的符号,这些符在用EXE_begin生成的头
代码中被引用.在这种模式下,你可以结束0x100处开始写代码,就象一个.com文件,事实上,如果你除去.exe文件32
字节的头,你将会得到一个有效的.com程序.所有段基址都是相同的,所以你可以限制到一个64k的程序就象一个
.com文件一样.注意一个ORG定向符被EXE_begin宏调用,所以你不应该用显式的调用.
你不能直接引用你的段基址,这将在头中需要一个重定位,这样事情将变得复杂.所以你将CS拷贝出来的方法得到
段基址.对于你的.EXE文件一个入口,SS:SP将会设置一个2K堆栈的顶部.你可以通过调用一个EXE_stack宏来调整
2KB的默认尺寸.例如,要将你程序的堆栈尺寸改变到64字节,则可以用EXE_stack 64.一个生成.EXE文件的例子将
NASM文档的子目录test中做为binexe.asm给出.


7.2 生成.COM文件
写一个大型的DOS程序必须写成.EXE格式的,小型的则可以写成.COM格式的..COM文件是纯二进制的,并很容易用
bin格式生成它.


7.2.1 用bin格式生成.COM文件
.COM文件通常在偏100h位置将入(虽然段可以改变).例如在程序右边的100h开始执行.所以写一个.COM程序,你应该
用下面的源码:
org 100h
section .text
start: ;将代码放到这
section .data
;将数据项放到这
section .bss
;将非初始化数据放到这
bin格式将.text段放在文件中最前面,所以你可以在要写的代码前定义数据或BSS项并代码将在它属于的文件前结
事.BSS(未初始化数据)段不在一个.COM文件中占用空间:相反BSS项的地址将用一个超过文件未尾的指针来解决.
然而你不能在你运行时相信BSS段被初始化为全0.如果编译上面的程序,你应该用这样的命令行参数:
nasm myprog.asm -fbin -o myprog.com
bin格式在没有显式输出文件名指定的情况下将生成一个叫myprog的文件,所你必须重载它并给出一个指定文件名


7.2.2 用obj格式生成.COM文件
如果你用.COM程序写了很多模块,你可以希望将几个.OBJ文件编译并连接成一个.COM程序,你可以这样做:直接提供你一个输出.COM的兼容连接器(TLINK这样做),或用一个象EXE2BIN的转换程序来使连接器将.EXE文件转成.COM
文件.如果你这样做,你需要注意以下事情:
第一个包含开始执行的代码段应该用一行RESB 100h的语句.这可以保证代码会在偏移100h开始,所以连接或转换器在生成.COM文件时将不必调整文件中的引用地址 .其它的编译器用一个ORG定向符来达到这个目的.
但在NASM中的ORG是一个指定bin输出格式的定向符,这不意味着它不能做与MSAM兼容编译所做的事.
你不必定义一个堆栈段.所有你定义的段都应该在一个相同的段中,所以你的代码或数据每次引用一个符号偏移时
,所有偏移都是对相同的段基址的偏移.这是因为一个.COM文件被取出时,所有段寄存器都包含相同的值.


7.3 生成.SYS文件
MS-DOS设备驱动程序-.SYS文件-是纯二进制文件,与.COM文件相似,除了它们是在初始0开始而不是100h.然而,如果
你用bin格式写了一个设备驱动程序,你不需要ORG定向符,因为对bin默认开始点为0.相似的,如果你用一个obj,你
不必在代码段开始处用RESB 100h..SYS文件用一个头结构开始,包含工作驱动程序里指向不同的例程.这个结构在
代码段的开始处定义,虽然这不是真正的代码.关于.SYS文件更多的信息和头结构中的数据,一本常见问题集将在
新闻组 comp.os.msdos.programmer上找到.


7.4 对于16位C程序的接口
这段说明了写关于调用或被调用 C程序的汇编程序的基本规则.为了做到这点,你应该写一个象.OBJ文件的典型汇
编模块,然后用它连接到你的C模块来生成一个混和语言程序。


7.4.1 外部符号名字
C编译器的常规为:所有定义的全部符号名字(函数或数据)用一个下划线加上名字来在C程序中出现。所以,一个
C程序员想象在汇编语言程序的printf函数将为_printf。这意味在你的汇编程序中,你可以定义一个不带下划线的
外部符号并不必和C符号冲突。如果你觉得下划线不方便,你可以定义宏来代替GLOBAL和EXTERN定向符:
%macro cglobal 1
global _%1
%define %1 _%1
%endmacro
%macro cextern 1
extern _%1
%define %1 _%1
%endmacro(这些宏格式一次只带一个参数;一个%rep构造可以解决这个。)
如果你定义一个外部符号如:
cextern printf
然后宏将扩展为:extern _printf
%define printf _printf
上面你可以象一个符号引用printf,预处理器将前导下划线符号放在需要的地方。cglobal宏也以相同的方式工作。你必须在定义一个符号前用cglobal,但你如果用GLOBAL便可以必须这样做。


7.4.2 内存模型
NASM不直接支持不同C内存模型;你必须跟踪你要写的哪一个。这意味着你必须做下面的事:
用一个单代码段的模式(tiny,small和compact),函数是near型的。这意味着将函数做为参数存入数据段或压入堆栈
时,函数指针将是一个16位并包含offset域(CS寄存器不会改变它的值,并给出函数全地址中的段部分),函数将
用序号near CALL指令调用并返回RETN(在NASM中等价与RET)。这就有两种相同的方法,你可以在代码里返回一
个RETN,也可以用near CALL指令调用外部C程序。
用多个代码段的模型(medium,large和huge),函数为far的,这就是说函数指针是一个32位长(包含一个16位的段
值加16位的偏移),函数用CALL FAR调用 (或CALL seg:offset)并返回RETF,并且用CALL FAR来调用外部程序。
在用一个单一数据段(tiny,small和medium)的模型中,数据指针为16位长的,只包含一个偏移域(DS寄存器不改变它
的值勤,并经常给出整个数据项地址的段部分。)
在用多个数据段的模型号中(compact,large和huge),数据指针是32位长,由一个16位的偏移和16位的段。你应该注
意不能修改你的程序,在后面没有存它时。但ES对你用来访问32位数据指针的内容是自由的。
大内存模式允许单数据项超过64K尺寸。在其它所有的数据模型中,你可以通过用给出的偏移值做算术运算来
访问整个数据项,一个段的值是否存在;在大模型中,你必须注意你的指针的数学运算。
在很多内存模型中,存在一个默认的数据段,整个段地址将保存在DS中在整个程序中。这个数据段做为堆栈
段保存在SS中,所以函数的本地变量(被存在堆栈中)和全局数据项存在其它段中不用改变DS可以简单被访问。
通常大数据项被存在其它段中。然而,有些内存模型(通常不为标准的)允许假定SS和DS保存了相同的值被移去。在后面例子中注意函数的本地变量。
在一个单代码段的模型中,段被定义为_TEXT,所以你的代码段必须用这个名字来连接到主代码段中相同的位置。在一个单数据段的模型中用一个默认的被叫_DATA的数据段。


--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:32:01

--

7.4.3 函数定义和函数调用
下面列出16位程序中的C调用。在下面的描述中,调用和被调用 指调用函数和被调用函数。
调用将函数参数压入堆栈,在剩下的顺序中一个接一个(从右到左,所以第一个指定的参烽将最后被
压入堆栈)。
调用然后指行一个CALL指令来将控制传给出被调用者。
这个CALL为near还是far取决于内存模型。
被调用者接收控制后,(虽然这并不必须,在函中不用访问它们的参数)开始将SP存入BP中以便可以用BP做为一个基址来在堆栈中找它们的参数。然而,调用者也可以做这个,所以调用部分的常规状态是BP必须
对任何C函数为可保存的。因此对被调用者,如果它做为一个frame指针设置时,必须
首先将前一个值压入堆栈。被调用者可以访问它的参数来引用BP。在[BP]中的值保存BP压入堆栈前的值;下一
字在[BP+2],保存部分返回地址的偏移部分,并通过CALL来隐式压入堆栈。在一个小模式(near)的函数中,返回地
址的段部分将存在[BP+4]中,并参数存在[BP+6]中。函数的最左边参数,将被最后压入堆栈,在从BP开始的偏移
是可以访问的;后面的其它值将会比偏移在。在此,象printf这样的函数将带很多的参数,并将参数按相反顺序压
入堆栈意味着在那个位置找到第一个参数,然后告诉它剩下参数的数量和类型。被调用者可以减小SP的值,以便
为本地变量分配堆栈空间,将从BP开始访问负偏移.
被调用者,如果它希望返回一个值给调用者,应该将值保存在AL,AX或DX:AX中取决于值的尺寸。浮点数有时(
取决于编译)将在ST0中返回。一旦被调用者完成处理,它将从BP中恢复SP,如果它分配了本地空间,弹出BP以
前的值,并通过RETN或RETF返回,这取决于内存模型。当调用者重新从被调用者得到控制时,函数参数将依然
在堆栈上面,所以它将加一个立即常数到SP上来移除它们(取代执行一系列的POP指令)。因此如果一个函数被
意外的用参数的错误号调用会引起一个原型不匹配的错误,从被调用 者那堆栈仍会返回一个敏感的状态,使我
们知道这有多少个参数被压入堆栈并被移除。用这个调用规则与Pascal程序的相比较是有意义的(在第7.5.1节
描述)。Pascal有一个简单的规则:没有函数带有可变的参数。然而被调用知道它应传递多少个参数,并能从它
自己的堆栈中不分配它们用用一个立即参数来RET或RETF指令,所以调用者不必做这个。参数也是从左到右的
顺序压入堆栈的,而不是从右到左,这表示一个编译器可以很好的保证关于性能上的顺序。因此,你可以在后
面定义一个C形式的函数下面是一个small model的例子:
global _)myfunc
_myfunc: push bp
mov bp,sp
sub sp,0x40 ;64字节的本地堆栈空间
mov bx,[bp+4] ;函数的第一个参数
mov sp,bp
pop bp ;取消上面的"sub sp,0x40"
ret

对于large-model函数,你应该用RETF来代替RET,用[BP+6]来代替第一个参数[BP+4]。所以,如果一个参数为指
针,那么子参数的偏移将会因为内存模型来改变:far指针当做为一个参数被传递时会在堆栈上放4个字节,而near
指针则会放两个。在另一个处理的结束,将从你的汇编代码中调用一个C函数。你可以象下面所做的:
extern _printrf
;然后,更多的...
push word [myint] ;我的一个整型变量
push word mystring ;指向我的数据段
call _printf
add sp,byte 4 ;\&#39;byte\&#39;保存空间
;然后为数据项
segment _DATAQ
myint dw 1234
mystring db \&#39;This number->%d<-should be 1234\&#39;,10,10
这个small-model汇编代码瑟下面的C代码等价:
int myint=1234
print("Thie number->%d<-should be 1234\\n",myint);
在large model中,函数调用代码看上去象这样,这个例子中假设DS已经为_DATA段的段基址,如果不是的话你
必须先初始化它:
push word [myint]
push word seg mystring ;现在压入段,并且...
push word mystring ;"mystring"的偏移
call far _print
add sp,byte 6
整数值将一个word放到堆栈上,这是因为large model对整数类型尺寸没有影响。第一个参数(最后压入的)将显示
,然而,如果为一个数据指针,则必须包含一个段和偏移部分。段将在内存中存在第二个位置上,但必须先压
入堆栈。(当然,PUSH DS 将为一个短型的指令而不是PUSH WORD SEG mystring,如果DS做为上面的例子被设置
时)这将变成一个far调用,因为函数将far调用 在large model;并且SP必须加6而不是4来构造另外的word参数。


7.4.4 访问数据项
为了得到C变量的内容,或定义C可以访问的变量,你只需用GLOBAL或EXTERN来定义它们.(名字需要用下划线来
引导,见第7.4.1节),因此从汇编中访问一个int i形式的C变量:
extern _i
mov ax,[_i]
为了定义一个C程序能象int j一样从外部访问,你可以这样做(确信你在一个_DATA段中汇编,如果需要):
global _j
_j dw 0
为了访问一个C的数组,你需要知道数组元素的尺寸.例如,int变量为2个字节长,所以一个C程序象int a[10]定义一个
数组,你可以用mov ax,[_a+6]来访问a[3].(字节偏移位置6由数组偏移3乘上数组元素的尺寸2得到).在16位的编译器
中C的基本类型尺寸为:char为1,short和int为2,long和float为4,double为8.为了访问一个C数据结构,你需要从知道你感
兴趣结构基位址中得到偏移.你也可以将C结构定义转换为NASM的结构定义(用STRUC)或象上面一样计算一个偏移
来做到这一步.为了做到上面这些,你应该读一个你的C编译器手册来找出它是如何组织数据结构的.NASM用它自己
STRUC宏不指定结构成员的偏移,所以如果C编译器产生偏移时,你必须在自己指定它.你可以发现如下的结构:
struct {
char c;
int i;
} foo;
为四个字节长而不是3个字节,因为int域为2字节对齐.然而这个特性在C编译器是可以配置的,或用命令行
参数#pargma,你也必须找出你自己的编译器是如何做的.


7.4.5 c16.mac:16位C接口的帮助宏
在NASM文档的misc目录下,有一个叫c16.mac的宏.它定义了三个宏:proc,arg和endproc.这些是为用C形式过程定义
的,在保存调用规则的轨迹方面它们会自动做很多工作.一个用宏来操作的汇编函数例子如下 :
proc _nearproc
%$i arg
%$j arg
mov ax,[bp+%$i]
mov bx,[bp+%$j]
add ax,[bx]
endproc
这里定义的_nearproc将为一个带2个参数的函数,第一个(i)为一个整数,第二个(j)为一个指向整数的指针.它将返回
i+*j.注意arg宏用了一个EQU做为表达式的第一行,因为在宏调用前的标号将会扩展第一行宏.EQU的工作是定义
%$i为从BP的一个偏移.一个上下文相关本地变量的使用:本地的上下文相关将审美观点proc压入堆栈,而用endproc
宏弹出堆栈 ,所以相同的参数名字可以在后面的过程中使用.当然你不必这样做.宏对于near函数设置了默认的
过程代码(tiny,small和compact-model代码),你可以用%define FARCODE来生成far函数( medium,large和huge-mode代
码).这将通过endproc改变return指令的类型,并改变参数开始点的偏移.宏设置包含在本质上不从属于数据指针是
近还是远.arg可以带一个可选的参数,给出参数的尺寸 .如果没给出尺寸便设为2,因为象许多函数参数一样为int类
型.large-model与上面相同:
%define FARCODE
proc_farpoc
%$i arg
%$j arg 4
mov ax,[bp+%$i]
mov bx,[bp+%$j]
mov es,[bp+%$j+2]
add ax,[bx]
endproc
因为j为一个远指针,所以arg宏的参数被定义成了尺寸为4的参数.当我们从j取数据时,我们必须取出一个段和一个
偏移.


7.5 对于Borland Pascal程序的接口
Borland Pascal程序的接口与16位C程序的接口相似.不同的是:对于C程序接口的前导下划线对于Pascal不需要.
内存模型经常为large:函数为far,数据指针为far,并且没有数据项能超过64K长.(事实上,一些函数是near,而且那些对
一个Pascal单元来说为本地的函数,永远不会在外部调用它所有Pascal调用的汇编函数,和所有汇编程序都能调用
Pascal函数为far型的.)然而所有在Pascal程序定义的静态数据会在一个默认的数据段中,当控制被传到你的汇编代
码时,它们的段地址将会在DS中.但当默认的数据段为本地变量时这将不成立(它们在堆栈段)并且支态的分配本地
变量.所有数据指针将是far的.函数调用规则不同-如下描述.一些数据类型如string存储也不同.
这将限定你们允许使用的段名字-Borland Pascal将忽略在一个段中的定义的代码和数据,这个段的名字不符合上面
,具体描述见下面.


7.5.1 Pascal调用规则
16位的Pascal调用规则如下 ,下面的描述中,词调用 者与被调用者是指做调用工作的函数和被调用的函数.
调用者将函数的参数一个接一个按正常的顺序压入堆栈(从左到右,所以第一个参数将被首先压入堆栈).
调用者然后执行一个far CALL指令将控制传给被调用者.
被调用者接到控制,开始(虽然事实上不需要,在函数中不必访问它们的参数)将SP入到BP中,以使BP可以做
为一个基指针来找出堆栈上它们的参数.然而,调用者也可以正确的做这个,所以调用规则的部分状态为BP必须被
一些函数保存.因此被调用者,如果它将BP做为一个frame指针设置的话,必须将前面的值压入堆栈.
被调用者可以参照BP来访问它的参数.[BP]中的值将为它压入堆栈前的值.在[BP+2]中将保存返回值的偏移部分,而
[BP+4]中则保存段值.参数值从[BP+6]开始.函数最右边的参数最后压入堆栈,可以从BP中得到这个偏移;其它后面的
值则会比偏移大.
被调用者可能希望以后减小SP的值,以便为本地变量分配堆栈空间,这些变量将从BP中得到负的偏移来被访问.
被调用者,如果它希望返回一个值给调用者,应该将值丰入AL,AX或DX:AX这取决于值的尺寸.浮点数的值存在ST0中.
Real(Borland的本身定义的浮点数据类型不能直接被FPU处理)类型的结果将存在DX:BX:AX.对于字串结果的返回值
,调用者将在压入参数前将一个临时字串的指针压入堆栈,并且被调用者将在这个位置放入返回的字串值.字串不是
一个参数,并且不应该用RETF指令从堆栈中移去.一旦被调用者完成处理,如果它分配本地堆栈空间,它将从BP中
恢复SP,然后弹出BP的值勤,并通过RETF返回.它用一个带立即参数的RET格式来给出要弹出堆栈的字节数.这使从
堆栈中移除的参数对返回指令产生一个边界反映.
当调用者重新从被调用者那里得到控制时,函数的参数可能已经从堆栈中移去,所以它不需要做什么工作.因此你
可以象下面一样定义一个Pascal形式的函数,带两个整型参数:
global myfunc
myfunc: push bp
mov bp,sp
sub sp,0x40 ;64个字节的堆栈空间
mov bx,[bp+8] ;函数的第一个参数
mov bx,[bp+6] ;函数的第二个参数
;一些代码
mov sp,bp ;取消"sub sp,0x40"
pop bp
retf 4 ;参数的尺寸为4
在一个进程结束时,从你的汇编代码中调用一个Pascal函数,可以用下面的代码:
extern SomeFunc
;后面下面
push word seg mystring ;现在将段压入堆栈,
push word mystring ;"mystring"的偏移
push word [myint] ;我的变量中的一个
call far SomeFunc
这等于Pascal代码:
procedure SomeFunc(String: PChar;Int: Integer);
SomeFunc(@mystring,myint);


7.5.2 Borland Pascal段命名限制
由于Borland Pascal内部单元文件格式与OBJ完全不同,当连接时,从一个OBJ文件读取和理解相关信息时将是一
个写生的过程。然而一个目标文件连接到一个Pascal程序时将必须遵守一些限制:
过程和函数必须为一个叫CODE,CSEG,_TEXT的段。
初始化数据必须在一个叫CONST或_DATA的段。
未初始化的数据必须在一个叫DATA,DSEG,_BSS的段。
另外一些在目标文件中的段将完全被忽略。GROUP定向符和段属性也将被忽略。


7.5.3 在Pascal程序中用c16.mac
c16.mac宏文件在第7.4.5节描述,也可以用来写一个被Pascal程序调用的函数,如果你用%define PASCAL时。这个
定义保证了函数为far(它执行FARCODE)并使过程返回一个用操作符生成的指令。定义PASCAL不会改变计算参数
偏移的那些代码;你必须用相反的顺序定义函数的参数。如:
%define PASCAL
proc_pascalproc
%$j arg 4
%$i arg
mov ax,[bp+%$i]
mov bx,[bp+%$j]
mov es,[bp+%$j+2]
add ax,[bx]
endproc
这定义了与第7.4.5节中的例子在概念上相同的例程:它定义了一个带2个参数的函数,一个整数和一个指向整数
的指针,将返回整数的和与指针的内容。与large-model C版本的唯一区别为定义PASCAL来代替FARCODE的定义
,参数将以相反的顺序定义?


--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:34:28

--

第八章 写32位代码(Unix,Win32,DJGPP)


本章试途说明:在写能运行在Win32或Unix下或由象DJGPP Unix形式的C代码编译器,的32位代码时的一个相关
内容。它包含了如何用汇编写32位C程序的接口,如何为共享库写位置独立代码。
很多32位代码,那些能在Win32,DJGPP或PC Unix变体平台下运行的,都是运行在平坦内存模式上。这意味段
寄存器和分页机制将给你32位的4Gb地址空间,而不用担心你在用哪个相关段,而且你也应该完全忽略所有的段
寄存器。当写平坦模式的应用代码时,你永远不需要重载或修改任何段寄存器,你可以在相同的地址空间将代
码地址做为CALL和JMP的参烽你可以通过数据段地址访问你的变量,用堆栈段地址访问你的本地变量和过程参
数。每一个地址是32位的并只包含偏移部分。


8.1 32位C程序的接口
在第7.4节中很多关于16位C程序的讨论也适应于32位的工作中。内存模型的缺乏和段限制使事情变得简单。


8.1.1 外部符号名
许多32位C编译器都共享16位编译器的限制,所有全局符号(函数或数据)名字的定义格式象C程序中一样在名字前
加一个前缀下划线。然而,并不是所有都是这样:ELF规则为C符号在它们的汇编语言名前可以没有前导下划线。旧的Linux a.out C编译器,所有的Win32编译器,DJGPP和NetBSD和FreeBSD都用前导下划线;对于这些编译器,在第7.4.1节给出的宏cextern和cglobal将仍然有效。对于ELF前导下划线不应该使用。


8.1.2 函数定义和函数调用
在32位程序中的C调用限制如下所示。在下面的描述中,调用者和被调用者表示执行调用的函数和被调用的函数
调用者将函数的参数一个接一个按相反的顺序压入堆栈(从右到左,所以第一个参数将最后压入堆栈)。
调用 者执行一个CALL指令将控制传给被调用者。
被调用者返回控制,并(虽然这不需要,在函数里不必访问它们的参数)开始将ESP存入EBP使EBP做为
一个基址来从堆栈中找出它们的参数。然而调用者也可以这样做,所以调用限制为EBP必须被任何的C函数保存
。因此被调用者,如果它做为一个frame指针来设置EBP,必须先将前一个值压入堆栈。
调用者可以根据EBP来访问它们的参数。在EBP被压入堆栈时,[EBP]中将保存的EBP以前的值;[EBP+4]中为返回
地址,通过CALL隐式压入。这个参数将在[EBP+8]开始。函数最左边的参数将最后被压入堆栈,并可以从EBP中
访问这个偏移;其它的将会成功的大于这个偏移。因此,在一个象printf这样带一系列可变参数的函数里,以相
反顺序压入堆栈意味着函数知道在哪里找到它的第一个参数,并从这个参数中得到剩余参数的个数和类型。
被调用者可能希望在以后可以减小ESP,以为本地变量在堆栈中分配空间,这个可以访问EBP中的负偏移。
被调用者,如果它希望从调用者那里返回一个值,应该将值保存在AL,AX或EAX,具体哪个寄存器取决于值的
尺寸。浮点数的结果将返回在ST0。一旦被调用者完成处理,如果它分配了本地堆栈空间,它将从EBP中恢复
ESP,然后将EBP以前的值弹出堆栈,并通过RET返回(也可用RETN)。
当调用者从被调用者那里重新得到控制,函数的参数将仍在堆栈上,所以为了移去它们(不是执行一系列的POP
指令)将会加一个立即常数到ESP上。因此如果一个函数被错误数量的参数将会引起一个原型不匹配的警告,因
为调用者知道有多少个参数被压入堆栈或移除,堆栈将返回一个敏感的状态。对于用Windows API调用的
Win32程序将是一个可选择的调用限制,并象Window过程一样被Windows API调用:它们将跟在微软的_stdcall
限制后。这与Pascal限制有些相似,被调用者将通过传一个参数给RET指定来清除堆栈。然而,参数仍然以从右
到左的顺序压入堆栈。因此你可以用下面的方式定义一个C形式的函数:
global _myfunc
_myfunc: push ebp
mov ebp,esp
sub esp,0x40 ;64字节的本地堆栈空间
mov ebx,[ebp+8] ;函数的第一个参数]
;一些代码
leave ;mov esp,ebx/pop ebp
ret
在一个进程结束时,为了从你的汇编代码中调用一个C函数,你应该象下面这样写:
extern _printf
;更多下面
push dword [myint] ;我的整数变量中的一个
push dword mystring ;指向我的数据段
call _printf
add esp,byte 8
segment _DATA ;\&#39;byte\&#39;存储空间
myint dd 1234
mystring db \&#39;This number->%d<-should be 1234\&#39;,10,0
这段代码将与下面的C代码等价:
int myint=1234;
printf("This number->%d<-shoulkd be 1234\\n",myint);


8.1.3 访问数据项
为了得到C变量的内容或定义一个C可以访问的变量,你只须将名字做为GLOBAL或EXTERN来定义。(名字象第
8.1.1节中一样要带前导下划线)。因此,一个C变量int i用下面代码可以被访问:
exern _i
mov eax,[_i]
如果定义你自己的整型变量使它们可以被C程序访问应该用extern int j,你可以这样做(如果需要确信你是在_DATA
段中写代码):
global _j
_j dd 0
为了访问一个C数组,你必须知道数组元素的大小。例如,int变量为4个字节长,所以如果一个C程序定义一个象
int a[10]的数组,你可以用代码mov ax,[_a+12]来访问a[3]。(字节偏移12是用数组索引3乘上数组元素的尺寸4得到
的)。32位编译器的C基本类型尺寸为:char为1,short为2,int,long,short为4,double为8。对于32位地址的指针也为
4个字节。为了访问一个C数据结构,你需要知道从结构的基地址到你想要的域的偏移。你也可以通过将C结构
定义转换成NASM里定义的结构(用STRUC), 或者用上面内容计算一个偏移。为了能做到这些,你应该读一个你
的C编译手册来找出它是如何组织数据结构的。NASM在它自己的STRUC宏不指定结构的对齐格式。所以你必须
自己指定对齐格式。你会发现下面的结构。
struct {
char c;
int i;
}foo;
为8个字节长而不是5个,因为int域为4个字节对齐的。然而,这种特性在C编译器是一个可配置的选项,也可以
用命令行参数#pargma,拟你必须找出你的编译器是如何做的。


8.1.4 c32.mac:32位C接口的帮助宏
在NASM文档的misc目录下有一个宏文件c32.mac。它定义了三个宏:proc,arg和endproc。这对C形式的过程定义
有用处,它们自动对保存调用限制的轨迹做很多工作。一个用宏来设置函数的例子如下:
proc _proc32
%$i arg
%$j arg
mov eax,[ebp+%$i]
mov ebx,[ebx+%$j]
add eax,[ebx]
endproc
这里定义了_proc32为一个带2个参数的过程,第一个(i)为一个整数而第二个(j)为一个指向整数的指针。它返回
i+*j。注意arg宏会将它的第一行扩展成一个EQU。因为宏前的标号将调用宏扩展的第一行,EQU的工作为定义
%$i为从BP中得到的一个偏移。一个相关本地变量被使用,本地的通过proc宏来压入堆栈 并且通过endproc宏
来弹出堆栈。所以相同名字的参数将在后面的过程中被使用。当然,你不必那样做。arg可以带一个可选的参
数,来给出参数的尺寸。如果尺寸没有给出则为4,因为许多参数都为int或指针。


8.2 写NetBSD/FreeBSD/OpenBSD和Linux/ELF共享库
ELF将替换Linux下老的a.out格式,这是因为它支持位置独立代码(PIC),这可以使写共享库变得容易些。NASM支
持ELF的位置独立代码特性,所以你可以在NASM里写Linux的ELF共享库。NetBSD,与它的近亲FreeBS和OpenBSD
在支持a.out格式的PIC方面采用不同方法。NASM做为aoutb输出格式支持这个,所以你也可以用NASM写一个BSD
共享库。操作系统通过内存映象文件写一个PIC共享库。在运行进程的地址空间的任何附近的点。库代码段内容
必须与它在内存中哪个地方装载无关。然而 ,你不能用下面的代码定义你的一个变量 :
mov eax,[myvar] ;错误
相反,连接器提供了一个叫全局偏移表的内存空间,或GOT;GOT被放在从你库代码一个常数距离的位置,所以
你可以找出你的库是在哪个地方装载的(用CALL和POP连合),你可以得到GOT的地址,你可以在GOT中连接器入
口外装载变量。一个PIC共享库的数据段没有这些限制:由于数据段是可写的,它必须拷贝到内存中而不是从
库文件中分页,一旦它被重定位也需要拷贝。所以你可以将重定位序号类型放在在数据段中而不用担心。(关于
一个警告见第8.,2.4节)。


8.2.1 取得GOT的地址
你共享库中的每一个代码模块,应该做为一个外部符号定义GOT:
extern _GLOBAL_OFFSET_TABLE_ ;在ELF中
extern _GLOBAL_OFFSET_TABLE_ ;在BSD a.out
在你共享库中任何函数的开始将计划访问你的数据或BSS段,你必须计算GOT的地址。这个可以通过下面的格式
写函数:
func: push ebp
mov ebp,esp
push ebx
call .get_GOT
.get_GOT:pop ebx
add ebx,_GOBAL_OFFSET_TABLE_+$$-.get_GOT wrt ..gotpc
;函数主体
mov ebx,[ebp-4]
mov esp,ebp
pop ebp
ret
(对于BSD,符号_GLOBAL_OFFSET_TABLE需要二个前导下划线。)函数的前两行为简单的标准C序言来设置一个
stack frame,最后三行为标准C函数尾声。第三行和第四行到最后一行将保存并恢复EBX寄存器,因为PIC共享库
用这个寄存器存储和恢复GOT的地址。最有兴趣的位为CALL指令和后面的两行。CALL和POP合起来将得到标号
.get_GOT的地址而不用知道程序是在哪个地方装载的(CALL指令用当前位置来编码)。ADD指令将利用指定PIC重定
位类型的一个:GOTPC重定位。用WRT .gotpc指定限定,引用的符号(这个_GLOBAL_OFFSET_TABLE_,指定的符号将
被指定为GOT)将从段开始给出一个偏移。(事实上,ELF将它编码为ADD指令操作符域的偏移,但NASM将故意简
化它,所以你可以对ELF和BSD做相同的事。)所以指令将加段开始的值,来得到GOT的真实地址,并且减去
.get_GOT从EBX得到的值。然而,当指令完成时,EBX包含GOT的地址。如果你不象上面的,也不用担心:它将
永远需要得到GOT地址,所以你可以将那三个指令放到一个宏中并安全的忽略它们:
%macro get_GOT 0
call %%getgot
%%getgot:pop ebx
add ebx,_GLOBAL_OFFSET_TABLE_+$$-%%getgot wrt ..gotpc
%endmacro


8.,2.2 找出你本地的数据项
当得到GOT,你也可以用它得到你数据项的地址 。许多变量可以放在你定义的段中;它们可以用..gotoff指定WRT
类型来访问。如下:
lea eax,[ebx+myvar wrt ..gotoff]
表达式myvar wrt ..gotoff被计算,当共享库被连接时,将会成为从GOT开始的本地变量myvar的偏移。然而,将它
加到一EBX上将在EAX中入入一个myvar的真正地址。
如果你不指定它们的尺寸而用GLOBAL定义变量时,它们将在库中的代码模块间共享,但不能从库引用到装载它
的程序。它们将仍在你的原始数据和BSS段中,所以你可以与本地变量一样的方法访问它,用上面的..gotoff
机制。注意由于BSD a.out格式处理这种重定位类型的特性,在你访问的地址上必须有至少一个非本地符在相同
的段中。


8.2.3 找出外部和常用数据项
如果你的库需要得到一个外部变量(在库的外部,而不是它里面的一个模块),你必须用..got类型来得到它。..got
类型取代给出唑GOT基址到变量的偏移,而是给出从GOT基址到一个包含变量地址的GOT入口的偏移。连接器在
它构造库时将设置这个GOT入口,动态连接器将在装载时放置正确的地址在里面。所以在EAX里得到外部变量
extvar的地址,你应该如下:
mov eax,[ebx+extvar wrt ..got]
这将在GOT入口外装载extvar的地址。连接器,当它构造共享库时,将每个类型..got的重定位收集到一起,然后
构造GOT以保证它有每一个需要的入口位置。常用变量必须这样来访问。


8.2.4 引出符号给库用户
如果你想将符号引出给库用户,你必须定义它们是函数还是数据,如果它们是数据,你必须给出数据项的尺寸
。这是因为动态连接器必须为引出函数构造过程连接表入口,并也可以从它们定义的库数据段中移去外部数据
项。所以导出一个函数到库用户,你必须用:
global func:functioin ;定义它为一个函数
func: push ebp
;等等
也可以导出一个象数组的数据项,你必须写一个全局数组:data array.end-arrya;也给出尺寸。
array: resd 128
.end:
注意:如果你导出一个变量到库用户,通过用GLOBAL定义并且提供一个尺寸,变量将在主程序中的一个数据段
结束,而不是在你定义的库数据段,所以你必须用..got机制来访问全局变量而不是用..gotoff,就象它是外部的。
(它将会有效的)。相等的,如果你需要在你的数据段中的一个存储一个导出全局符号的地址,你不能用下面的
代码实现:
dataptr: dd global_data_item ;错误
NASM将做为一个序号重定位来解释,global_data_item将是一个从.data段开始的偏移(什么都可以),所以这将
引用你的数据段结束点而不是外部符号在别的地方。上面的代码将写成:
dataptr: dd global_data_item wrt ..sym
这将利用指定的WRT类型 ..sym来使NASM为找一个特殊符号而搜索符号表,而不是用段基址重定位。
每一种方法都将为函数工作:引用你函数中的一个:
funcptr: dd my_function
将给用户一个你写代码的地址,而
funcptr: dd my_function wrt ..sym
将给出一个函数的过程连接表的地址, 这将调用程序相信函数的存在。每一个地址都可以有效的调用函数。


8.2.5 在库外面调用过程
在你的共享库外面调用过程必须用一个过程连接表或PLT来做。PLT被放在库装载时的一个已知的偏移,所以库
代码可以做一个位置独立代码来调用PLT。在PLT中代码将跳过在GOT中的偏移,所以函数调用其它共享库或在
主程序中的例程可以传给它们真正的目标。为了调用一个外部例程,你必须用另外一个指点定的PIC重定位类
型,WRT ..plt。这将比GOT-基位置容易:你可以简单的用PLT-相关版本CALL printf WRT ..plt代替CALL printf。


8.2.6 生成库文件
写一些代码模块并将它们汇编成.o文件,然后用下面的命令行生成你的共享库文件:
ld -shared -o library.so module1.o module2.o #对于ELF
ld -Bshareable -o library.so module1.o module2.o #对于BSD
对于ELF,如果你的共享库是在系统的子目录中如/usr/lib或/lib,它将会用-soname标志给连接器,来存最终文件的
名字。并用一个库中的版本号:
ld -shared -soname library.so.1 -o library.so.1.2 *.o
你也可以将library.so.1.2拷贝到库目录中,并创建library.so.1做为一个符号连接。

--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:35:37

--

第九章 混合16位和32位编码


本章主要说明一些不常用的地址格式和跳转指令,在写象保护模式下的操作系统代码时需要的混合段尺寸操作
代码,如16位的段试途修改一个32位的数据或在不同尺寸段间的跳转。


9.1 混合尺寸跳转
当写一个32位的系统时要用一个常用的混合尺寸指令:你在16位模式设置,如取内核,然后切换到保护模式中
跳到32位内核的起始地址。在一个全32位的OS中,这将需要混合指令,因为种种原因在它在纯16位代码运行前
和纯32位代码运行后。跳转必须指定一个48位的远地址,因为种种原因一个目标地址为32位的。然而,它必须
在一个16位的段中汇编。所以这样做:
jmp 0x1234:0x56789ABC;错的
将不工作,因为偏移部分的地址将被载成0x9ABC并跳到一个原始的16位远地址处。
Linux内核设置代码得到相似于as86用代码生成的指令,用DB指令。NASM可以更好些,但它自己会生成正确的
代码: jmp dword 0x1234:0x56789ABC;对的
DWORD前缀(严格的说,它应该跟在冒号后面,因为种种原因它定义了一个doubleword的偏移;但NASM将会接受
这种格式,因为两者都是明确的)将使偏移部分被做为far对待,假设你故意写一个从16位段到32位段的跳转。
你可以保佑和相反操作,从32位段到16位用一个WORD前缀:
jmp word 0x8765:0x4321 ;32到16位
如果WORD前缀在16位模式中被指定,或DWORD前缀在32位模式它们将被忽略,因为每一个会使NASM在它在的模式中操作。


92. 在不同尺寸段中定址
如果你的OS是16位和32位混和,或你正写一个DOS扩展,你想处理一些16位段和32位。这时,你将在16位段中
写一个访问在32位段中的数据,反之也是。
最简单的方法是保证你用一个地址寄存器,因为有效地址包含一个32位的寄存器将被变成32位地址 ,所以:
mov eax,offset_into_32_bit segment_specified_by_fs
mov dword [fs:eax],0x11223344
这样做不错,但如果你已经知道了你要的精确偏移,这就有些讨厌(因为它浪费了一个指令和一个寄存器)。x86
体系允许32位有效地址指定4字节的偏移,所以为什么NASM不能为这个目的生成最好的指令?它能,象在第
9.1节中,你只需要用DWORD前缀地址,并且为一个32位的地址:
mov dword [fs:dwrod my_offset],0x11223344
也象在第9.1节中一样,NASM不注意DWORD前缀是在段重载前还是后,所以关于一个美观的争论为:
mov dword [dword fs:my_offset],0x11223344
不要对DWORD前缀在方括号外感到迷惑,这是为了控制存在地址上的数据的尺寸 。在方括号里面的将控制地址
本身的长度。这两个很容易区分:
mov word [dword 0x12345678],0x9abc
这将移动16个位数据到用32位偏移指定的位置。你也可以指令WORD或DWORD前缀来使FAR前缀来表示跳转或
调用,例如:
call dword far [fs:word 0x4321]
这条指令包含了16位偏移指定的地址;它从中取出48位远指针(16位段和32位偏移),然后调用地址。


9.2 其它混合尺寸的指令
另外的访问数据的指令为字串指令(LODSx,STOSx等待)或XLATB指令。这些指令由于它们没有参数,在16位段中
汇编是时32位定址不是很容易。这是NASM用a16和a32前缀的原因。如果你在16位段中用LODSB编码但它支持
在32位段中访问一个字串,你可以从ESI中取出地址然后:
a32 lodsb
这个前缀使定址的尺寸为32位的,意味着LODSB从[DS:ESI]中取数而不是[DS:SI]。当写一个32位代码时要在
一个16位段中访问字串,可以用a16前缀。
a16和a32前缀可以用在NASM指令表中的任何指令前,但大多数指令在没有用前缀的时,也会生成正确的指令。
这个前缀对于隐式的定址是有用的:CMPSx,(见第A.24节),SCASX(见第A.229节),LODSx(见第A.117节),STOSx(见第
A.243节),MOVSx(见第A.137节),INSx(见第A.98节),OUTSx(见第A.149节)和XLATB(见第A.269节),不同的push和pop指令
也可以用a16或a32做前缀使SP或ESP中的一个成为堆栈指针。(PUSHA和POPF与PUSH和POP一样)在这种情况下,
堆栈段将与代码段为不一样的尺寸 。PUSH和POP,当用在32位模式的段寄存器中时,每次将处理4个字节,上
面的2个将忽略,而下面的2个将交给段寄存器处理。为了使用16位行为的段寄存器push和pop指令,你可以用操
作尺寸前缀o16:
o16 push ss
o16 push ds
这段代码将存一个doubleword的堆栈通过将两个段寄存器修改空间用压入堆栈和消费的方法。(你也可以用o32前
缀在16位模式中用,但这很少用。

--------------------------------------------------------------------------------

-- 作者:航天奇侠
-- 发布时间:2004-7-1 11:36:51

--

第十章 常见问题

本章描述了用户在使用NASM所遇到的一个问题及解答。它也给出了一些NASM中的显示bugs的指令,如果你发
现它不在列表中时。


10.1常见问题

10.1.1 NASM生成的无效代码
我得到很多\&#39;bug\&#39;显示NASM生成了无效的或\&#39;wrong\&#39;的代码象指令ADD ESP,8。这是一个故意设计的特性:连到
预期的输出:NASM处理ADD ESP,8时将生成为32位偏移保存空间的指令。你需要用ADD ESP,BYTE 8如果你想要
让空间有效的指令。这不是bug:这是一个有歧义的特性并只是一种观点。


10.1.2我的跳转出界了
同样,人们抱怨当他们用跳转指令跳太远时,NASM报告\&#39;short jump out of range\&#39;而不是生成一个远跳转。这也是
预处理的一种,但事实是有更多的原因。NASM不能告诉代码所运行的处理器的类型;所以它不能决定它是在
386还是更高的机器运行生成Jcc NEAR类型的指令。另外它可心伤脑筋一个短JE指令代替出界的短JNE指令也就
是跳过JMP NEAR;这对低于386的处理器是一个敏感的解决方法,保龄球对有好的预期操作或用JNE NEAR的处
理器上却无效。所以这是由用户而不是编译器来决定应生成什么的指令。


10.1.3 ORG不能工作
人们用bin格式引导程序经常抱怨ORG不能象他们想的去工作:为了在512个字节后放一个0xAA55标志,他们的
用MASM代码如下:
ORG 0
;一些扇区代码
ORG 510
DW 0xAA55
而在NASM中用ORG定向符则不工作,在NASM中正确的方法是TIMES定向符:
ORG 0
;一些扇区代码
TIMES 510-($-$$) DB 0
DW 0xAA55
TIMES定向符将会插入适当的0到输出中使汇编大小为510个字节。这种方法也可以是如果你意外的将你的引导
扇区填满,NASM将在汇编时捕到错误并显示,所以你不必在运行时反汇编它来找错误。


10.14 TIMES不工作
用上面代码常见的问题为人们用下面的代码:
TIME 510-$ DB 0
这会使$成为一个纯数字,象510一样,它们间的区别为一个数字被传给了TIMES。NASM是一个模块式的编译器
:不同的单元被设计成可重用的和易分隔的,所以它们不需要交换信息。结果bin输出格式甚至用ORG定向符
来表明.text段为从0开始,而不把这个信息传给后面的表达式。所以从计算的观点看,$不是一个纯数字:它是
从一个段基址的偏移,然而在$和510之间不同的也不是纯数字,除了包括一个段基址。包含段基址的值的不能
被传到TIMES里做参数。象前面的面一样,应中下写:
TIMES 510-($-$$) DB 0
这里$和$$是从相同段基址的偏移,所以它们的区别为纯数字。这解决了问题并生成敏感的代码。


10.2 Bugs
我们从未出版过带有任何已知问题的NASM。这不会经常停在我们不很知道的地方。你发现的任何问题请向
[email]hpa@zytor.com[/email]报告。
请先读第2.2节,不要报告列在列表中的问题。(如果你觉得这个特性不好请说明原因,但不要写\&#39;This is a buf\&#39;)。
然后 读一下10.1节也不要报告在列表中问题。
如果你报告了一个问题,请给我们下面的信息:
你用NASM的操作系统,DOS,Linux,NetBSD,Win16,Win32,VMS(外加的)或其它。
如果你在DOS或Win32下运行NASM,请告诉我们你是从DOS源文档中编译的执行文件还是用标准的二进制文档。
如果你用本地的二进制文件,请用标准的二进制文件重新生成一遍,使我们更容易修正问题。
你用的NASM的版本号,及你如何运行它的。给我们精确的命令行。和NASM环境变量的内容。
你所用程序的任何补充版本,你是如何执行它们的。如果问题在连接时出现,说明你用的连接器,及它的版本
和连接器的命令行。如果问题在连接用另外的编译器生成的目标文件出现的,说明是什么编译器,什么版本,
和你用的命令行。(如果你用的是一个IDC请用命令行版本的编译重新生成一下。)
如果有可能,请发给我们一分NASM出错的源文件。如果这引起版权问题(如你只能重生成限制发布代码的一个
问题)那么考虑以下两点:首先,我们保证任何发给我们源码的文件只用于调试NASM,如果我们修改完后,我们
将删除我们所有的拷贝。第二,我们接收很大的源码。文件越小越好。一个三行的文件将比上千行的代码更容易找到问题。(当然在大文件中有些问题突然出现)
问题的具体描述,\&#39;它不能工作\&#39;不是一个有用的描述!请精确的描述应该发生什么现象,不应该发生什么。例如:
\&#39;\&#39;NASM生成一条错误信息表示第3行有个错误实际在第5行\&#39;,\&#39;NASM生成一个错误信息我认为 它根本不应该生成\&#39;
,\&#39;从源码生成的目标文件使我的连接器不能工作\&#39;;\&#39;一个输出文件的第9个字节为66而我认为它应为77\&#39;
如果你认为 输出文件从NASM出来是错的,请发给我们。这让我们来决定我们的NASM输出是否为同一个文件,
或问题是在于我们还是你们。我们可以处理的二进制文件为MIME附件,uuencode和BinHex.我们提供FTP站点
你可以上传有问题的文件,但寄给我们是最容易的。
另外信息或数据文件可能有用,如果NASM失败生成一个目标文件而TASM生成一个无错的等同文件。那么请把
两个文件都寄给我们。我们可以看出TASM的不同?

suntercel 2005-12-13 21:23

晕了。。。。。还好考试不考这么难。。。

页: [1]
© 1999-2008 EvilOctal Security Team