2007-12-27

Gnome 下 把 Dream Aquarium 设为壁纸

在Windows下发现这个软件不错后,决定要尝试把它弄成我的ubuntu的“壁纸”

首先是安装,默认会出问题,原因是少了个gdiplus.dll, Dream Aquarium主页上关于Win2000版本的地方有说明。 不过我的XP似乎没有这个文件也能跑。 总之,下一个gdiplus.dll放wine的系统目录里就好了。

如果想装破解版,会麻烦些,我按照Windows的方法试了好久,没有成功,最后是胡搞了几次才成,可能有用的几点是:

1.破解包里有一个dai文件,用16进制编辑器查看发现其前三行是明文,之后是一个PE文件头,于是删掉前三行,果然可以运行,但是经常会crash。 这个文件应该就是1.1090完全版的主程序了。一般在鼓弄过程(最好还是按着破解包里面的说明弄)中可以试试这个能不能正常运行,即显示鱼缸。我这里最好结果是它可以运行,但是仍是使用版,似乎只能加2号鱼。

2.注册似乎一直都不成功,我最后是在上面一条成功后,认为注册表方面没问题了,然后把Windows下的安装文件拷过来了一份,于是能正常运行了,可加的鱼也没有限制了。

3.破解包里说要block这个屏保的网络连接,但是我不会弄,说不准哪一天屏保自己发现被破解了,又拒绝运行了。不过现在已经运行了几次,没有问题。

能运行以后,开始尝试把它弄成壁纸.

首先是要配置一下wine.设上虚拟桌面,并去掉Allow the window manager to control the windows。 我一开始没设好这个,总是不行。原因大概是屏保发现失去焦点后就会自动退出,可是我们把它设成了桌面以后,肯定要做别的事,于是切换到其他窗口时,屏保就退出了。 去掉Allow the window manager to control the windows 后, 该窗口就得不到失去焦点的信息了,也就不会自动退出了。 另外如果不选上虚拟桌面,后面用的devilspie会找不到这个窗口。

其次,配置一下屏保(在屏保里面按回车),去掉鼠标退出,然后把键盘退出的范围改小,我弄的是Esc only。

接着是装上devilspie, 在home里建目录.devilspie,并写一个文件DreamAquarium.ds
(if (is (application_name) "Wine desktop")
(begin
(below)
(undecorate)
(focus)
(wintype "desktop")
(opacity 60)
(pin)
(stick)
(skip_pager)
(geometry "1280x800+0+0")
))
再运行devilspie和屏保就成了 :)

此外可以参考Dream Aquarium as a Desktop Wallpaper with Beryl!
我这里是Compiz,一直没弄成功,也不知道是Compiz不兼容还是我没配好。

另外说一下这个方法的不足,从脚本中可以看出我是把Wine desktop设了stick, below之类,因此对其他在同一个wine环境下运行的程序也有效,特别地,屏保会退出。 当然可以设一下用其他环境,像我这的ie4linux就是如此。 当然我很少用wine程序, 而且ie4linux由于是单独的环境与其互不影响,所以不成大问题。

还有,opacity这个参数似乎不起作用,于是桌面图标看不到了
喜欢的话还可以把devilspie设成自启动。

软件推荐: Dream Aquarium

最初听说Dream Aquarium是在一个介绍如何把它嵌入Beryl桌面的帖子,当时wine安装都没成功。

最近在Windows试了下, 确实不错。很有趣的屏保。

最新试用版是1.1040, 而最新正式版似乎是1.1090

破解不大好找,但确实能找到,我找到的链接给删掉了,好像是在www.leehare.cn.

2007-12-22

gnome-blog: blog小工具

看full circle发现了一些blog工具, 后来自己又查了查,发现如下几个
gnome-blog //gnome-panel 的 applet
drivel //程序
blogtk //程序
scribefire //firefox 插件

其中除最后一个外均可以正常发帖,但都不支持tag。 感觉还是gnome-blog比较好,因为功能方面三者差不多,但它最轻巧。

xournal: 批注pdf的小工具

最近多人合写一个pdf, 需要批注功能。

网上搜了搜,发现了一个叫xournal的软件,试了试, 很不错。

非常推荐。 另外希望evince也能加入这个功能。

2007-12-19

修改UBuntu登录后桌面显示的黄色

sudo gedit /etc/gdm/PreSession/Default

# Default value
if [ "x$BACKCOLOR" = "x" ]; then
BACKCOLOR="#dab082"
fi


#dab082 就是颜色 可以随便改
例如 #000000 为黑
#ffffff白色

2007-12-18

wine下安装DirectX 9.0

参考http://feeds.feedburner.com/~r/winereview/~3/187817620/directx-90c-on-linux-with-wine.html

首先拷来mscoree.dll 和 streamci.dll
然后设置dll override
"d3d8"="builtin"
"d3d9"="builtin"
"d3dim"="native"
"d3drm"="native"
"d3dx8"="native"
"d3dxof"="native"
"dciman32"="native"
"ddrawex"="native"
"devenum"="native"
"dinput"="builtin"
"dinput8"="builtin"
"dmband"="native"
"dmcompos"="native"
"dmime"="native"
"dmloader"="native"
"dmscript"="native"
"dmstyle"="native"
"dmsynth"="native"
"dmusic"="native"
"dmusic32"="native"
"dnsapi"="native"
"dplay"="native"
"dplayx"="native"
"dpnaddr"="native"
"dpnet"="native"
"dpnhpast"="native"
"dpnlobby"="native"
"dsound"="builtin"
"dswave"="native"
"dxdiagn"="native"
"mscoree"="native"
"msdmo"="native"
"qcap"="native"
"quartz"="native"
"streamci"="native"

接着下载并安装DirectX9.0, 地址为http://filehippo.com/download_directx/,下载运行后指定解压路径,再运行解压出来的DXSETUP.exe

原文说可能需要装两次。
另外原文说为了Direct Music要安装gm.dls,位于windows/system32/drivers,不过我没有成功.

之后可以用dxdiag.exe看看效果, 还不错。

2007-12-15

Wolfenstein: Enemy Territory 在linux下不发声的解决

按网上的说法,原因是ET只支持oss,对ALSA支持得不够好。

解决办法为

echo "et.x86 0 0 direct" > /proc/asound/card0/pcm0p/oss (需要管理员权限)

gnome的彩蛋

按alt+f2调出Run Application对话框, 然后输入
free the fish

gegls from outer space

前者会出现一个小鱼,不过要想让它消失只能kill掉gnome-panel
后者出现一个鱼打牛的小游戏

2007-12-10

pidgin 2.3.1 支持QQ了

最近看到Pidgin升级到2.3.1了,Changelog里赫然写着改进了对QQ的支持, 于是我下载编译试了一下。(注意首先要卸了原来的)

开始时我的QQ号需要激活,还需要输入验证码,这些都是用Wine的QQ发现的。 前者上个网页就行了,后者则需要更改密码。 都还不算麻烦。

之后用pidgin连了一下,果然连上了。 只是现在不支持好友分组, 也不支持表情。

但是已经很高兴了。

2007-12-04

ubuntu vim-latexsuite默认变量的问题

ubuntu里装了vim-latexsuite后问题多多,除了上次说的找不到插件的问题外,还有一个问题,就是默认变量没有被设置,每次按\ll编译时都会提示没有设置initTarget. 以前就没有遇到这个问题。

今天研究了一下,原因是vim自己的目录下有一个关于tex文件compile的设置,优先级比latexsuite的高,所以latexsuite的那个就不能被运行了。

解决办法是把/usr/share/vim/vim71/compiler/tex.vim改名或删除,之后就正常了 :)

2007-12-01

游戏推荐: Anika's Odyssey

见http://www.kongregate.com/games/Trickysheep/anikas-odyssey
point and click类游戏,难度不大,但是美工非常棒,情节也比较有趣。是我喜欢的类型。

游戏推荐: excit

见http://www.kongregate.com/games/Krystman/excit
puzzle类游戏,做得不错,一共30关,最后一关比较难,前面还好
我一口气玩完的,最后排名在558。

2007-11-30

SharedObject 修改器

最近破解一个Flash时需要修改它的SharedObject, 找到一些修改器,其中最好用的一个叫做.sol
Editor, 主页是http://sourceforge.net/projects/soleditor/
只可惜只有Windows版本,但是在wine下跑得也不错。 另外就是不开源,也有点不爽。
但是它是我找到的几个修改器里最好用的一个。

2007-11-25

beep media player mp3 信息乱码的解决

右键 -> Preferences -> Plugins -> Media -> MPEG Audio Plugin -> Preferences -> Title

选上Covert non-UTF8 ID3 tags to UTF8
并把ID3 encoding设为GBK

2007-11-24

linux 连接 蓝牙 手机

参考:http://blog.flyingdream.net.cn/2007/%E5%9C%A8-ubuntu-linux-%E4%B8%8B%E5%88%A9%E7%94%A8%E8%93%9D%E7%89%99%E9%80%9A%E8%BF%87-gprs-%E6%97%A0%E7%BA%BF%E4%B8%8A%E7%BD%91/
http://www.chinalinuxpub.com/read.php?wid=1490

测试环境, ubuntu 7.10 + motorola A768i

网上搜这个能搜到很多, 大致是装上很多以bluez-开头和包含bluetooth的包(我差不多都装了), 之后启动bluetooh服务,以及运行gnome-obex-server(Applications->Accessories->Bluetooth File Sharing). 然后就能自动找到手机了(可能也需要修改hcid.conf,见下), 传文件是点右键, Send to, 里面有个Bluetooh的选项。

今天一直在搞rfcomm, 本来是试图连上蓝牙局域网的,但是一直没成功,到现在有点成果。记录如下。

注意:
要用到的命令:hcitool, hciconfig, sdptool, rfcomm, 最好都看一下文档。
另外每次修改配之后注意重启bluetooth服务:/etc/init.d/bluetooth restart
很多命令需要管理员权限,以下省略sudo

步骤:
1.修改/etc/bluetooth/hcid.conf, 关键是如下两行
security auto;
passkey "1234";
第一行很重要, 我一开始设的user,手机怎么都连不上电脑
第二行是手机连电脑时需要输入的验证码

2.hcitool scan 得到手机的地址

3.sdptool browse <地址> 得到手机服务列表
我这里这条命令结果为空,
我用的是spdtool search --bdaddr <地址>
常用的service有
DUN 拨号
LAN
HS headset
OPUSH obex push

得到列表后注意看Protocal Descriptor List里“RFCOMM”一行及下一行,
找到Channel: x, 记住这个x

4.编辑 /etc/bluetooth/rfcomm.conf,
rfcomm0 {
bind yes;
device <地址>;
channel ;
comment “your comment here”;
}

之后/dev/rfcomm0就是这个channel的连接了
如果rfcomm0已经使用,那么直接改rfcomm.conf里的设备名

引文里提到用kermit可以通过/dev/rfcomm0实现与手机通信, 我这里没成功
另一引文里提到可以用/dev/rfcomm0通过手机上网, 我没有试

现在我这里主要就是能够手机电脑互传文件, 互相bond而已,很多功能还有待研究

2007-11-19

ubuntu 7.10 下安装 windriver

最近要用Xilinx ISE, 装驱动时老装不上, 去Windriver官网上下的也没用。 configure和make都报错, 自己尝试修改未果。

几经查找, 在http://www.visionlab.uncc.edu/component/option,com_jd-wiki/Itemid,46/上找到解决办法:

首先去ftp://ftp.xilinx.com/pub/utilities/fpga/install_drivers.tar.gz下载并解压最新的驱动, 我下的跟上面那个网站说的一样, md5是d0aea515f1b4523eb2537df86bc6db41

创建一个diff文件

--- install_drivers/linux_drivers/windriver32/windrvr/linux_wrappers.c.orig 2007-03-27 22:35:36.000000000 +0200
+++ install_drivers/linux_drivers/windriver32/windrvr/linux_wrappers.c 2007-09-24 00:32:44.000000000 +0200
@@ -44,9 +44,6 @@
#endif
#include <asm/mman.h>

#include "linux_wrappers.h"
-#if defined(LINUX_26)
- #include <linux/ioctl32.h>

-#endif
#if defined(UDEV_SUPPORT)
#include <linux/devfs_fs_kernel.h>
#endif
@@ -91,7 +88,10 @@
#endif

#if defined(MODULE_LICENSE)
- MODULE_LICENSE("Proprietary");
+ // MODULE_LICENSE("Proprietary");
+ // Uh! Uh! We need to set this to GPL so we can talk to the USB API.
+ // Is it a copyright infringement if I do that for private use??
+ MODULE_LICENSE("GPL");
#endif
#if defined(MODULE_AUTHOR)
MODULE_AUTHOR("Jungo");
@@ -587,7 +587,7 @@
if (!bh)
return NULL;
memset(bh, 0, sizeof(*bh));
- bh->data = data;
+ atomic_long_set(&bh->data, data);
#if defined(LINUX_26)
bh->func = routine;
bh->entry.next = &bh->entry;
@@ -650,7 +650,7 @@
#else
void
#endif
-wrapper_handler(int irq, void *ctx, struct pt_regs *pt)
+wrapper_handler(int irq, void *ctx)
{
struct int_wrapper *context = (struct int_wrapper *)ctx;
int rc = 0;
@@ -1023,7 +1023,7 @@
}

#if defined(WINDRIVER_KERNEL)
- pci_module_init(&generic_pci_driver);
+ pci_register_driver(&generic_pci_driver);
#endif
return 0;
#endif
--- install_drivers/linux_drivers/xpc4drvr2_6/xpc4drvr/Makefile.orig 2007-09-24 00:36:20.000000000 +0200
+++ install_drivers/linux_drivers/xpc4drvr2_6/xpc4drvr/Makefile 2007-09-24 00:36:27.000000000 +0200
@@ -17,7 +17,7 @@
# Run ./configure first, to generate regparm_option
REG_PARM = $(shell grep -c "regparm=3" $(PWD)/regparm_option)
SYM_FILE_DIR = /lib/modules/$(shell uname -r)/build
-GET_USER_SIZE_SYM = $(shell grep -c "get_user_size" $(SYM_FILE_DIR)/Module.symvers)
+GET_USER_SIZE_SYM = 0
ARCH_64BIT = $(shell arch | grep -c "64")
ifeq ($(ARCH_64BIT),1)
CFLAGS += -mcmodel=kernel

保存在源码顶层目录,用patch -p1 < filename打补丁

最后是安装,原文说的是直接运行./install_drivers即可,我这里不行,要进入linux_drivers/windriver32/windrvr
然后运行bash ./configure 和sudo make && sudo make install

那个configure挺诡异的。。。

不过最后终于编译成功了

2007-11-13

Bonjour.exe 清除办法

刚装了Adobe CS3, 防火墙突然给我蹦出一堆Bonjour.exe的信息,很是烦人。
查了一下,不算病毒,是Adobe的某软件, 删之。

具体方法是:

运行“C:\Program Files\Bonjourm\DNSResponder.exe -remove”
打开 C:Program Files\Bonjour ,重命名 mdnsNSP.dll 为 mdnsNSP.old
重启电脑
删除 Program Files\Bonjour 文件夹

2007-11-03

linux 下 nvidia 显卡双屏幕设置

这个被nvidia称为TwinView, 其设置方法是在Section "Screen"里加入
Option "TwinView" "True"
之后是Option "MetaModes" "nvidia-auto-select, nvidia-auto-select" (这个也可以用nvidia-settings设置)

这样就启动了TwinView, 不过这时两个屏幕是拼接的关系。 我这次是要接投影仪, 因此需要两个屏幕显示一致,因此还要加上
Option "TwinVieworientation" "Clone"

之后重启X即可。

nvidia驱动的文档写得很清楚,我看了2分钟就搞明白了。

2007-10-30

ubuntu 中文默认字体的设置

我一直希望能够分别设置中文和英文的字体, 但是一直都没成功。

之前一直是只设置喜欢的英文字体作为默认字体, 但是中文的话就非常难看,现象是很多文字字体,大小都不一致。

仔细想想,它(我一直用gnome)字体显示的工作原理大致是有个字体列表,然后要显示一个字的时候就依次查找,直到找到一个能够显示的为止。 比如如果我设的默认字体是个中文字体,那么英文字体一般就会跟着改变。但是现在默认字体是英文的,显示中文时自然找不到,于是它就从自己的默认列表里去找到了。

至于列表具体存放的位置, 我找了找,是在/etc/fonts/conf.d里,相关的有两个文件, 40-generic.conf 和 65-nonlatin.conf, 根据/etc/fonts/conf.avail里的说明,这些配置文件是按编号顺序依次加载的, 于是修改65-nonlatin.conf, 由于我想使用文鼎楷体,于是找到对应的, 并把它移到所在里的最前面,成为第一个

之后重启X时没进去,卡住了,然后重启了一下问题解决。

现在爽眼多了。

2007-10-29

linux下麦克风不工作

我这里麦克风的症状如之前声卡output的一样, 不报错但是不工作(偶尔好像也报错)。
弄了好几次不见起色。
今天偶然又看看了alsa网站里的详细安装说明,发现我这里设备文件的权限没设对。
于是运行chmod a+rw /dev/dsp /dev/mixer /dev/sequencer,之后问题解决(我这里没有midi)
不过似乎噪声很大,尚待解决。但是好歹可以用了。

2007-10-25

创建 ssh 信任连接

参考: http://www.powersite.cn/?q=node/62&p=9334c90a88934eac5df7c7710c149d&user=baidu

主要目的是让服务器给客户端建立信任关系,使得以后连接时不需要输入密码

具体步骤是

1.在客户端建立key, 如果没有创建过, 用ssh-keygen -t rsa创建一个
2.把生成的id文件(一般是~/.ssh/id_rsa)的内容追加到服务器的~/.ssh/authorized_keys文件中

就是这么简单。

evince 看中文pdf乱码的解决

有时候用evince看中文pdf会出乱码,大概是Adobe-GB1这个字体的问题, 装了xpdf-chinese*和cmap-adobe*之后,xpdf可以正常工作了, 但是evince仍不行, 控制台消息会报跟adobe-gb1有关的错误。

源里没有poppler相关的包, 我又搜了搜,终于发现了应该安装poppler-data,可以从http://poppler.freedesktop.org/下载到。那个由于是版权归Adobe,所以不能放源里。唉。

不过装了之后确实正常了,如果是ubuntu,注意用sudo make install prefix=/usr来安装。

2007-10-22

ubuntu 7.10 rainlendar 崩溃的解决

日历软件我一直用rainlendar+GCALDaemon, 然而ubuntu升级至7.10后,rainlendar就出了问题, 虽然界面可以显示,但是点击菜单后就会崩溃。起初认为是和以前一样与scim冲突, 但是尝试修复未果。

今天搜到了http://www.rainlendar.net/cms/index.php?option=com_fireboard&Itemid=36&func=view&catid=4&id=4316

根据作者所说, 这个是和gtk和wxwidget有关。 之后我现在了2.2.b48版, 基本工作正常

链接在http://www.rainlendar.net/cms/index.php?option=com_fireboard&Itemid=36&func=view&id=4540&catid=7

2007-10-18

[转]AT&T汇编语言与GCC内嵌汇编简介

AT&T汇编语言与GCC内嵌汇编简介

版本 0.1
时间04/3/30
EMAIL chforest_chang@hotmail.com


1 AT&T 与INTEL的汇编语言语法的区别
1.1大小写
1.2操作数赋值方向
1.3前缀
1.4间接寻址语法
1.5后缀
1.6指令

2 GCC内嵌汇编
2.1简介
2.2内嵌汇编举例
2.3语法
2.3.1汇编语句模板
2.3.2输出部分
2.3.3输入部分
2.3.4限制字符
2.3.5破坏描述部分
2.4GCC如何编译内嵌汇编代码

3后记

本节先介绍
AT&T汇编语言语法与INTEL汇编语法的差别,然后介绍GCC内嵌汇编语法。阅读本节需要读者具有INTEL
汇编语言基础。



1 AT&T 与INTEL的汇编语言语法的区别


1.1
指令大小写
INTEL格式的指令使用大写字母,而AT&T
格式的使用小写字母。
例:
INTEL AT&T
MOV EAX,EBX movl %ebx,%eax
1.2
指令操作数赋值方向

在INTEL语法中,第一个表示目的操作数,第二个表示源操作数,赋值方向从右向左。
AT&T语法第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然。


例:
INTEL AT&T
MOV EAX,EBX movl %ebx,%eax
1.3
指令前缀
在INTEL语法中寄存器和立即数不需要前缀;
AT&T中寄存器需要加前缀“%”;立即数需要加前缀“$”。


例:
INTEL AT&T
MOV EAX,1 movl $1,%eax

符号常数直接引用,不需要加前缀,如:
movl value , %ebx
value为一常数;
在符号前加前缀 $, 表示引用符号地址,

movl $value, %ebx
是将value的地址放到ebx中。

总线锁定前缀“lock”:
总线锁定操作。“lock”前缀在Linux
核心代码中使用很多,特别是SMP
代码中。当总线锁定后其它CPU
不能存取锁定地址处的内存单元。

远程跳转指令和子过程调用指令的操作码使用前缀“l“,分别为ljmp,lcall,
与之相应的返回指令伪lret。
例:

INTEL AT&T

lcall $secion:$offset
JMP FAR SECTION:OFFSET ljmp $secion:$offset
RET FAR SATCK_ADJUST lret $stack_adjust

1.4 间接寻址语法

INTEL中基地址使用“[”、“]”,而在AT&T“(”、“)”;
另外处理复杂操作数的语法也不同,
INTEL为Segreg:[base+index*scale+disp]
,而在AT&T中为%segreg:disp(base,index,sale),其中segreg
,index,scale,disp都是可选的,在指定index而没有显式指定Scale
的情况下使用默认值1。Scale,disp不需要加前缀“&”。

INTEL AT&T
Instr foo,segreg:[base+index*scale+disp] instr %segreg:disp(base,index,scale),foo

1.5
指令后缀

AT&T
语法中大部分指令操作码的最后一个字母表示操作数大小,“b”表示byte
(一个字节);“w”表示word(2,个字节);“l”表示long(4,个字节)。
INTEL中处理内存操作数时也有类似的语法如:
BYTE PTR、WORD PTR、DWORD PTR。

例:
INTEL AT&T
mov al, bl movb %bl,%al
mov ax,bx movw %bx,%ax
mov eax, dword ptr [ebx] movl (%ebx), %eax


AT&T汇编指令中,操作数扩展指令有两个后缀,一个指定源操作数的字长,另一个指定目标操作数的字长。AT&T的符号扩展指令的为“movs”,零扩展指令为“movz
”(相应的Intel指令为“movsx”和“movzx”)。因此,“movsbl %al,%edx”表示对寄存器al
中的字节数据进行字节到长字的符号扩展,计算结果存放在寄存器edx
中。下面是一些允许的操作数扩展后缀:

l
bl: ,字节>->长字 l
bw: ,字节>->字 l
wl: ,字->长字

跳转指令标号后的后缀表示跳转方向,“f”表示向前(forward),
“b,”表示向后(back)。
例:

jmp 1f
jmp 1f

1.6 指令
INTEL汇编与AT&T汇编指令基本相同,差别仅在语法上。关于每条指令的语法可以参考I386Manual。



2 GCC内嵌汇编

2.1 简介

内核代码绝大部分使用C
语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。GCC提供了内嵌汇编的功能,可以在C代码中直接内嵌汇编语言语句,大大方便了程序设计。

简单的内嵌汇编很容易理解


例:

__asm__
__volatile__("hlt");

“__asm__”表示后面的代码为内嵌汇编,“asm”是“__asm__”的别名。
“__volatile__”表示编译器不要优化代码,后面的指令保留原样,
“volatile”是它的别名。括号里面是汇编指令。

2.2 内嵌汇编举例在内嵌汇编中,可以将C
语言表达式指定为汇编指令的操作数,而且不用去管如何将C
语言表达式的值读入哪个寄存器,以及如何将计算结果写回C
变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可, GCC
会自动插入代码完成必要的操作。




使用内嵌汇编,要先编写汇编指令模板,然后将C语言表达式与指令的操作数相关联,并告诉
GCC对这些操作有哪些限制条件。例如在下面的汇编语句:

__asm__ __violate__
("movl %1,%0" : "=r" (result) : "m" (input));







“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将C
语言表达式与指令操作数相对应。指令模板后面用小括号括起来的是C
语言表达式,本例中只有两个:“result”和“input”,他们按照出现的顺序分别与指令操作
数“%0”,“%1,”对应;注意对应顺序:第一个C表达式对应“%0”;第二个表达式对应“%1
”,依次类推,操作数至多有10个,分别用“%0”,“%1”….“%9,”表示。在每个操作数前
面有一个用引号括起来的字符串,字符串的内容是对该操作数的限制或者说要求。“result”前面
的限制字符串是“=r”,其中“=”表示“result”是输出操作数,“r
”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后
在指令中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值
存入变量“result”,从表面上看好像是指令直接对“result”进行操作,实际上GCC
做了隐式处理,这样我们可以少写一些指令。“input”前面的“r”表示该表达式需要先放入
某个寄存器,然后在指令中使用该寄存器参加运算。




我们将上面的内嵌代码放到一个C源文件中,然后使用gcc –c–S得到该C
文件源代码相对应的汇编代码,然后查看一下汇编代码,看看GCC是如何处理的。



C源文件如下内容如下,注意该代码没有实际意义,仅仅作为例子。


extern int
input,result;

void test(void)
{
input
= 1;
__asm__ __volatile__ ("movl %1,%0" :
"=r" (result) : "r" (input));
return
;
}







对应的汇编代码如下;

行号 代码 解释

1
7
8 movl $1, input 对应C语言语句input = 1;
9 input, %eax
10 #APP GCC插入的注释,表示内嵌汇编开始
11 movl %eax,%eax 我们的内嵌汇编语句
12 #NO_APP GCC 插入的注释,表示内嵌汇编结束
13 movl %eax, result 将结果存入result变量
14

18
。。。。。。

从汇编代码可以看出,第9行和第13行是GCC,自动增加的代码,GCC
根据限定字符串决定如何处理C表达式,本例两个表达式都被指定为“r”型,所以先使用指令:
movl input, %eax

将input读入寄存器%eax;GCC,也指定一个寄存器与输出变量result
相关,本例也是%eax,等得到操作结果后再使用指令:

movl %eax, result


将寄存器的值写回C变量result中。从上面的汇编代码我们可以看出与result
和input,相关连的寄存器都是%eax,GCC使用%eax,替换内嵌汇编指令模板中的
%0,%1

movl %eax,%eax
显然这一句可以不要。但是没有优化,所以这一句没有被去掉。

由此可见,C表达式或者变量与寄存器的关系由GCC自动处理,我们只需使用限制字符串指导GCC
如何处理即可。限制字符必须与指令对操作数的要求相匹配,否则产生的汇编代码
将会有错,读者可以将上例中的两个“r”,都改为“m”(m,表示操作数放在内存,而不是寄
存器中),编译后得到的结果是:

movl input, result

很明显这是一条非法指令,因此限制字符串必须与指令对操作数的要求匹配。例如指令movl
允许寄存器到寄存器,立即数到寄存器等,但是不允许内存到内存的操作,因此两个操作数
不能同时使用“m”作为限定字符。

2.3 语法


内嵌汇编语法如下:

__asm__(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分)


共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格
开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,
也需要用“:”格开,相应部分内容为空。例如:

__asm__ __volatile__(
"cli":
:
:"memory")

2.3.1 汇编语句模板


汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开。
指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1…,%9。
指令中使用占位符表示的操作数,总被视为long型(4,个字节),但对其施加的操作
根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。
对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,
“b”代表低字节,“h”代表高字节,例如:%h1。

2.3.2 输出部分

输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和
C语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。


例:
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )

描述符字符串表示对该变量的限制条件,这样GCC就可以根据这些条件决定如何
分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。

2.3.3 输入部分

输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由
限定字符串和C语言表达式或者C语言变量组成。


例1:
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));


例二(bitops.h):

Static __inline__ void __set_bit(int nr,
volatile void * addr)
{
__asm__(
"btsl%1,%0" :
"=m"(ADDR) :
"Ir"(nr));
}

后例功能是将(*addr)的第nr位设为1。第一个占位符%0与C,语言变量ADDR
对应,第二个占位符%1与C,语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价:
btsl nr, ADDR,该指令的两个操作数不能全是内存变量,因此将nr的限定字符串指定为“Ir”,
将nr,与立即数或者寄存器相关联,这样两个操作数中只有ADDR为内存变量。

2.3.4 限制字符
2.3.4.1 限制字符列表
限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和i386
中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的C
语言变量与指令操作数之间的关系,例如是将变量放在寄存器中还是放在内存中等,
下表列出了常用的限定字母。

分类
限定符 描述 通用寄存器


“a”将输入变量放入eax

这里有一个问题:假设eax已经被使用,那怎么办?

其实很简单:因为GCC知道eax已经被使用,它在这段汇编代码的起始处插入一条
语句pushl %eax,将eax内容保存到堆栈,然后在这段代码结束处再增加一条
语句popl %eax,恢复eax的内容

“b”将输入变量放入ebx
“c”将输入变量放入ecx
“d”将输入变量放入edx
“s”将输入变量放入esi
“d”将输入变量放入edi
“q”将输入变量放入eax,ebx ,ecx ,edx中的一个
“r”将输入变量放入通用寄存器,也就是eax ,ebx,ecx,edx,esi,edi中的一个
“A”把eax和edx,合成一个64位的寄存器(uselong longs)
“m”内存变量
“o”操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
“V”操作数为内存变量,但寻址方式不是偏移量类型
“,” 操作数为内存变量,但寻址方式为自动增量
“p”操作数是一个合法的内存地址(指针)


寄存器或内存

“g” 将输入变量放入eax,ebx,ecx ,edx中的一个或者作为内存变量
“X”操作数可以是任何类型


立即数
“I” 0-31 之间的立即数(用于32位移位指令)
“J” 0-63 之间的立即数(用于64 位移位指令)
“N” 0-255 ,之间的立即数(用于out 指令)
“i” 立即数
“n” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”


匹配

“0”,“1 ,”... “9 ”

表示用它限制的操作数与某个指定的操作数匹配,也即该操作数就是指定的那个操作数,
例如用“0 ”去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为
限定符字母的0-9 ,与指令中的“%0”-“%9”的区别,前者描述操作数,后者代表操作数。


后面有详细描述 & 该输出操作数不能使用过和输入操作数相同的寄存器


后面有详细描述


操作数类型
“=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)

浮点数
“f”

浮点寄存器
“t”第一个浮点寄存器
“u”第二个浮点寄存器
“G”标准的80387

浮点常数
% 该操作数可以和下一个操作数交换位置


例如addl的两个操作数可以交换顺序(当然两个操作数都不能是立即数)


# 部分注释,从该字符到其后的逗号之间所有字母被忽略

* 表示如果选用寄存器,则其后的字母被忽略


现在继续看上面的例子,
"=m" (ADDR)表示ADDR为内存变量(“m”),而且是输出变量(“=”);"Ir" (nr)表示nr,为
0-31之间的立即数(“I”)或者一个寄存器操作数(“r”)。



2.3.4.2
匹配限制符

I386
指令集中许多指令的操作数是读写型的(读写型操作数指先读取原来的值然后参加运算,最后
将结果写回操作数),例如addl %1,%0,它的作用是将操作数%0与操作数%1的和存入操作数%0,
因此操作数%0是读写型操作数。老版本的GCC对这种类型操作数的支持不是很好,它将操作数严格
分为输入和输出两种,分别放在输入部分和输出部分,而没有一个单独部分描述读写型操作数,
因此在GCC中读写型的操作数需要在输入和输出部分分别描述,靠匹配限制符将两者关联到一起
注意仅在输入和输出部分使用相同的C变量,但是不用匹配限制符,产生的代码很可能不对,后
面会分析原因。




匹配限制符是一位数字:“0”、“1”……“9,”,分别表示它限制的C表达式分别与
占位符%0,%1,……%9对应的C变量匹配。例如使用“0”作为%1,的限制字符,那么
%0和%1表示同一个C,变量。


看一下下面的代码就知道为什么要将读写型操作数,分别在输入和输出部分加以描述。


该例功能是求input+result的和,然后存入result:


extern int input,result;

void test_at_t()
{
result= 0;
input = 1;
__asm__
__volatile__ ("addl %1,%0":"=r"(result): "r"(input));

}

对应的汇编代码为:

movl $0,_result
movl $1,_input
movl _input,%edx /APP
addl %edx,%eax /NO_APP
movl %eax,%edx
movl %edx,_result

input 为输入型变量,而且需要放在寄存器中,GCC给它分配的寄存器是%edx,在执行addl之前%edx,
的内容已经是input的值。可见对于使用“r”限制的输入型变量或者表达式,在使用之前GCC会插入
必要的代码将他们的值读到寄存器;“m”型变量则不需要这一步。读入input后执行addl,显然%eax
的值不对,需要先读入result的值才行。再往后看:movl %eax,%edx和movl %edx,_result
的作用是将结果存回result,分配给result的寄存器与分配给input的一样,都是%edx。

综上可以总结出如下几点:

1. 使用“r”限制的输入变量,GCC先分配一个寄存器,然后将值读入寄存器,最后
用该寄存器替换占位符;


2. 使用“r”限制的输出变量,GCC会分配一个寄存器,然后用该寄存器替换占位符,
但是在使用该寄存器之前并不将变量值先读入寄存器,GCC认为所有输出变量以前的
值都没有用处,不读入寄存器(可能是因为AT&T汇编源于CISC架构处理器的汇编语言
,在CISC处理器中大部分指令的输入输出明显分开,而不像RISC那样一个操作数既
做输入又做输出,例如add r0,r1,r2,r0,和r1是输入,r2是输出,输入和输出分开,
没有使用输入输出型操作数,这样我们就可以认为r2对应的操作数原来的值没有用处,
也就没有必要先将操作数的值读入r2,因为这是浪费处理器的CPU周期),最后GCC插入代码,
将寄存器的值写回变量;


3. 输入变量使用的寄存器在最后一处使用它的指令之后,就可以挪做其他用处,因为
已经不再使用。例如上例中的%edx。在执行完addl之后就作为与result对应的寄存器。


因为第二条,上面的内嵌汇编指令不能奏效,因此需要在执行addl之前把result的值读入
寄存器,也许再将result放入输入部分就可以了(因为第一条会保证将result
先读入寄存器)。修改后的指令如下(为了更容易说明问题将input限制符由“r,”改为“m”):


extern int input,result;

void test_at_t()
{

result = 0;
input = 1;
__asm__
__volatile__ ("addl %2,%0":"=r"(result):"r"(result),"m"(input));

}


看上去上面的代码可以正常工作,因为我们知道%0和%1都和result相关,应该使用同一个
寄存器,但是GCC并不去判断%0和%1,是否和同一个C表达式或变量相关联(这样易于产生与
内嵌汇编相应的汇编代码),因此%0和%1使用的寄存器可能不同。我们看一下汇编代码就知道了。

movl $0,_result
movl $1,_input
movl _result,%edx /APP
addl _input,%eax /NO_APP
movl %eax,%edx
movl %edx,_result

现在在执行addl之前将result的值被读入了寄存器%edx,但是addl指令的操作数%0
却成了%eax,而不是%edx,与预料的不同,这是因为GCC给输出和输入部分的变量分配了不同
的寄存器,GCC没有去判断两者是否都与result相关,后面会讲GCC如何翻译内嵌汇编,看完之后
就不会惊奇啦。


使用匹配限制符后,GCC知道应将对应的操作数放在同一个位置(同一个寄存器或者同一个
内存变量)。使用匹配限制字符的代码如下:


extern int input,result;

void test_at_t()
{
result = 0;
input = 1;
__asm__
__volatile__ ("addl %2,%0":"=r"(result):"0"(result),"m"(input));

}

输入部分中的result用匹配限制符“0”限制,表示%1与%0,代表同一个变量,
输入部分说明该变量的输入功能,输出部分说明该变量的输出功能,两者结合表示result
是读写型。因为%0和%1,表示同一个C变量,所以放在相同的位置,无论是寄存器还是内存。

相应的汇编代码为:


movl $0,_result
movl $1,_input
movl _result,%edx
movl %edx,%eax /APP
addl _input,%eax /NO_APP
movl %eax,%edx
movl %edx,_result


可以看到与result相关的寄存器是%edx,在执行指令addl之前先从%edx将result读入%eax,
执行之后需要将结果从%eax读入%edx,最后存入result中。这里我们可以看出GCC
处理内嵌汇编中输出操作数的一点点信息:addl并没有使用%edx,可见它不是简单的用result
对应的寄存器%edx去替换%0,而是先分配一个寄存器,执行运算,最后才将运算结果存入
对应的变量,因此GCC是先看该占位符对应的变量的限制符,发现是一个输出型寄存器变量,
就为它分配一个寄存器,此时没有去管对应的C变量,最后GCC,知道还要将寄存器的值写回变量,
与此同时,它发现该变量与%edx关联,因此先存入%edx,再存入变量。


至此读者应该明白了匹配限制符的意义和用法。在新版本的GCC中增加了一个限制字符“+”,
它表示操作数是读写型的,GCC知道应将变量值先读入寄存器,然后计算,最后写回变量,而
无需在输入部分再去描述该变量。


例;
extern int input,result;

void test_at_t()
{

result = 0;
input = 1;
__asm__
__volatile__ ("addl %1,%0":"+r"(result):"m"(input));

}


此处用“+”替换了“=”,而且去掉了输入部分关于result的描述,产生的汇编代码如下:
movl $0,_result
movl $1,_input
movl _result,%eax /APP
addl _input,%eax /NO_APP
movl %eax,_result
L2:
movl %ebp,%esp

处理的比使用匹配限制符的情况还要好,省去了好几条汇编代码。


2.3.4.3 “&”限制符

限制符“&”在内核中使用的比较多,它表示输入和输出操作数不能使用相同的寄存器,
这样可以避免很多错误。

举一个例子,下面代码的作用是将函数foo的返回值存入变量ret中:


__asm__ ( “call foo;movl %%edx,%1”, :”=a”(ret) : ”r”(bar) );


我们知道函数的int型返回值存放在%eax中,但是gcc编译的结果是输入和输出同时使用了
寄存器%eax,如下:

movl bar, %eax
#APP
call foo
movl %ebx,%eax

#NO_APP
movl %eax, ret

结果显然不对,原因是GCC并不知道%eax中的值是我们所要的。避免这种情况的方法是使用“&”
限定符,这样bar就不会再使用%eax寄存器,因为已被ret指定使用。

_asm__ ( “call foo;movl %%edx,%1”,:”=&a”(ret) : ”r”(bar) );


2.3.5 破坏描述部分

2.3.5.1 寄存器破坏描述符


通常编写程序只使用一种语言:高级语言或者汇编语言。高级语言编译的步骤大致如下:
l
预处理;
l
编译
l
汇编
l
链接


我们这里只关心第二步编译(将C代码转换成汇编代码):因为所有的代码都是用高级语言编写,
编译器可以识别各种语句的作用,在转换的过程中所有的寄存器都由编译器决定如何分配使用,
它有能力保证寄存器的使用不会冲突;也可以利用寄存器作为变量的缓冲区,因为寄存器的访问
速度比内存快很多倍。如果全部使用汇编语言则由程序员去控制寄存器的使用,只能靠程序员去
保证寄存器使用的正确性。但是如果两种语言混用情况就变复杂了,因为内嵌的汇编代码可以直接
使用寄存器,而编译器在转换的时候并不去检查内嵌的汇编代码使用了哪些寄存器(因为很难检测
汇编指令使用了哪些寄存器,例如有些指令隐式修改寄存器,有时内嵌的汇编代码会调用其他子过程,
而子过程也会修改寄存器),因此需要一种机制通知编译器我们使用了哪些寄存器(程序员自己知道
内嵌汇编代码中使用了哪些寄存器),否则对这些寄存器的使用就有可能导致错误,修改描述部分
可以起到这种作用。当然内嵌汇编的输入输出部分指明的寄存器或者指定为“r”,“g”型由编译器
去分配的寄存器就不需要在破坏描述部分去描述,因为编译器已经知道了。


破坏描述符由逗号格开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外
还有“memory”。例如:“%eax”,“%ebx”,“memory”等。


下面看个例子就很清楚为什么需要通知GCC内嵌汇编代码中隐式(称它为隐式是因为GCC并不知道)
使用的寄存器。

在内嵌的汇编指令中可能会直接引用某些寄存器,我们已经知道AT&T格式的汇编语言中,寄存器
名以“%”作为前缀,为了在生成的汇编程序中保留这个“%”号,在asm语句中对寄存器的
引用必须用“%%”作为寄存器名称的前缀。原因是“%”在asm,内嵌汇编语句中的作用与“\”在C
语言中的作用相同,因此“%%”转换后代表“%”。

例(没有使用修改描述符):

int main(void)
{
int input, output,temp;
input = 1;

__asm__ __volatile__ ("movl $0, %%eax;\n\t
movl %%eax, %1;\n\t
movl %2, %%eax;\n\t
movl %%eax, %0;\n\t"
:"=m"(output),"=m"(temp) /* output */
:"r"(input) /* input */
);
return 0;
}


这段代码使用%eax作为临时寄存器,功能相当于C代码:“temp = 0;output=input”,
对应的汇编代码如下:

movl $1,-4(%ebp)
movl -4(%ebp),%eax /APP
movl $0, %eax;
movl %eax, -12(%ebp);
movl %eax, %eax;
movl %eax, -8(%ebp); /NO_APP

显然GCC给input分配的寄存器也是%eax,发生了冲突,output的值始终为0,而不是input。

使用破坏描述后的代码:


int main(void)
{
int input, output,temp;

input = 1;

__asm__ __volatile__
( "movl $0, %%eax;\n\t
movl %%eax, %1;\n\t
movl %2, %%eax;\n\t
movl %%eax, %0;\n\t"
:"=m"(output),"=m"(temp) /* output */
:"r"(input) /* input */
:"eax"); /* 描述符 */

return 0;
}

对应的汇编代码:


movl $1,-4(%ebp)
movl -4(%ebp),%edx /APP
movl $0, %eax;
movl %eax, -12(%ebp);
movl %edx, %eax;
movl %eax, -8(%ebp); /NO_APP


通过破坏描述部分,GCC得知%eax已被使用,因此给input分配了%edx。在使用内嵌汇编时请记
住一点:尽量告诉GCC尽可能多的信息,以防出错。


如果你使用的指令会改变CPU的条件寄存器cc,需要在修改描述部分增加“cc”。

2.3.5.2 memory破坏描述符

“memory”比较特殊,可能是内嵌汇编中最难懂部分。为解释清楚它,先介绍一下编译器的
优化知识,再看C关键字volatile。最后去看该描述符。

2.3.5.2.1 编译器优化介绍

内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,
加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性
的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。
再看软件一级的优化:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器
优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的
是重新排序读写指令。


对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux提供了一个宏解决编译器的执行顺序问题。

void Barrier(void)

这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU
寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。

2.3.5.2.2 C 语言关键字volatile

C 语言关键字volatile(注意它是用来修饰变量而不是上面介绍的__volatile__)表明某个变量
的值可能在外部被改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新存取。
该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修
改,而程序通过该变量同步各个线程,例如:


DWORD __stdcall threadFunc(LPVOID signal)
{

int* intSignal=reinterpret_cast(signal);

*intSignal=2;

while(*intSignal!=1)
sleep(1000);

return 0;

}


该线程启动时将intSignal置为2,然后循环等待直到intSignal为1,时退出。显然intSignal
的值必须在外部被改变,否则该线程不会退出。但是实际运行的时候该线程却不会退出,即使
在外部将它的值改为1,看一下对应的伪汇编代码就明白了:


mov ax,signal
label:
if(ax!=1)
goto label


对于C编译器来说,它并不知道这个值会被其他线程修改。自然就把它cache在寄存器里面。记住,C
编译器是没有线程概念的!这时候就需要用到volatile。volatile的本意是指:这个值可能会在
当前线程外部被改变。也就是说,我们要在threadFunc中的intSignal前面加上volatile
关键字,这时候,编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取,所作
的循环变为如下面伪码所示:

label:
mov ax,signal
if(ax!=1)
goto label


2.3.5.2.3 Memory


有了上面的知识就不难理解Memory
修改描述符了,Memory描述符告知GCC:
(1)不要将该段内嵌汇编指令与前面的指令重新排序;也就是在执行内嵌汇编代码之前,
它前面的指令都执行完毕。


(2)不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会
以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,
如果后面又访问这些变量,需要重新访问内存。


如果汇编指令修改了内存,但是GCC本身却察觉不到,因为在输出部分没有描述,
此时就需要在修改描述部分增加“memory”,告诉GCC内存已经被修改,GCC得知这个信息后,
就会在这段指令之前,插入必要的指令将前面因为优化Cache到寄存器中的变量值先写回内存,
如果以后又要使用这些变量再重新读取。


例:
………..
Char test[100];
char a;
char c;

c = 0;
test[0] = 1;
……..
a = test [0];
……
__asm__(
"cld\n\t"
"rep\n\t"
"stosb"
: /* no output */

: "a" (c),"D" (test),"c" (100)
:
"cx","di","memory");
……….
// 我们知道test[0] 已经修改,所以重新读取
a=test[0];

……

这段代码中的汇编指令功能与
memset
相当,也就是相当于调用了memset(test,0,100);它使用stosb修改了test
数组的内容,但是没有在输入或输出部分去描述操作数,因为这两条指令都不需要
显式的指定操作数,因此需要增加“memory”通知GCC。现在假设:GCC在优化时将test[0]
放到了%eax寄存器,那么test[0] = 1对应于%eax=1,a = test [0]被换为a=%eax
,如果在那段汇编指令中不使用“memory”,Gcc,不知道现在test[0]
的值已经被改变了(如果整段代码都是我们自己使用汇编编写,我们自己当然知道
这些内存的修改情况,我们也可以人为的去优化,但是现在除了我们编写的那一小段外,
其他汇编代码都是GCC
生成的,它并没有那么智能,知道这段代码会修改test[0]),结果其后的a=test[0]
,转换为汇编后却是a=%eax,因为GCC不知道显式的改变了test数组,结果出错了。
如果增加了“memory”修饰符,GCC知道:
“这段代码修改了内存,但是也仅此而已,它并不知道到底修改了哪些变量”,
因此他将以前因优化而缓存到寄存器的变量值全部写回内存,从内嵌汇编开始,如果后面
的代码又要存取这些变量,则重新存取内存(不会将读写操作映射到以前缓存的那个寄存器)。
这样上面那段代码最后一句就不再是%eax=1,而是test[0] = 1。

这两条对实现临界区至关重要,第一条保证不会因为指令的重新排序将临界区内的代码调
到临界区之外(如果临界区内的指令被重排序放到临界区之外,What will happen?),
第二条保证在临界区访问的变量的值,肯定是最新的值,而不是缓存在
寄存器中的值,否则就会导致奇怪的错误。例如下面的代码:


int del_timer(struct timer_list * timer)
{
int
ret = 0;
if
(timer->next) {
unsigned
long flags;
struct
timer_list * next;
save_flags(flags);
cli();

// 临界区开始
if
((next = timer->next) != NULL) {
(next->prev = timer->prev)->next = next;
timer->next = timer->prev = NULL;
ret = 1;
} // 临界区结束

restore_flags(flags);
}
return
ret;
}

它先判断timer->next
的值,如果是空直接返回,无需进行下面的操作。如果不是空,则进入临界区进行操作,但是cli()
的实现(见下面)没有使用“memory”,timer->next的值可能会被缓存到寄存器中,
后面if ((next =timer->next) != NULL)会从寄存器中读取timer->next的值,如果
在if (timer->next)之后,进入临界区之前,timer->next的值可能被在外部改变,
这时肯定会出现异常情况,而且这种情况很难Debug。但是如果cli使用“memory”,
那么if ((next = timer->next) !=NULL)语句会重新从内存读取timer->next的值,而不会从寄存器
中取,这样就不会出现问题啦。

2.4 版内核中cli和sti的代码如下:
#define __cli()
__asm__
__volatile__("cli": : :"memory")
#define __sti()
__asm__
__volatile__("sti": : :"memory")

通过上面的例子,读者应该知道,为什么指令没有修改内存,但是却使用“memory
”修改描述符的原因了吧。应从指令的上下文去理解为什么要这样做。

使用“volatile”也可以达到这个目的,但是我们在每个变量前增加该关键字,
不如使用“memory”方便。

2.4 GCC如何编译内嵌汇编代码

GCC 编译内嵌汇编代码的步骤如下:

1.输入变量与占位符

根据限定符和破坏描述部分,为输入和输出部分的变量分配合适的寄存器,如果限定符指定为立即数
(“i”),或内存变量(“m”),则不需要该步骤,如果限定符没有具体指定输入操作数的
类型(如“g”),GCC会视需要决定是否将该操作数输入到某个寄存器。这样每个占位符都与某个
寄存器、内存变量或立即数形成了一一对应的关系。对分配了寄存器的输入变量需要增加代码
将它的值读入寄存器。另外还要根据破坏描述符的部分增加额外代码。


2.指令模板部分
然后根据这种一一对应的关系,用这些寄存器、内存变量或立即数来取代汇编代码中的占位符。

3.变量输出

按照输出限定符的指定将寄存器的内容输出到某个内存变量中,如果输出操作数的限定符指定为内存变量(“m”),则该步骤被省略。


3 后记


该文档参照了Web上的许多与GCC内嵌汇编相关的文章编写而成,在此表示感谢,
如有问题请发Email至:chforest_chang@hotmail.com 一起讨论。

2007-10-16

ubuntu 升级至7.10后framebuffer console故障排除

症状:没有修改任何配置,升级后framebuffer console突然不工作了,纯控制台变成黑屏,手工加载fbcon, vesafb, nvidiafb(我的显卡是nvidia)也不行。

折腾了好半天,参考了一些网页,比如https://bugs.launchpad.net/ubuntu/+source/initramfs-tools/+bug/129910

了解了大致原因,一个是 fbcon不能自动加载,另外是framebuffer的驱动被加入了blacklist

得解决方案:
1.修改/etc/initramfs-tools/modules, 添加fbcon, vesafb(每个模块占一行)
2.修改/etc/modprobe.d/blacklist-framebuffer, 把含vesafb, nvidiafb的两行注释掉
3.确认grub里的启动参数中设置好了vga

问题解决。

注:第一步里似乎不用加上nvidiafb, 但是另外两个似乎要加上

ubuntu 升级至7.10后网卡设备名自动改变的解决方法

症状是每次开机时会说invaid MAC address, 然后就会生成一个随机的MAC。
但是7.04时这个可以忽略。7.10里每次MAC一更改,网卡设备名就会更改,从eth0一直往上加。
查了一下,是udev这个东西搞的鬼。具体来说是/etc/udev/rules.d/75-persistent-net-generator.rules

我把一开始的GOTO="persistent_net_generator_do"该成了GOTO="persistent_net_generator_end",即直接跳过了生成新设备名的代码,问题得以解决。

2007-10-15

ubuntu 升级至7.10后 vim-latexsuite 不工作

https://bugs.launchpad.net/ubuntu/+source/vim-latexsuite/+bug/137205处找到了相关资料。

原因就是vim的runtimepath不包含/usr/share/vim/addons,但是vim-latexsuite默认是被装在那里的(不知道ubuntu7.04是如何配置的)。

解决办法,可以将/usr/share/vim/addons的内容拷至/usr/share/vim/vimfiles,也可以在.vimrc里加入set runtimepath+=/usr/share/vim/addons。

ubuntu 升级至7.10后无法登录

现象是输入密码的地方可以进入,但输入后X过一会儿就崩溃了,之后自动回到登录状态。

首先是要确定已装好显卡驱动,譬如我的nvidia显卡,更改内核后需要重新安装。

之后我把~/.gnome*和~/.config/autostart都改名后再进就正常了,然后试着该回来,又不行了。
后来折腾了几次,主要都是这几个文件夹,现在好了,发现新的gnome自启动好像不用~/.config/autostart了.

2007-10-12

Intel HD Audio 在 linux 下耳机插孔不工作的解决办法

症状是:使用hda-intel模块驱动声卡,扬声器工作正常,但插入耳机后耳机不出声,而扬声器仍发声。

参考了http://leufke.info/linux/asus/index.html, 可以尝试给snd-hda-intel模块加入position_fix=1的参数,一般来说是修改/etc/modprobe.d/options,加入options snd-hda-intel position_fix=1即可

游戏推荐: fish fillets

画面简单但关卡不简单的小游戏
关卡设计非常巧妙,到了后面有时我连看着攻略都过不去。
如果完全是自己研究出来的解法,过关后甚至有想哭的感觉:设计得太精妙了!
极力推荐!

现在版本叫fish fillets next generation.

2007-10-08

vaucanson-g: latex画自动机的宏包

最近作业要画自动机,找来找去发现这个vaucanson-g挺不错,只不过要注意根据它的文档所说,它只能生成 postscript指令,因此我这不能用pdflatex直接编译为pdf,而是要用pslatex编成dvi, 然后再依次转成ps,pdf才可以。

效果还不错,不过目前只用到英文,也没有插图,还没出现问题,不知道它对于中文和插图支持得如何。

2007-10-05

wine Windows Media Player 10

参考:
http://feeds.feedburner.com/~r/winereview/~3/161431304/windows-media-player-9-10-on-linux-with.html

主要步骤是:

1.安装一些dll, 一般可以从windows目录拷来或下载,然后用regsvr32注册以及winecfg中native的设置。包括如下dll:
quartz.dll
devenum.dll
jscript.dll
MSCAT32.dll

2.安装Microsoft XML Parser

3.安装解码器,我用的K-Lite Mega Codec Pack

4.安装Windows Media Player 10

一开始几次都没弄好,主要是可能忘了注册某几个dll, 一般来说几次重装就没问题了。

根据引用的帖子所说,好像网络方面不能用,别的都没问题。

wine qq

参考:http://linuxdesktop.cn/2007/04/13/step-by-step-install-qq2006-with-wine

我大概是按着这个帖子做的,但也不尽然。

主要应该还是弄来riched20.dll和riched32.dll, 可以从windows盘拷,也可以装一个。之后安装QQ,最后注意把TIMPlatform.exe改名基本就OK了。

原文说要把键盘加密那个sys也改名, 不过我这里没有找到。

还有就是一开始我用的是2007飘云版,但是虽然能正常登录及显示好友列表,而一进聊天窗口就卡住了。后来用的官方版本(也是2007)就好了。

最后就是我的wine没有打输入法补丁,因此用scim无法输入中文,不过问题不大。

关于在bochs的gdb-stub中加入对虚拟内存访问的支持

最近操统课需要用到bochs这个模拟器,我用的2.3.5. 但是实际出现了以个问题,就是gdb-stub这个功能不大正常,运行gdb后可以正常下断点,但是单步进可以,单步过却和单步进相同,这个问题不大比较恶心的是无法显示局部变量的值,总是显示0xffffffff
                        
google了半天没看到一点有用的。。。。。唉,只好自己搞
                        
参阅了gdb protocol后我修改了bochs的gdbstub.cc,让其显示出与gdb交互的内容,大致了解了内存这部分的步骤,主要是gdb给server(即bochs)发一个内存读取的请求,格式为m addr,length, 然后server返回十六进制的数据
                        
在bochs的gdbstub.cc 558行为处理这个指令的,不难看懂,我修改了它,让它输出access_linear的返回值,然后再调试,查看局部变量,果然valid为0,进一步,到access_linear, 经过痛苦地多次调试,发现问题处在valid=BX_MEM(0)->dgb_fetch_mem...一行,最后进到或则个函数,在memory/misc_mem.cc 512行下看到了这部分,原来是bochs发现读取的地址越界,就用0xff填充,然后返回0, 我修改了0xff,然后用gdb调试时证实了这一点。
                        
最后往上看,发现是addr比内存的长度还要大, 加入调试输出后发现gdb读的地址是0xf010
9fe0左右,而BX_MEM_THIS len仅有2000000,这显然会越界。
                        
起初我考虑也许是那里关于内存大小的设置没有弄好,但没有找到具体位置,但后来发现其实那BX_MEM_THIS len 是在.bochsrc里有定义的,0x2000000=2^25=32k,而那个0xf0109fe0是内核做的虚拟内存,gdb只跟bochs打交道,而虚拟内存是内核弄的,虚拟机又管不着。因此好像没什么好办法。。。

之后我查了很多关于虚拟内存的资料,发现其实是cpu直接支持的,于是我看到了希望。仍是从bochs的源码开刀。

一开始我是把目光放在了gdt和ldt上,但是未果。最后又发现了它里面的CPU类有一个get_segment_base函数,几经尝试,找到了临时解决方法:

修改bochs的gdbstub.cc
将567行附近的
access_linear(addr,len,BX_READ,mem);
改成
access_linear((BX_CPU(0)->get_segment_base(BX_SEG_REG_CS))+addr,len,BX_READ,mem)
;
然后重新编译,记得是用gdb stub版本(./configure --enable-gdb-stub)

思路就是在gdb服务器端自动对虚拟内存作个映射。目前发现常用的关于断点,堆栈,数据,单步等相关命令都基本正常(不过好像有同学说w命令不好使,我没确认)。

不知道这个算不算bochs的一个bug, 跟它反映了一下,至今没有回应。

不过这还是第一次半系统地阅读了一个开源软件的代码,另外涉及到gdb协议,虚拟内存,cpu等知识,混在一起,最后还弄成功了,实在很过瘾。
  

2007-09-28

mldonkey+sancho试用

参考http://forum.ubuntu.org.cn/about42337.html
http://www.haijd.net/doc/read-29.html
http://sparkplugcn.wordpress.com/2007/08/15/%e4%bb%8eemule%e5%88%b0mldonkey/

很多地方说mldonkey比amule如何如何好,而我觉得 amule本身就挺好,下完了最近几个文件后想尝试一下mldonkey

首先我是想编译一个不带 gui的 mlnet, 按照它代码里的readme, 只需简单地./configure然后make就行了,但是我这里却说需要 ocaml (readme里似乎是说只有 gui版本才需要),一开始让它自动下载编译ocaml, 但很慢,于是放弃,改用二进制包。

之后是前端,推荐的是sancho, 我下了个gtk版本的。

再之后按着帖子里的大概弄弄,配置些关于网络的东西,然后再用mldonkey release里带的插件把firefox和mldonkey关联上,需要强调的几点是

1.导入文件似乎只能下载后再本地导入
2.以下命令可以用sachon的console输入,好像也可以用telnet连接mlnet输入
2.用servers命令导入服务器,推荐下载http://www.emule.org.cn/server.met
3.用ov_load命令导入overnet的node列表,推荐下载http://download.overnet.org/contact.dat
4.用kad_load命令导入kad的node列表,推荐下载http://www.emule-inside.net/nodes.dat,也可使用eMule的nodes.data
5.注意把kad和overnet的选项打开,我一开始就忘了。。
6.注意修改最大下载限速,默认是50,太小了


感觉上如果设好了确实挺快,但也没有传说中的那么快,当然可能跟我这里网络环境有关。但是它可以连接多个服务器确实是个创新的想法

2007-09-23

linux 旋转桌面

只需在 xorg.conf里给驱动程序加上

Option "RandRRotation" "on"

然后用xrandr命令即可

ubuntu下pdf打印机

是cups-pdf包,装了就会多一个打印机,用法跟windows下acrobat那个类似.

你还别说,效果真不错。

firefox 中 设置 ed2k与amule的关联

参考:http://forum.ubuntu.org.cn/weblog.php?w=646&previous=10

安装 amule-utils包
然后在firefox中利用about:config添加如下两项

新建一个Boolean,名字为network.protocol-handler.external.ed2k,值为true
新建一个String,名字为network.protocol-handler.app.ed2k,值为/usr/bin/ed2k

2007-09-21

gedit 查看 gbk 编码文件

gedit默认不能正常查看gbk编码的文件, 原因是它第一个尝试utf-8

解决办法是用gconf修改/apps/gedit-2/preferences/encodings/auto_detected,在最前面加入一个gbk即可。

[转] Python的转码

字符串内码的转换,是开发中经常遇到的问题。
在Java中,我们可以先对某个String调用getByte(),由结果生成新String的办法来转码,也可以用NIO包里面的Charset来实现
在Python中,可以对String调用decode和encode方法来实现转码。
比如,若要将某个String对象s从gbk内码转换为UTF-8,可以如下操作
s.decode('gbk').encode('utf-8')

可是,在实际开发中,我发现,这种办法经常会出现异常:
UnicodeDecodeError: 'gbk' codec can't decode bytes in position 30664-30665: illegal multibyte sequence

这 是因为遇到了非法字符——尤其是在某些用C/C++编写的程序中,全角空格往往有多种不同的实现方式,比如\xa3\xa0,或者\xa4\x57,这些 字符,看起来都是全角空格,但它们并不是“合法”的全角空格(真正的全角空格是\xa1\xa1),因此在转码的过程中出现了异常。

这样的问题很让人头疼,因为只要字符串中出现了一个非法字符,整个字符串——有时候,就是整篇文章——就都无法转码。

幸运的是,tiny找到了完美的解决办法(我因此被批评看文档不仔细,汗啊……)
s.decode('gbk', 'ignore').encode('utf-8')

因为decode的函数原型是decode([encoding], [errors='strict']),可以用第二个参数控制错误处理的策略,默认的参数就是strict,代表遇到非法字符时抛出异常;
如果设置为ignore,则会忽略非法字符;
如果设置为replace,则会用?取代非法字符;
如果设置为xmlcharrefreplace,则使用XML的字符引用。

2007-09-20

rainlendar2.2无法运行的解决

症状:总是说cannot touch ~/.rainlendar2/rainlendar2.ini, 手工touch 也不行

原因:scim与之冲突

解决:安装scim-bridge代替scim, 具体是安装后修改/etc/X11/xinit/xinput.d/scim, 将GTK_IM_MODULE改为scim-bridge即可

2007-09-19

Amarok 不出声音

今天尝试Amarok,是以前非常喜欢的播放器。

但是就是不出声音, 还弹出一个No MP3 Support为标题的对话框。

确定插件安装全后,开始研究它的选项, 最后发现是在Settings--Configure Amarok--Engine--Configure xine Engine--Output plugin处为Autodetect,怀疑是此处问题。
改成alsa后问题解决。

2007-09-18

wine 程序菜单乱码的解决

我原来的locale 除了LC_CTYPE是zh_CN.UTF-8外,其余均为en_US.UTF-8, 但是这时wine的程序的菜单中文都不能正常显示,而程序内部有些中文则能够正常显示

解决方法是设置LC_ALL为zh_CN.UTF-8, 当然可能只改几个就行,但是没有进一步尝试。

2007-09-16

禁止自动运行的注册表项

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
"NoDriveTypeAutoRun"=dword:000000B5

[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
"NoDriveTypeAutoRun"=dword:000000B5

Windows XP 设置 NTFS 文件权限

一般来说直接点右键, 有个“安全”标签,就是了, 如果说不能改, 那么应该点“高级”, 然后去掉“从父项继承那些可以应用到子对象的权限项目, 包括那些在此明确定义的项目”

如果连“安全“标签”都没有,那么应该在资源管理器中选工具--文件夹选项--查看, 然后去掉“使用简单文件共享”。

2007-09-13

再谈声卡识别但不出声的问题

上次在一个关于声卡识别的问题提到了一个关于声卡在设备管理器中能够识别, 但是声音选项中缺说找不到音频设备的问题。

我转的解法是用Plug and Play Software Device Enumerator替换一个已有设备ISAPNP Read Data Port。起初我认为是识别有误,

但后来感到有些不对劲, 觉得那本身是一个正常的系统设备。 虽然能正常工作, 但毕竟不是完美解决方案。 后来再次尝试了上次

也提到的重装Plug and Play Software Device Enumerator的方法:

1.假设windows装在c:\windows下, 将c:\windows\inf\machine.inf, c:\windows\system32\streamci.dll, c:\windows\system32

\drivers\swenum.sys拷到一个临时目录下

2.修改machine.inf,找到[ControlFlags], 把下面一行ExcludeFromSelect=*删去

3.如果现在设备管理器中有Plug and Play Software Device Enumerator, 那么用现在的临时目录给它更新驱动即可,否则依次进

入控制面板->添加硬件, 几次"下一步"后依次选"添加新的硬件设备","安装我手动从列表选择的硬件","显示所有设备","从磁盘

安装", 此时选那个目录, 然后厂商选"(标准系统设备)", 型号选"Plug and Play Software Device Enumerator", 然后安装便可



最后还查到一个解法, 是说在注册表中删除所有swenum的相关项, 然后就能自动识别Plug and Play Software Device Enumerator

了, 不过这个没试过, 不知是否真的可行。

2007-09-12

一个关于声卡识别的问题

环境为Windows XP SP2, 声卡realtek hd audio, 似乎是nvidia的主板

症状比较特别, 是偶然卸了某驱动后发现很多硬件不能用了, 打开设备管理器发现dvd-rom, 声卡等都有黄色叹号, 另外还有一个"Plug and Play Software Device Enumerator" 也是这样。

一开始想用"卸载-重新识别"的方法, 但是那个"Plug and Play Software Device Enumerator"卸掉便一去不复返了。 一开始我没在意, 全心折腾声卡, 但是总是不行, 后来在网上艰难搜索才发现问题就出现在那个"Plug and Play Software Device Enumerator"上。

网上有个从系统目录copy出inf, sys, dll个一个文件, 稍微修改并利用它们安装一个这个硬件的方法。 我没试成功(后来发现是没理解好说明)。 之后在
http://www.softwaretipsandtricks.com/forum/windows-xp/7021-no-sound-says-no-audio-device-but-drivers-there-enabled-working-properly-5.html找到了一种解法, 原文加翻译如下:

1. From Device Manager (be sure to select View/Show hidden devices), find the 'ISAPNP Read Data Port' device from the System Devices list. (Of note: once I successfully repaired this problem, this device no longer appears.)
在设备管理器中(注意选上查看->显示隐藏的设备。(译注:似乎不选也可))找到ISAPNP Read Data Port这个设备(当成功进行下面操作后, 这个设备将不再存在(译注:成功后会由Plug and Play Software Device Enumerator取而代之, 也就是说, 问题应该是对这个硬件的识别问题))

2. Right click the ISAPNP device, and select "Update Driver"
右击那个设备,选择"更新驱动程序"

3. Select "Install from a list or specific location"
选择"从列表或指定位置安装"

4. Select "Don't search. I will choose the driver to install."
选择"不要搜索, 我要自己选择要安装的驱动程序"

5. The step I left out on prior trials: UNSELECT "Show Compatible Hardware"
不要选中"显示兼容硬件"

6. Be sure that "(Standard System Devices)" is selected under Manufacturer.
厂商选择"(标准系统设备)"

7. Under Model, scroll down until you see "Plug and Play Software Device Enumerator". Select this item, and select "Next>".
设备选Plug and Play Software Device Enumerator, 单击下一步

8. You will receive some frightening error message that you should probably disregard.
你将会看到一个警告框, 忽略她。

9. Complete the installation of the driver.
完成安装。

经过实验, 这个方法确实可行, 至少是对我的机器配置。

2007-09-09

跳过linux开机磁盘检查

linux挂载windows分区前总要扫描一下, 我觉得不大必要, 一直想跳过但不知怎么设置。
今天偶然看到了解法, 只需修改/etc/fstab, 把对应行的一列的值改为0即可

初尝 64位 linux kernel

机器是athlon64的, 不跑跑64bit linux实在不过瘾, 于是下了个linux kernel 2.6.22.5自己编译看, 一开始下载是想编译32位的, 但是启动时有点小问题, 中间会卡住, 要不断按开机键才能继续, 挺诡异的, 但是64位的编译好后竟然没有这个问题了, 比较有趣。

在编译前进行配置时倒是出了些问题, 由于我要在32位系统下编译64位内核, 是交叉编译, 不是很熟练, 查了半天在发现应该在make是加入ARCH=x86_64的参数。(ubuntu里习惯称amd64, 但linux内核里叫x86_64)

另外configure的时候也有问题, 我直接make ARCH=x86_64总报错, make ARCH=x86_64 config也不行, 后来想到, 现在的.config是按i386配置的, 可能跟64位的有冲突, 于是用make ARCH=x86_64 oldconfig重新配置, 别说, 还真问了几个新问题, 之后再make ARCH=x86_64就可以编译了, 最后别忘了编译模块,再都安装就好了。

现在运行挺不错, 但是aptitude不能找到64位程序, 还有待研究。

2007-09-03

dojo学习笔记 (一)

最近几天开始学习dojo 0.9.0, 主要是看它的源码和测试样例, 以下是这几天的研究成果

一、 dojo 概述
我用的是dojo 0.9.0, 下载压缩包解压后有四个文件夹, 依次为:
dijit: 提供了很多ui控件, 被称为widget。
dojo: 提供了很多跨浏览器的实用函数,
dojox: 似乎是提供了一些插件性质的东西, 我只对dojox.flash有点了解
util:里面只有个doh, 功能不明

二、 dojo库的使用
首要的是在html里导入dojo.js文件, dojo加载支持很多参数, 其中我目前用到的有parseOnLoad和isDebug. 设定参数的方法有两种, 一种是在导入dojo.js前通过javascript建立一个对象djConfig,包含要设定的键值对,如djConfig={isDebug:true}; 另一种是在<script>标签里加入djConfig属性,值为JSON语法的键值对, 如<script djConfig="isDebug:true" src="dojo.js"></script>
一开始我对于后一种方法感到很神奇, 起初我以为javascript可以获得自己所在标签的信息, 后来看dojo.js, 查找djConfig, 发现它一上来就判断djConfig是否存在, 否则就建立新值,这时我认为djConfig标签可以作为一个对象在javascript中可以访问, 但亲自尝试后发现并不行。于是我甚至认为这是dojo的一个bug, 但在随后的研究中我发现它确实起作用,于是不得不再认真得看dojo.js(当然是dojo.js.uncompressed.js), 发现原来它会遍历所有script标签,并对导入dojo的标签的djConfig进行处理, 实际上就是加上一对括号然后eval一下。 此时我才恍然大悟。
再说说那两个参数, isDebug不必多说, parseOnLoad是指定dojo.js是否在html加载完毕后进行扫描,识别通过标记方法声明的对象(比如dijit的widget就都支持这种声明, 十分方便)如<div dojoType=dijit.form.Menu> </div>

三、dojo库的几个常用函数
1.dojo.connect(source_obj, event, func)或dojo.connect(source_obj, event, target_obj, target_method)
这个函数相当于把自定义的函数绑在source_obj.event上, 当此事件发生(即这个函数被调用)后,便会调用source_obj.func或target_obj.target_method, 而传进去的参数跟source_obj.event得到的参数相同。
这个函数十分方便, 也是我最早听说的几个dojo实用函数之一, 但关于其实现方法, 我是看了源码才明白。

2.dojo.provide(package_name)
这个是声明当前js提供的包名, 与dojo.require配合使用

3.dojo.require(package_name)
这个指定导入一个包, 这也是十分有用的, 一个是它非常智能, package_name一般为A.B.C的格式,我们暂且称A为bass_name, 它会根据A找到基准目录, 默认是按dojo.js所在路径加上../A,不过也可以用dojo.registerModulePath注册自定义的路径(dojo已内置了一些包的路径, 如dojo, doh)。有了基准路径后, 它会首先尝试B/C.js, 看是否成功, 否则再试B.js,如果有多极, 就逐层往上退。 如果一直找不到就报错。
另一个有用的地方是它可以实现动态导入和避免重复导入, 这一方面非常适合动态的Ajax, 又保证了性能。

4.dojo.declare(class_name, base_class, properties)
dojo提供的一个OOP机制, 而这个是声明一个新类, 举例说明:
dojo.declare("son", father, {
constructor:function(){
alert("hi");
}
});
这样就声明了一个名为son的类, 父类为father, 以后可以用 new son()来创建一个实例
另外base_class可以为null或数组, 后者时,里面必须全为function对象, 以后第一个元素为新类的父类, 其他类只是为了继承其方法。
关于继承, 首先constructor是构造函数, 不会覆盖父类的构造函数, 并且父类的构造函数会被自动调用, 而其他函数则会覆盖父类的。
此外, properties中也可以定义成员变量。

5.dojo.body(), dojo.doc
前者返回body的DOM node, 后者是当前document对象, 一般就是当前的document, 不过由于可以设置整体的上下文, 如dojo.setContext, 所以在dojo框架下应该尽量使用dojo.doc

6.dojo.byId(id_string)
应该比document.getElementById有所增强, 但无论如何, 精致的名字也是诱人之处。

当然还有很多其他函数, 比如dojo.io.bind。 只是我现在还没看到网络通信部分, 等下次再补上吧。

四、dijit库的使用
dijit库的函数我主要是用的dijit.byId, 注意他和dojo.byId的不同, 这里是根据id返回已经生成的dijit的widget对象, 并不是DOM node

dijit提供的控件主要有两种生成方法, 一种是用javascript直接生成, 如new dojo.form.Button(null, node), 另一种是在html中作标记, 如<button dojoType=dijit.form.Button label="button"></button>
关于其生成widget的机制, 我目前所知是它从原始DOM节点中获取信息, 如id, label, dojoType之类, 然后据此生成一个widget, 它本身应该是绑定了一个自己的DOM节点, 最后再将它替换原始DOM节点。

根据dijit.js,在创建一个widget过程中会调用如下几个函数
postMixInProperties: 刚刚从原始节点读入信息后调用, 如果扩展已有widget,可通过connect此函数来为widget对象添加新的属性(每个widget只从原始DOM节点(可用this.srcNodeRef得到其引用)中获取需要的节点, 其他的一概抛弃, 如果需要扩展, 要在销毁前得到它,而这里所列的几个函数中,仅有此函数执行时srcNodeRef尚未背销毁)
buildingRendering:刚刚为当前widget创建好DOM节点时调用
postCreate: 当前widget已创建好并放入UI后调用
startup: 所有widget都创建好后依次为每个widget调用此函数

尽管dijit提供了非常丰富的控件, 但也未必就能满足全部需要。 因此有时需要在现有控件基础上进行扩展。这时上面三个函数就显得非常有用。 不过需要注意的是, 用dojo.declare声明一个子类时, 不能直接重定义上面三个函数,否则父类的方法会被覆盖, 而且dojo似乎没有提供super之类的属性。 我的解决方法是用dojo.declare声明时仅添加新函数,而之后再用dojo.connect对已有函数进行扩展。 如果用此方法, 需要注意, 如果新类叫childClass, 那么应该connect childClass.prototype的方法, 我一开始忘了这一点, 结果怎么都不起作用。

五、其他
dojo与Firebug支持得非常好, 它能自动识别Firebug并与其交互, 再配合Firebug的强大功能, 开发javascript 轻松了很多

六、总结
通过几天的学习, 我对dojo库的基本思想和基本实现方法都有了些了解, 也十分欣赏和钦佩它的结构。它充分利用了javascript的灵活性, 创造了很多异想天开般的设计, 令我大开眼界
就先写这么多, 能些的也就是这么多了。 随着学习的深入, 我会再补充这个学习笔记的。

2007-09-01

linux 下 驱动 Acer 笔记本 Realtek ALC268 声卡

我这个笔记本是Acer的, 声卡为nvidia mcp67集成, 解码器为realtek alc268
刚装上ubuntu 7.04后声卡能够识别, 有nvidia unknown device 和alc268字样, 但是不能发声。
起初认为是声卡没有识别好, 但是装了最新的alsa1.0.15rc1和ossv4都没效果, 了解了update-pciid才知道其实已经识别了声卡, 而问题出现在 realtek alc268上。

realtek官网上也有个驱动, 下了一看,里面原来也是alsa1.0.14, 只是驱动版本号比较特别。装上后仍不起作用。


在网上搜了很多文章, 其中这个比较好
Bug #116326 in linux-source-2.6.22 (Ubuntu): “No audio INTEL HD audio - Realtek ALC268 codec - Toshiba A205-S4577”

看来这个问题主要出在新型的笔记本上。 另外我还得知了alsa1.0.14对alc268支持的不是很好。上面那个链接里, 中间有几个人帖上了for realtek的alsa补丁, 看样子能起作用。 他们说1.0.14上支持的声卡列表里没有alc268。还有一点, 提出这个问题的人是toshiba的笔记本, 下面帖子中多次提到了model=toshiba这个参数, 于是我自然想到有没有for acer的。

于是我把目光放在了1.0.15rc1上, 看changlog里赫然有个alc268, 而其中专门多了一个acer笔记本的model。 我顿时眼前一亮。 有戏!

再看一下alsa的安装说明, 应该在snd_hda_intel模块加载时加上model=xxx 的参数。 那我这情况自然是在/etc/modprobe.d/options里加一句options snd_hda_intel model=acer

然后重启(可能仅重加载snd-hda-intel也可), 听到了熟悉的ubuntu启动声音。 啊哈哈哈!!!

update-pciids

今天学会了这个命令, 主要是lspci时一大堆unknown, 而驱动里明明支持那些id, 但是就是不能显示正确的字符串, 后来知道了update-pciids这个命令, 可以下载到最新的pci id, 之后lspci少了几个unknown. 此时才恍然大悟, 原来lspci显示的字符串跟驱动没有关系, 而是自己有一个索引。

[转] Firefox 加速技巧

转载请注明出处:www.NiDaYe.orG,谢谢

作者:Roby Liang

很早很早以前曾经写过一篇“Firefox 终极加速”的文章,可以通过手动修改 user.js 文件来获取理论上的最佳浏览速度,后来发现这种修改方式对于普通用户来说真的有点勉为其难,而且国内的网络环境并不尽如人意,我们所做的 js 优化修改在臃堵的网路上显得那么渺小与无助。

不过聊胜于无,让我们暂且先把这些复杂的优化操作变的简单一点,然后再来亲自感觉一下这些已经在网络上流传了很久的提速技巧吧。下面的操作我们都在 about:config 里进行。在 Firefox 的地址栏中输入 about:config,让我们开始提速吧。

  • network.http.pipelining


  • 在 Filter 中输入 network.http.pipelining,双击赋值为 true,默认为 false。如果没有找到这个键值,可以右键新建一个 Boolean,把她赋值为 true 就 OK 了。

    还 是像我在从前解释过的那样,激活这个键值之后,Pipelining 同时发出成倍数的连接请求,从而达到提升连接速度的效果。网络上的大多数网站都是基于 HTTP 协议,而 HTTP 1.1 可以支持多线程的连接请求,通过这个操作可以减少 Firefox 载入网页的时间。不过并不是所有网页所在的服务器都支持这种操作,所以当你修改键值之后却看不到一点实际效果的时候,请不要对我破口大骂。

  • network.http.pipelining.maxrequests


  • 在 Filter 中输入 network.http.pipelining.maxrequests,双击并赋值为 8,默认键值为 4。

  • network.http.proxy.pipelining


  • 在 Filter 中输入 network.http.proxy.pipelining,双击并赋值为 true。

    这两条优化的道理同上,这里就不再多解释了。

  • network.dns.disableIPv6


  • 在 Filter 中输入 network.dns.disableIPv6,双击并赋值为 true。

    IPv6 把 IP 地址由 32 位增加到 128 位,从而能够支持更大的地址空间,当用户在终端向一个 IPv6-capable DNS 服务器发送连接请求时,也许服务器端会错误的返回给用户一个 IPv4 地址。而 Firefox 可以对这一切明察秋毫,不过在 Firefox 纠错的同时也必然会导致信号的延迟,所以这里我们把她赋值为 true,禁用掉她。

  • content.interrupt.parsing


  • 右键新建 Boolean 值,键名为 content.interrupt.parsing,赋值 true。

    默 认这个键值并不存在。我们激活这个键值之后,当目标网页载入时,Firefox 会根据一定频率打断解析的过程,不断的向用户反馈她所收集到的网页信息,有点像流媒体的意思。这时的 Firefox 很聪明,不会一根筋的一直钻牛角。在下面的内容中我还会具体讲一下这个键值的魅力所在。

  • content.max.tokenizing.time


  • 右键新建 Integer 值,键名为 content.max.tokenizing.time,赋值 2250000。

    这 个键值的作用其实就是指定一个循环事件的处理周期,这里的单位是微秒,要是我没有算错的话。理论上当我们将这个值取的越小,网页就会从视觉上载入的越流 畅,因为 Firefox 会在很短的单位时间里反馈回解析到的网页信息。可是这样无疑延迟了网页整体载入的时间,所以在这里我们不妨将这个周期取的大一些,理论上可以加速网页的载 入。

  • content.notify.interval


  • 右键新建 Integer 值,键名为 content.notify.interval,赋值 750000。

    载 入一个网页其实也是一门很大的学问。让我们来放一个慢动作,我们姑且先把在终端第一次收到的网页信息很不专业的叫做预载入页面吧,这个页面有可能是不完整 的图片或者文字,或者别的媒体文件。从我们第一次向远端主机发出连接请求到我们在终端收到这个预载入页面花费的时间,就是这里我们要定义的键值。理论上当 我们将这个时间设置的很低时,肯定会更快的拿到所谓的预载入页面,可这是一种杀鸡取卵的做法,这样无形中反而增加了我们整体页面的载入时间。按照官方的说 法,低于 100,000 将会降低 Firefox 的性能,那好吧,那我们把她彪到 750000 吧。

  • content.notify.ontimer


  • 右键新建 Boolean 值,键名为 content.notify.ontimer,赋值 true。

    为了使我们上面设置的 750000 微秒生效,还需要把这个键值激活。只有这两个键值配合,才会起作用。

  • content.notify.backoffcount


  • 右键新建 Integer 值,键名为 content.notify.backoffcount,赋值 5。

    这 个键值控制 Firefox 的内置计数器在归零之前载入页面返回的次数。我们将目标网页分成好多个部分进行下载,每下载完一个部分,计数器归零一次。-1 就是没有限制,值为 0 时这项功能被禁用。这里我们将她设置成 5, 当返回的次数达到五次而这部分网页还没有完全下载完时,那么剩下的没有下载完的网页内容将不会再按照我们预告设置的周期,像之前的五次那样一点一点的搬运 回来,而是会一次性的下载完。也就是说在这个部分的网页下载过程中,Firefox 一共向我们反馈了 6 次信息,前 5 次的时间间隔是我们在上面的键值中设置的周期 2250000 微秒,而第 6 次也就是最后一次则没有时间限制,什么时候把剩下的下完了,什么时候反馈回来。

    只有当我们在上面提到的 content.notify.ontimer 键值为 true 的时候,这里的设置才会生效。

  • content.switch.threshold


  • 右键新建 Integer 值,键名为 content.switch.threshold ,赋值 750000,也就是四分之三秒。

    在 前面我们提到了一个键值 content.interrupt.parsing,通过激活她实际上我们可以在载入页面的过程中跟 Firefox 产生互动,毕竟我们每一个人的心里都充满了爱。把 content.interrupt.parsing 激活后当页面载入时 Firefox 会有两种操作模式:高频和低频中断模式。使用高频模式时,网页回馈的频率也很高,我们坐在显示器前看到的网页载入过程也会更加的平滑。低频时网页回馈的频 率相对比较低,可是这时反而加快了网页载入的时间。当我们移动鼠标或者触击键盘时,高频模式被激活。在经过某一段时间我们没有碰鼠标和键盘,程序没有接到 鼠标和键盘发出的任何指令时,Firefox 就会自动进入低频模式工作,而这所谓的某一段时间,就是我们这里要指定的值。

  • nglayout.initialpaint.delay


  • 右键新建 Integer 值,键名为 nglayout.initialpaint.delay,赋值 0。

    这里实际上延迟了整个网页的显示速度,但是因为用户更喜欢在整个网页完全截入之前就开始阅读网页 (就像流媒体那样),所以在这里可以把值调为零,加速用户阅读网页的速度,有时候阅读速度和载入速度并不是成正比的。

    今天先写这些吧。在网络状况稳定的情况下这些优化的确是会起到一些效果的,并不光是心理作用,大家在为自己的浏览器提速时,也可以稍微参考一下。

    2007-08-29

    ubuntu nvidia 驱动安装

    显卡是nvidia geforce 8400M G, ubuntu 论坛上说的方法是装nvidia-glx, 我看了看源里, 除了这个包还有个nvidia-glx-new, 但是看说明,似乎太老了, 不认geforce8。 事实确实如此。

    于是上nvidia官网下了个驱动, 叫做NVIDIA-Linux-x86-100.14.11-pkg1.run,但是装了不能用, X的错误信息说nvidia kernel interface版本不匹配, 而这个应该是ubuntu的linux-restricted-modules包提供的, 问题可能在这里.

    后来在nvidia论坛上证实了这一点, 在http://www.nvnews.net/vbulletin/showthread.php?s=5e6ef2b1d1fb97772f6b738b135f220b&t=72490说道

    If you wish to install the NVIDIA Linux graphics driver on a Debian GNU/Linux or Ubuntu system that ships with Xorg 7.x, please ensure that your system meets the following requirements:

    * development tools like make and gcc are installed
    * the linux-headers package matching the installed Linux kernel is installed
    * the pkg-config and xserver-xorg-dev packages are installed
    * the nvidia-glx package has been uninstalled with the --purge option and the files /etc/init.d/nvidia-glx and /etc/init.d/nvidia-kernel do not exist

    If you use Ubuntu, please also ensure that the linux-restricted-modules or linux-restricted-modules-common packages have been uninstalled. Alternatively, you can edit the /etc/default/linux-restricted-modules or /etc/default/linux-restricted-modules-common configuration file and disable the NVIDIA linux-restricted kernel modules (nvidia, nvidia_legacy) via:

    DISABLED_MODULES="nv nvidia_new"

    Additionally, delete the following file if it exists:

    /lib/linux-restricted-modules/.nvidia_new_installed

    Please note: unfortunately, it has become difficult to keep track of the pre-/post-installation steps required for [K]Ubuntu, and the above instructions may be incomplete. If in doubt, it is recommended that you use your distributor's NVIDIA Linux graphics driver packages, exclusively.

    其实我更愿意用配置文件的方法禁掉nvidia-glx, 但是后来想了想, 还是把这个包删了。

    这时再装一遍, 重启gdm, 哈哈, 出现了nvidia的logo, 成功了。 如果不想要logo, 在xorg.conf适当位置加入Option "NoLogo" "1"就行了

    另外xserver-xorg-dev似乎不是必需的, 至少我这里没装这个也能成功安装驱动。

    2007-08-14

    东方妖妖梦破解手记

    最近无聊, 又开始玩东方系列了, 其实我一直挺喜欢的, 但是水平太差.

    不过这次我发现我可以7人通妖妖梦easy了, 挺高兴. 但是lunatic...哦...难以想像.

    我的原则是"玩游戏, 而不被游戏玩", 于是只好...虽说网上有一堆现成的修改器, 但是那样就没有乐趣了.

    首先是考虑简单的方法---金山游侠, 失败.

    然后就只能去分析程序了. 首先看了眼, 没有壳. 再看导入表, 异常地干净, 只有windows库和一些direct3d的库.

    让我觉得它好像是直接用汇编写的.

    之后就要找切入点了. 我第一反应就是配置文件, th07.cfg, 因为选项里能调整默认人数, 那必定存在这里

    经过简单地研究, 发现它存在1c出, 单字节, 为实际人数-1

    初次尝试是直接修改配置文件, 比如改成7f, 然后启动游戏, 但是发现人数回到默认值了, 且退出游戏后发现配置文件也改回来了. 嗯. 有防备啊...

    那只好再看代码了, 给所有CreateFileA设断, 运行时只看那些打开th07.cfg的, 一共两个, 且一个是打开后写些数据就CloseHandle了, 重点再另一个. 再找有关的ReadFile, 只有一个, 且在那之前有个GetFileSize, 看来是一次读入的, 呵呵, 给ReadFile的buffer下内存断, 不一会儿断在了00436f3d, 是个串移动指令rep movs...此时注意目标地址, 56ba64, 这可是重要情报!

    然后该上静态分析了, 搜这个地址(一看就是全局变量), 没有多少, 其中看到了读完文件后进行校验的, 比如00436fbd处, 判断它是否比5大, 若是就回到默认值, 真是一清二楚. 那没什么说的了, 给exe做个备份, 然后ultraedit之...

    但是事情没有想的那么顺利, 程序自动退出了, 看来这里也有防备.

    不过既然网上有内存修改器, 那说明它肯定内存方面没有设防, 于是用olly载入, 修改后再运行, 就成功了!

    后来想再整理一下, 找到最关键的修改位置, 找到了0044ff3f处, 简单地说如果当前人数小于4就加1, 否则不变, 我挺奇怪这里是干什么的, 下了断运行了半天, 发现原来是设置人数是, 按下右方向键, 则人数加1, 但是如果已经加到5个人就不能再加了. 啊哈哈, 被我抓住尾巴了, 从这里返回的数据似乎没有再做什么校验, 把小于4的限制去了, 再看看配置, 哈, 想加多少就加多少.其实也可以把代码直接改成mov byte ptr [56ba64], ff

    现在再去玩Lunatic, 啊哈哈, 这才叫玩游戏. "轻松"通关. (你要是这样都过不去, 神也救不了你了)

    最后是想玩Extra, 但是数据是内置的, 比较麻烦, 一开始一筹莫展, 不知从何下手.

    一番苦战后, 想到看看字符串资源, 发现了Extra Rank字样, 于004269bd, 向上看发现了各个级别的名称, 再看一下, 原来是switch的[61c260], 那这个就是级别没错了.

    据此下断, 找所有对级别==4(即extra)的特殊判断, 运行游戏, 选extra, 断在了00451803, 这里特殊的地方只是改了个全局变量[62583c], 第二次断在了0042d09e, 运行至0042d0a4时发现原来eax此时指的就是配置文件区,这里它把一个变量设为2, 然后判断一个全局变量[625628]的最低位, 若为1则又把此变量改为8, 这里很可疑, 我断在此处, 清了zf, 让它改成8, 然后运行, 啊, 果然它就是人数, 于是恍然大悟, 赶快去0042d0a4把人数改成7f. Extra, 我来了...

    总结一下, 这次的成功主要是由于源程序本身很干净, 而我的思路正确, 运气也不错, 希望下次还能这样.

    另外补充几点:
    1.人数如果改的比较小, 而你又死光了, 好像会出除零错, 这个我就没再研究了, 总之改大些呗
    2.游戏进到图形模式后, olly断下了却看不到, 我的办法是把分辨率调到1600x1200, 然后游戏只占左上角一小片, 其它地方看olly就好了
    3.有时卡得连分辨率也改不了, 这是我就让电脑待机再启动, 这样就行了.
    4.后来玩东方永夜抄, 过程几乎一样, 有了上次经验, 这次轻松破解, 不过感觉还是直接改配置文件, 然后把配置文件校验改了方便些. 另外, 改extra时直接搜了mov byte ptr [eax+1c], 2就搜到了, 比较搞笑
    5.注, 永夜抄人数在[17ce88c], 而级别在[160f538]

    2007-08-12

    再谈pdftohtml支持中文

    以前的文章里提到过pdftohtml支持中文的问题, 是用ccmap里面的.cmap文件解决的.

    所谓pdftohtml支持中文, 其实根fpdftohtml没什么关系, 而是pdf文件自己需要支持. 我认为它跟pdf支持复制粘贴是一回事.

    今天突然发现我的pdftohtml又不支持中文了, 整蛊半天发现我不知什么时候我把那些.cmap文件删了-_-b

    于是再去http://lsec.cc.ac.cn/cgi-bin/viewcvs.cgi/cct/ccmap/#dirlist逛逛, 发现原来的cmap的tar包已经删除了, 而多了一个makecmap.tex文件用于生成cmap文件.

    我要转的是GBK编码的, 用命令 sudo latex \\def\\cmapEnc{GBK} \\input{makecmap.tex} 即可,
    但是虽然弄出来了cmap文件, 编译出来的pdf还是不能复制粘贴, pdftohtml当然也不行.

    于是上网搜了搜, 发现了CJKutf8, 说是能代替ccmap, 于是试了下, 真的可以, 只需要加入\usepackage{CJKutf8} 即可, 真不错.

    不过,好像有超长行时会出些bug.

    以后考虑用utf8了...

    2007-07-11

    kernel 编译补遗

    上回说到我的kernel能够启动了,不过还是有些问题。

    一个是不断出modprobe: fatal: 说找不到.../modules.dep之类, 而且一出就好几屏,上网搜了一下,也有人遇到这问题,不过好像他们都是不能启动, 估计跟我情况不一样。后来经高人指点, 原来我的kernel不用initrd, 于是make-kpkg时不要--initrd就好了(一开始没出initrd我还以为出bug了)

    另外是声卡的问题, 我查了下,并不是没认出声卡,而是多了dummy设备和virtual midi设备, 我没查到如何配置alsa默认声卡,于是呼把这两个选项去了。

    最后是说dma没打开,不知道为什么当时漏掉了,不过它还真是体贴,提醒了一下,要不然我以后慢的都不知道怎么回事。

    最后基本就都正常了,跟原来比,启动是多了一点点错误信息,但是似乎不影响使用。

    2007-07-10

    Linux kernel 2.6.20.3 编译成功!

    几经波折,终于把Linux内核搞定了。

    起因是原先ubuntu带的内核版本较低, 决定升级,可是发现安装后modules 有59M。 启动后lsmod
    再一看,俺的神呀, 刷刷地, 竟然有两屏。 另外新内核把我的ide硬盘人成了sda, grub里还要改革选项(一开始不知道,怎么也不能启动), 于是决定自己编译一下。

    不过过程并不顺利, 一个是我机器的配置, 是近10年前的笔记本, 编译一次内核要将近两个小时。 另外就是我没经验。

    现在想想,可能一共得编译了十几次。 中间闹了很多笑话。比如一开始删的模块比较少, 编译完能启动, 但是modules仍有29M, 硬盘还是sda, 后来有狠心删了一次, 这回好了, modules小多了,硬盘也变回hda了, 可是initramfs mount根时说'no such device' , 有时则是 kernel panic。 我在这个问题上就折腾了好久。还有一次是配置文件搞错了,一下编译了很多模块, 中间吃掉硬盘1G多, 告诉我‘no space left’。。。

    中间配置了好几次, 一般都是把ubuntu默认的config拷过来并在此基础上修改。 今天终于成功了, 最后kernel是2M左右, modules一共5M多。 感觉上还能删一些, 不过不用了。

    经过几次配置,我对kernel倒是有了进一步了解, 至少对其大概架构有了个印象。若不是我机器如此之慢, 编译过程应该也是很有趣的。现在残留的问题是有关声卡的,应该很容易解决。不过到现在我仍不清楚为什么一开始新内核会认成sda, 也听说一些同学有类似情况。

    另外,在编译过程中参考了http://lamp.linux.gov.cn/Linux/kernel_options.html,很有帮助, 对作者表示感谢!

    [转] 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 [-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的,那你应该使用-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 -y
    来得到。

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

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

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

    2.1.7 `-E 选项: 把错误信息输入到文件。

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

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

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

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

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

    nasm -s -f obj myfile.asm | more

    请参考2.1.7的-E选项.

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

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

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

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

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

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

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

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

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

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

    nasm myfile.asm -p myinc.inc

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

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

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

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

    nasm myfile.asm -dFOO=100

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

    %define FOO 100

    在文件的开始,你可以取消一个宏定义,同样,选项-dFOO等同于代码%define FOO。
    这种形式的操作符在选择编译时操作中非常有用,它们可以用%ifdef来进行测试,
    比如-dDEBUG。

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

    2.1.12 `-u 选项: 取消一个宏定义。

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

    比如,下面的命令语句:

    nasm myfile.asm -dFOO=100 -uFOO

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

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

    2.1.13 `-e选项: 仅预处理。

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

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

    %assign tablesize ($-tablestart)

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

    2.1.14 `-a 选项: 不需要预处理。

    如果NASM被用作编译器的后台,那么假设编译器已经作完了预处理,并禁止NASM的预
    处理功能显然是可以节约时间,加快编译速度。-a选项(不需要参数),会让NASM把
    它强大的预处理器换成另一个什么也不做的预处理器。

    2.1.15 `-On选项: 指定多遍优化。

    NASM在缺省状态下是一个两遍的汇编器。这意味着如果你有一个复杂的源文件需要
    多于两遍的汇编。你必须告诉它。

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

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

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

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

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

    2.1.16 `-t选项: 使用TASM兼容模式。

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

    (*)本地符号的前缀由.改为@@

    (*)TASM风格的以@开头的应答文件可以由命令行指定。这和NASM支持的-@resp
    风格是不同的。

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

    (*)%arg预处理操作符被支持,它同TASM的ARG操作符相似。

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

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

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

    (*) 还有很多...

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

    2.1.17 `-w选项: 使汇编警告信息有效或无效。

    NASM可以在汇编过程中监视很多的情况,其中很多是值得反馈给用户的,但这些情况
    还不足以构成严重错误以使NASM停止产生输出文件。这些情况被以类似错误的形式
    报告给用户,但在报告信息的前面加上warning字样。警告信息不会阻止NASM产生
    输出文件并向操作系统返回成功信息。

    有些情况甚至还要宽松:他们仅仅是一些值得提供给用户的信息。所以,NASM支持-w
    命令行选项。它以使特定类型的汇编警告信息输出有效或无效。这样的警告类型是
    以命名来描述的,比如,orphan-labels,你可以以下列的命令行选项让此类警告信息
    得以输出:-w+orphan-labels,或者以-w-orphan-labels让此类信息不能输出。

    可禁止的警告信息类型有下列一些:

    (*)`macro-params包括以错误的参数个数调用多行的宏定义的警告。这类警告信息
    缺省情况下是输出的,至于为什么你可能需要禁止它,请参阅4.3.1。

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

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

    2.1.18 `-v选项: 打印版本信息。

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

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

    2.1.19 `NASMENV环境变量。

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

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

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

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

    2.2 MASM用户速成。

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

    2.2.1 NASM是大小写敏感的。

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

    2.2.2 NASM需要方括号来引用内存地址。

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

    foo equ 1
    bar dw 2

    然后有两行的代码:

    mov ax,foo
    mov ax,bar

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

    NASM为了避免这种令人讨厌的情况,拥有一个相当简单的内存引用语未能。规则
    是任何对内存中内容的存取操作必须要在地址上加上方括号。但任何对地址值
    的操作不需要。所以,形如mov ax,foo的指令总是代表一个编译时常数,不管它
    是一个 EQU或一个变量的地址;如果要取变量bar的内容,你必须与
    mov ax,[bar]。

    这也意味着NASM不需要MASM的OFFSET关键字,因为MASM的代码mov ax,offset bar
    同NASM的语法mov ax,bar是完全等效的。如果你希望让大量的MASM代码能够被
    NASM汇编通过,你可以编写%idefine offset让预处理器把OFFSET处理成一个无
    操作符。

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

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

    2.2.3 NASM不存储变量的类型。

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

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

    2.2.4 NASM不会 `ASSUME

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

    2.2.5 NASM不支持内存模型。

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

    2.2.6 浮点处理上的不同。

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

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

    2.2.7 其他不同。

    由于历史的原因,NASM把MASM兼容汇编器的TBYTE写成TWORD。

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

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

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

    3.1 NASM源程序行的组成。

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

    label: instruction operands ; comment

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

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

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

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

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

    在使用一个前缀时,指令不是必须的,像CS,A32,LOCK或REPE这样的段前缀
    可以单独出现在一行上,NASM仅仅产生一个前缀字节。

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

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

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

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

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

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

    3.2 伪指令。

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

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

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

    db 0x55 ; just the byte 0x55
    db 0x55,0x56,0x57 ; three bytes in succession
    db a,0x55 ; character constants are OK
    db hello,13,10,$ ; so are string constants
    dw 0x1234 ; 0x34 0x12
    dw a ; 0x41 0x00 (its 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: 重复指令或数据。

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

    2.1.13 `-e选项: 仅预处理。

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

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

    %assign tablesize ($-tablestart)

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

    2.1.14 `-a 选项: 不需要预处理。

    如果NASM被用作编译器的后台,那么假设编译器已经作完了预处理,并禁止NASM的预
    处理功能显然是可以节约时间,加快编译速度。-a选项(不需要参数),会让NASM把
    它强大的预处理器换成另一个什么也不做的预处理器。

    2.1.15 `-On选项: 指定多遍优化。

    NASM在缺省状态下是一个两遍的汇编器。这意味着如果你有一个复杂的源文件需要
    多于两遍的汇编。你必须告诉它。

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

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

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

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

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

    2.1.16 `-t选项: 使用TASM兼容模式。

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

    (*)本地符号的前缀由.改为@@

    (*)TASM风格的以@开头的应答文件可以由命令行指定。这和NASM支持的-@resp
    风格是不同的。

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

    (*)%arg预处理操作符被支持,它同TASM的ARG操作符相似。

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

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

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

    (*) 还有很多...

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

    2.1.17 `-w选项: 使汇编警告信息有效或无效。

    NASM可以在汇编过程中监视很多的情况,其中很多是值得反馈给用户的,但这些情况
    还不足以构成严重错误以使NASM停止产生输出文件。这些情况被以类似错误的形式
    报告给用户,但在报告信息的前面加上warning字样。警告信息不会阻止NASM产生
    输出文件并向操作系统返回成功信息。

    有些情况甚至还要宽松:他们仅仅是一些值得提供给用户的信息。所以,NASM支持-w
    命令行选项。它以使特定类型的汇编警告信息输出有效或无效。这样的警告类型是
    以命名来描述的,比如,orphan-labels,你可以以下列的命令行选项让此类警告信息
    得以输出:-w+orphan-labels,或者以-w-orphan-labels让此类信息不能输出。

    可禁止的警告信息类型有下列一些:

    (*)`macro-params包括以错误的参数个数调用多行的宏定义的警告。这类警告信息
    缺省情况下是输出的,至于为什么你可能需要禁止它,请参阅4.3.1。

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

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

    2.1.18 `-v选项: 打印版本信息。

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

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

    2.1.19 `NASMENV环境变量。

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

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

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

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

    2.2 MASM用户速成。

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

    2.2.1 NASM是大小写敏感的。

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

    2.2.2 NASM需要方括号来引用内存地址。

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

    foo equ 1
    bar dw 2

    然后有两行的代码:

    mov ax,foo
    mov ax,bar

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

    NASM为了避免这种令人讨厌的情况,拥有一个相当简单的内存引用语未能。规则
    是任何对内存中内容的存取操作必须要在地址上加上方括号。但任何对地址值
    的操作不需要。所以,形如mov ax,foo的指令总是代表一个编译时常数,不管它
    是一个 EQU或一个变量的地址;如果要取变量bar的内容,你必须与
    mov ax,[bar]。

    这也意味着NASM不需要MASM的OFFSET关键字,因为MASM的代码mov ax,offset bar
    同NASM的语法mov ax,bar是完全等效的。如果你希望让大量的MASM代码能够被
    NASM汇编通过,你可以编写%idefine offset让预处理器把OFFSET处理成一个无
    操作符。

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

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

    2.2.3 NASM不存储变量的类型。

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

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

    2.2.4 NASM不会 `ASSUME

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

    2.2.5 NASM不支持内存模型。

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

    2.2.6 浮点处理上的不同。

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

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

    2.2.7 其他不同。

    由于历史的原因,NASM把MASM兼容汇编器的TBYTE写成TWORD。

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

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

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

    3.1 NASM源程序行的组成。

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

    label: instruction operands ; comment

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

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

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

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

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

    在使用一个前缀时,指令不是必须的,像CS,A32,LOCK或REPE这样的段前缀
    可以单独出现在一行上,NASM仅仅产生一个前缀字节。

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

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

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

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

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

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

    3.2 伪指令。

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

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

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

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

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

    3.2.2 `RESB类的伪指令: 声明未初始化的数据。

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

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

    3.2.3 `INCBIN:包含其他二进制文件。

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

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

    3.2.4 `EQU: 定义常数。

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

    message db hello, world
    msglen equ $-message

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

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

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

    zerobuf: times 64 db 0

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

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

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

    times 100 movsb

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

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

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

    3.3 有效地址

    一个有效地址是一个指令的操作数,它是对内存的一个引用。在NASM中,有效地址
    的语法是非常简单的:它由一个可计算的表达式组成,放在一个中括号内。比如:

    wordvar dw 123
    mov ax,[wordvar]
    mov ax,[wordvar+1]
    mov ax,[es:wordvar+bx]

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

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

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

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

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

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

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

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

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

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

    3.4 常数

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

    3.4.1 数值常数。

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

    请看一些例子:

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

    3.4.2 字符型常数。

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

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

    mov eax,abcd

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

    3.4.3 字符串常数。

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

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

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

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

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

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

    3.4.4 浮点常量

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

    一些例子:

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

    NASM不能在编译时求浮点常数的值。这是因为NASM被设计为可移植的,尽管它
    常产生x86处理器上的代码,汇编器本身却可以和ANSI C编译器一起运行在任
    何系统上。所以,汇编器不能保证系统上总存在一个能处理Intel浮点数的浮
    点单元。所以,NASM为了能够处理浮点运算,它必须含有它自己的一套完整
    的浮点处理例程,它大大增加了汇编器的大小,却获得了并不多的好处。

    3.5 表达式

    NASM中的表达式语法跟C里的是非常相似的。

    NASM不能确定编译时在计算表达式时的整型数尺寸:因为NASM可以在64位系
    统上非常好的编译和运行,不要假设表达式总是在32位的寄存器中被计算的,
    所以要慎重地对待整型数溢出的情况。它并不总能正常的工作。NASM唯一能
    够保证的是:你至少拥有32位长度。

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

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

    3.5.1 `|: 位或运算符。

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

    3.5.2 `^: 位异或运算符。

    `^ 提供位异或操作。

    3.5.3 `&: 位与运算符。

    `& 提供位与运算。

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

    `<< 提供位左移, 跟C中的实现一样,所以5<<3相当于把5乘上8。>>提
    供位右移。在NASM中,这样的位移总是无符号的,所以位移后,左侧总是以
    零填充,并不会有符号扩展。

    3.5.5 `+ and `-: 加与减运算符。

    +与-运算符提供完整的普通加减法功能。

    3.5.6 `*, `/, `//, `%和`%%: 乘除法运算符。

    *是乘法运算符。/和//都是除法运算符,/是无符号除,//是带
    符号除。同样的,%和%%提供无符号与带符号的模运算。

    同ANSI C一样,NASM不保证对带符号模操作执行的操作的有效性。

    因为%符号也被宏预处理器使用,你必须保证不管是带符号还是无符号的
    模操作符都必须跟有空格。

    3.5.7 一元运算符: `+, `-, `~和`SEG

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

    3.6 `SEG和`WRT

    当写很大的16位程序时,必须把它分成很多段,这时,引用段内一个符号的
    地址的能力是非常有必要的,NASM提供了SEG操作符来实现这个功能。

    SEG操作符返回符号所在的首选段的段基址,即一个段基址,当符号的偏
    移地址以它为参考时,是有效的,所以,代码:

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

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

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

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

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

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

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

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


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

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

    dw symbol, seg symbol

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

    3.7 `STRICT: 约束优化。

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

    push dword 33

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

    push strict dword 33

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

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

    3.8 临界表达式。

    NASM的一个限制是它是一个两遍的汇编器;不像TASM和其它汇编器,它总是
    只做两遍汇编。所以它就不能处理那些非常复杂的需要三遍甚至更多遍汇编
    的源代码。

    第一遍汇编是用于确定所有的代码与数据的尺寸大小,这样的话,在第二遍
    产生代码的时候,就可以知道代码引用的所有符号地址。所以,有一件事
    NASM不能处理,那就是一段代码的尺寸依赖于另一个符号值,而这个符号又
    在这段代码的后面被声明。比如:

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

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

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

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

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

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


    mov ax,symbol1
    symbol1 equ symbol2
    symbol2:

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

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

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

    mov eax,[ebx+offset]
    offset equ 10

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

    3.9 本地Labels

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

    label1 ; some code

    .loop
    ; some more code

    jne .loop
    ret

    label2 ; some code

    .loop
    ; some more code

    jne .loop
    ret

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

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

    label3 ; some more code
    ; and some more

    jmp label1.loop

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

    label1: ; a non-local label
    .local: ; this is really label1.local
    ..@foo: ; this is a special symbol
    label2: ; another non-local label
    .local: ; this is really label2.local

    jmp ..@foo ; this will jump three lines up

    NASM还能定义其他的特殊符号,比如以两个句点开始的符号,比如
    ..start被用来指定.obj输出文件的执行入口。(参阅6.2.6)

    第四章 NASM预处理器。
    --------------------------------

    NASM拥有一个强大的宏处理器,它支持条件汇编,多级文件包含,两种形式的
    宏(单行的与多行的),还有为更强大的宏能力而设置的‘context stack机制
    预处理指令都是以一个%打头。

    预处理器把所有以反斜杠(\)结尾的连续行合并为一行,比如:

    %define THIS_VERY_LONG_MACRO_NAME_IS_DEFINED_TO \
    THIS_value

    会像是单独一行那样正常工作。

    4.1 单行的宏。

    4.1.1 最常用的方式: `%define

    单行的宏是以预处理指令%define定义的。定义工作同C很相似,所以你可
    以这样做:

    %define ctrl 0x1F &
    %define param(a,b) ((a)+(a)*(b))

    mov byte [param(2,ebx)], ctrl D

    会被扩展为:

    mov byte [(2)+(2)*(ebx)], 0x1F & D

    当单行的宏被扩展开后还含有其它的宏时,展开工作会在执行时进行,而不是
    定义时,如下面的代码:

    %define a(x) 1+b(x)
    %define b(x) 2*x

    mov ax,a(8)

    会如预期的那样被展开成mov ax, 1+2*8, 尽管宏b并不是在定义宏a
    的时候定义的。

    用%define定义的宏是大小写敏感的:在代码%define foo bar之后,只有
    foo会被扩展成bar:Foo或者FOO都不会。用%idefine来代替%define
    (i代表insensitive),你可以一次定义所有的大小写不同的宏。所以
    %idefine foo bar会导致foo,FOO,Foo等都会被扩展成bar。

    当一个嵌套定义(一个宏定义中含有它本身)的宏被展开时,有一个机制可以
    检测到,并保证不会进入一个无限循环。如果有嵌套定义的宏,预处理器只
    会展开第一层,因此,如果你这样写:

    %define a(x) 1+a(x)

    mov ax,a(3)

    宏 `a(3)会被扩展成1+a(3),不会再被进一步扩展。这种行为是很有用的,有
    关这样的例子请参阅8.1。

    你甚至可以重载单行宏:如果你这样写:

    %define foo(x) 1+x
    %define foo(x,y) 1+x*y

    预处理器能够处理这两种宏调用,它是通过你传递的参数的个数来进行区分的,
    所以foo(3)会变成1+3,而foo(ebx,2)会变成1+ebx*2。尽管如此,但如果
    你定义了:

    %define foo bar

    那么其他的对foo的定义都不会被接受了:一个不带参数的宏定义不允许
    对它进行带有参数进行重定义。

    但这并不能阻止单行宏被重定义:你可以像这样定义,并且工作得很好:

    %define foo bar

    然后在源代码文件的稍后位置重定义它:

    %define foo baz

    然后,在引用宏foo的所有地方,它都会被扩展成最新定义的值。这在用
    %assign定义宏时非常有用(参阅4.1.5)

    你可以在命令行中使用-d选项来预定义宏。参阅2.1.11

    4.1.2 %define的增强版: `%xdefine

    与在调用宏时展开宏不同,如果想要调用一个嵌入有其他宏的宏时,使用
    它在被定义的值,你需要%define不能提供的另外一种机制。解决的方案
    是使用%xdefine,或者它的大小写不敏感的形式%xidefine。

    假设你有下列的代码:

    %define isTrue 1
    %define isFalse isTrue
    %define isTrue 0

    val1: db isFalse

    %define isTrue 1

    val2: db isFalse

    在这种情况下,val1等于0,而val2等于1。这是因为,当一个单行宏用
    %define定义时,它只在被调用时进行展开。而isFalse是被展开成
    isTrue,所以展开的是当前的isTrue的值。第一次宏被调用时,isTrue
    是0,而第二次是1。

    如果你希望isFalse被展开成在isFalse被定义时嵌入的isTrue的值,
    你必须改写上面的代码,使用%xdefine:

    %xdefine isTrue 1
    %xdefine isFalse isTrue
    %xdefine isTrue 0

    val1: db isFalse

    %xdefine isTrue 1

    val2: db isFalse

    现在每次isFalse被调用,它都会被展开成1,而这正是嵌入的宏isTrue
    在isFalse被定义时的值。

    4.1.3 : 连接单行宏的符号: `%+

    一个单行宏中的单独的记号可以被连接起来,组成一个更长的记号以
    待稍后处理。这在很多处理相似的事情的相似的宏中非常有用。

    举个例子,考虑下面的代码:

    %define BDASTART 400h ; Start of BIOS data area

    struc tBIOSDA ; its structure
    .COM1addr RESW 1
    .COM2addr RESW 1
    ; ..and so on
    endstruc

    现在,我们需要存取tBIOSDA中的元素,我们可以这样:

    mov ax,BDASTART + tBIOSDA.COM1addr
    mov bx,BDASTART + tBIOSDA.COM2addr

    如果在很多地方都要用到,这会变得非常的繁琐无趣,但使用下面
    的宏会大大减小打字的量:

    ; Macro to access BIOS variables by their names (from tBDA):

    %define BDA(x) BDASTART + tBIOSDA. %+ x

    现在,我们可以象下面这样写代码:

    mov ax,BDA(COM1addr)
    mov bx,BDA(COM2addr)

    使用这个特性,我们可以简单地引用大量的宏。(另外,还可以减少打
    字错误)。

    4.1.4 取消宏定义: `%undef

    单行的宏可以使用%undef命令来取消。比如,下面的代码:

    %define foo bar
    %undef foo

    mov eax, foo

    会被展开成指令mov eax, foo,因为在%undef之后,宏foo处于无定义
    状态。

    那些被预定义的宏可以通过在命令行上使用-u选项来取消定义,参阅
    2.1.12。

    4.1.5 预处理器变量 : `%assign

    定义单行宏的另一个方式是使用命令%assign(它的大小写不敏感形式
    是%iassign,它们之间的区别与%idefine,%idefine之间的区别完全相
    同)。

    %assign被用来定义单行宏,它不带有参数,并有一个数值型的值。它的
    值可以以表达式的形式指定,并要在%assing指令被处理时可以被一次
    计算出来,

    就像%define,%assign定义的宏可以在后来被重定义,所以你可以这
    样做:

    %assign i i+1

    以此来增加宏的数值

    %assing在控制%rep的预处理器循环的结束条件时非常有用:请参
    阅4.5的例子。另外的关于%assign的使用在7.4和8.1中的提到。

    赋给%assign的表达式也是临界表达式(参阅3.8),而且必须可被计算
    成一个纯数值型(不能是一个可重定位的指向代码或数据的地址,或是包
    含在寄存器中的一个值。)

    4.2 字符串处理宏: `%strlen and `%substr

    在宏里可以处理字符串通常是非常有用的。NASM支持两个简单的字符
    串处理宏,通过它们,可以创建更为复杂的操作符。

    4.2.1 求字符串长度: `%strlen

    %strlen宏就像%assign,会为宏创建一个数值型的值。不同点在于
    %strlen创建的数值是一个字符串的长度。下面是一个使用的例子:

    %strlen charcnt my string

    在这个例子中,charcnt会接受一个值8,就跟使用了%assign一样的
    效果。在这个例子中,my string是一个字面上的字符串,但它也可以
    是一个可以被扩展成字符串的单行宏,就像下面的例子:

    %define sometext my string
    %strlen charcnt sometext

    就像第一种情况那样,这也会给charcnt赋值8

    4.2.2 取子字符串: `%substr

    字符串中的单个字符可以通过使用%substr提取出来。关于它使用的
    一个例子可能比下面的描述更为有用:

    %substr mychar xyz 1 ; equivalent to %define mychar x
    %substr mychar xyz 2 ; equivalent to %define mychar y
    %substr mychar xyz 3 ; equivalent to %define mychar z

    在这个例子中,mychar得到了值z。就像在%strlen(参阅4.2.1)中那样,
    第一个参数是一个将要被创建的单行宏,第二个是字符串,第三个参数
    指定哪一个字符将被选出。注意,第一个索引值是1而不是0,而最后一
    个索引值等同于%strlen给出的值。如果索引值超出了范围,会得到
    一个空字符串。

    4.3 多行宏: `%macro

    多行宏看上去更象MASM和TASM中的宏:一个NASM中定义的多行宏看上去就
    象下面这样:

    %macro prologue 1

    push ebp
    mov ebp,esp
    sub esp,%1

    %endmacro

    这里,定义了一个类似C函数的宏prologue:所以你可以通过一个调用来使
    用宏:

    myfunc: prologue 12

    这会把三行代码扩展成如下的样子:

    myfunc: push ebp
    mov ebp,esp
    sub esp,12

    在%macro一行上宏名后面的数字1定义了宏可以接收的参数的个数。
    宏定义里面的%1是用来引用宏调用中的第一个参数。对于一个有多
    个参数的宏,参数序列可以这样写:%2,%3等等。

    多行宏就像单行宏一样,也是大小写敏感的,除非你使用另一个操作符
    ‘%imacro

    如果你必须把一个逗号作为参数的一部分传递给多行宏,你可以把整
    个参数放在一个括号中。所以你可以象下面这样编写代码:

    %macro silly 2

    %2: db %1

    %endmacro

    silly a, letter_a ; letter_a: db a
    silly ab, string_ab ; string_ab: db ab
    silly {13,10}, crlf ; crlf: db 13,10

    4.3.1 多行宏的重载

    就象单行宏,多行宏也可以通过定义不同的参数个数对同一个宏进行多次
    重载。而这次,没有对不带参数的宏的特殊处理了。所以你可以定义:

    %macro prologue 0

    push ebp
    mov ebp,esp

    %endmacro

    作为函数prologue的另一种形式,它没有开辟本地栈空间。

    有时候,你可能需要重载一个机器指令;比如,你可能想定义:

    %macro push 2

    push %1
    push %2

    %endmacro

    这样,你就可以如下编写代码:

    push ebx ; this line is not a macro call
    push eax,ecx ; but this one is

    通常,NASM会对上面的第一行给出一个警告信息,因为push现在被定义成
    了一个宏,而这行给出的参数个数却不符合宏的定义。但正确的代码还是
    会生成的,仅仅是给出一个警告而已。这个警告信息可以通过
    -wmacro-params’命令行选项来禁止。(参阅2.1.17)。

    4.3.2 Macro-Local Labels

    NASM允许你在多行宏中定义labels.使它们对于每一个宏调用来讲是本地的:所
    以多次调用同一个宏每次都会使用不同的label.你可以通过在label名称前面
    加上%%来实现这种用法.所以,你可以创建一条指令,它可以在Z标志位被
    设置时执行RET指令,如下:

    %macro retz 0

    jnz %%skip
    ret
    %%skip:

    %endmacro

    你可以任意多次的调用这个宏,在你每次调用时的时候,NASM都会为%%skip
    建立一个不同的名字来替换它现有的名字.NASM创建的名字可能是这个样子
    的:..@2345.skip,这里的数字2345在每次宏调用的时候都会被修改.而
    ..@前缀防止macro-local labels干扰本地labels机制,就像在3.9中所
    描述的那样.你应该避免在定义你自己的宏时使用这种形式(..@前缀,然后
    是一个数字,然后是一个句点),因为它们会和macro-local labels相互产生
    干扰.

    4.3.3 不确定的宏参数个数.

    通常,定义一个宏,它可以在接受了前面的几个参数后, 把后面的所有参数都
    作为一个参数来使用,这可能是非常有用的,一个相关的例子是,一个宏可能
    用来写一个字符串到一个MS-DOS的文本文件中,这里,你可能希望这样写代码:

    writefile [filehandle],"hello, world",13,10

    NASM允许你把宏的最后一个参数定义成"贪婪参数", 也就是说你调用这个宏时
    ,使用了比宏预期得要多得多的参数个数,那所有多出来的参数连同它们之间
    的逗号会被作为一个参数传递给宏中定义的最后一个实参,所以,如果你写:

    %macro writefile 2+

    jmp %%endstr
    %%str: db %2
    %%endstr:
    mov dx,%%str
    mov cx,%%endstr-%%str
    mov bx,%1
    mov ah,0x40
    int 0x21

    %endmacro

    那上面使用writefile的例子会如预期的那样工作:第一个逗号以前的文本
    [filehandle]会被作为第一个宏参数使用,会被在%1的所有位置上扩展,而
    所有剩余的文本都被合并到%2中,放在db后面.

    这种宏的贪婪特性在NASM中是通过在宏的%macro一行上的参数个数后面加
    上+来实现的.

    如果你定义了一个贪婪宏,你就等于告诉NASM对于那些给出超过实际需要的参
    数个数的宏调用该如何扩展; 在这种情况下,比如说,NASM现在知道了当它看到
    宏调用writefile带有2,3或4个或更多的参数的时候,该如何做.当重载宏
    时,NASM会计算参数的个数,不允许你定义另一个带有4个参数的writefile
    宏.

    当然,上面的宏也可以作为一个非贪婪宏执行,在这种情况下,调用语句应该
    象下面这样写:

    writefile [filehandle], {"hello, world",13,10}

    NASM提供两种机制实现把逗号放到宏参数中,你可以选择任意一种你喜欢的
    形式.

    有一个更好的办法来书写上面的宏,请参阅5.2.1

    4.3.4 缺省宏参数.

    NASM可以让你定义一个多行宏带有一个允许的参数个数范围.如果你这样做了,
    你可以为参数指定缺省值.比如:

    %macro die 0-1 "Painful program death has occurred."

    writefile 2,%1
    mov ax,0x4c01
    int 0x21

    %endmacro

    这个宏(它使用了4.3.3中定义的宏writefile)在被调用的时候可以有一个
    错误信息,它会在退出前被显示在错误输出流上,如果它在被调用时不带参数
    ,它会使用在宏定义中的缺省错误信息.

    通常,你以这种形式指定宏参数个数的最大值与最小值; 最小个数的参数在
    宏调用的时候是必须的,然后你要为其他的可选参数指定缺省值.所以,当一
    个宏定义以下面的行开始时:

    %macro foobar 1-3 eax,[ebx+2]

    它在被调用时可以使用一到三个参数, 而%1在宏调用的时候必须指定,%2
    在没有被宏调用指定的时候,会被缺省地赋为eax,%3会被缺省地赋为
    [ebx+2].

    你可能在宏定义时漏掉了缺省值的赋值, 在这种情况下,参数的缺省值被赋为
    空.这在可带有可变参数个数的宏中非常有用,因为记号%0可以让你确定有
    多少参数被真正传给了宏.

    这种缺省参数机制可以和贪婪参数机制结合起来使用;这样上面的die宏
    可以被做得更强大,更有用,只要把第一行定义改为如下形式即可:

    %macro die 0-1+ "Painful program death has occurred.",13,10

    最大参数个数可以是无限,以*表示.在这种情况下,当然就不可能提供所有
    的缺省参数值. 关于这种用法的例子参见4.3.6.

    4.3.5 `%0: 宏参数个数计数器.

    对于一个可带有可变个数参数的宏, 参数引用%0会返回一个数值常量表示
    有多少个参数传给了宏.这可以作为%rep的一个参数(参阅4.5),以用来遍历
    宏的所有参数. 例子在4.3.6中给出.

    4.3.6 `%rotate: 循环移动宏参数.

    Unix的shell程序员对于shift shell命令再熟悉不过了,它允许把传递给shell
    脚本的参数序列(以$1,$2等引用)左移一个,所以, 前一个参数是‘$1的话
    左移之后,就变成’$2可用了,而在$1之前是没有可用的参数的。

    NASM具有相似的机制,使用%rotate。就象这个指令的名字所表达的,它跟Unix
    的shift是不同的,它不会让任何一个参数丢失,当一个参数被移到最左边的
    时候,再移动它,它就会跳到右边。

    %rotate以单个数值作为参数进行调用(也可以是一个表达式)。宏参数被循环
    左移,左移的次数正好是这个数字所指定的。如果%rotate的参数是负数,那么
    宏参数就会被循环右移。

    所以,一对用来保存和恢复寄存器值的宏可以这样写:

    %macro multipush 1-*

    %rep %0
    push %1
    %rotate 1
    %endrep

    %endmacro

    这个宏从左到右为它的每一个参数都依次调用指令PUSH。它开始先把它的
    第一个参数%1压栈,然后调用%rotate把所有参数循环左移一个位置,这样
    一来,原来的第二个参数现在就可以用%1来取用了。重复执行这个过程,
    直到所有的参数都被执行完(这是通过把%0作为%rep的参数来实现的)。
    这就实现了把每一个参数都依次压栈。

    注意,*也可以作为最大参数个数的一个计数,表明你在使用宏multipush的
    时候,参数个数没有上限。

    使用这个宏,确实是非常方便的,执行同等的POP操作,我们并不需要把参数
    顺序倒一下。一个完美的解决方案是,你再写一个multipop宏调用,然后把
    上面的调用中的参数复制粘贴过来就行了,这个宏会对所有的寄存器执行相反
    顺序的pop操作。

    这可以通过下面定义来实现:

    %macro multipop 1-*

    %rep %0
    %rotate -1
    pop %1
    %endrep

    %endmacro

    这个宏开始先把它的参数循环右移一个位置,这样一来,原来的最后一个参数
    现在可以用%1引用了。然后被pop,然后,参数序列再一次右移,倒数第二个
    参数变成了%1,就这样,所以参数被以相反的顺序一一被执行。

    4.3.7 连结宏参数。

    NASM可以把宏参数连接到其他的文本中。这个特性可以让你声明一个系例
    的符号,比如,在宏定义中。你希望产生一个关于关键代码的表格,而代码
    跟在表中的偏移值有关。你可以这样编写代码:

    %macro keytab_entry 2

    keypos%1 equ $-keytab
    db %2

    %endmacro

    keytab:
    keytab_entry F1,128+1
    keytab_entry F2,128+2
    keytab_entry Return,13

    会被扩展成:

    keytab:
    keyposF1 equ $-keytab
    db 128+1
    keyposF2 equ $-keytab
    db 128+2
    keyposReturn equ $-keytab
    db 13

    你可以很轻易地把文本连接到一个宏参数的尾部,这样写即可:%1foo。

    如果你希望给宏参数加上一个数字,比如,通过传递参数foo来定义符
    号foo1和foo2,但你不能写成%11,因为这会被认为是第11个参数。
    你必须写成%{1}1,它会把第一个1跟第二个分开

    这个连结特性还可以用于其他预处理问题中,比如macro-local labels(4.3.2)
    和context-local labels(4.7.2)。在所有的情况中,语法上的含糊不清都可以
    通过把%之后,文本之前的部分放在一个括号中得到解决:所以%{%foo}bar
    会把文本bar连接到一个macro-local label:’%%foo的真正名字的后面(这
    个是不必要的,因为就NASM处理macro-local labels的机制来讲,%{%foo}bar
    和%%foobar都会被扩展成同样的形式,但不管怎么样,这个连结的能力是在的)

    4.3.8 条件代码作为宏参数。

    NASM对于含有条件代码的宏参数会作出特殊处理。你可以以另一种形式
    %+1来使用宏参数引用%1,它告诉NASM这个宏参数含有一个条件代码,
    如果你调用这个宏时,参数中没有有效的条件代码,会使预处理器报错。

    为了让这个特性更有用,你可以以%-1的形式来使用参数,它会让NASM把
    这个条件代码扩展成它的反面。所以4.3.2中定义的宏retz还可以以
    下面的方式重写:

    %macro retc 1

    j%-1 %%skip
    ret
    %%skip:

    %endmacro

    这个指令可以使用retc ne来进行调用,它会把条件跳转指令扩展成JE,
    或者retc po会把它扩展成JPE。

    %+1的宏参数引用可以很好的把参数CXZ和ECXZ解释为有效的条件
    代码;但是,%-1碰上上述的参数就会报错,因为这两个条件代码没有相
    反的情况存在。

    4.3.9 禁止列表扩展。

    当NASM为你的源程序产生列表文件的时候,它会在宏调用的地方为你展开
    多行宏,然后列出展开后的所有行。这可以让你看到宏中的哪些指令展
    开成了哪些代码;尽管如此,有些不必要的宏展开会把列表弄得很混乱。

    NASM为此提供了一个限定符.nolist,它可以被包含在一个宏定义中,这
    样,这个宏就不会在列表文件中被展开。限定符.nolist直接放到参数
    的后面,就像下面这样:

    %macro foo 1.nolist

    或者这样:

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

    4.4 条件汇编

    跟C预处理器相似,NASM允许对一段源代码只在某特定条件满足时进行汇编,
    关于这个特性的语法就像下面所描述的:

    %if
    ;if 满足时接下来的代码被汇编。
    %elif
    ; 当if不满足,而满足时,该段代码被汇编。
    %else
    ;当都不满足时,该段代码被汇编。
    %endif

    %else跟%elif子句都是可选的,你也可以使用多于一个的%elif子句。

    4.4.1 `%ifdef: 测试单行宏是否存在。

    %ifdef MACRO可以用来开始一个条件汇编块,跟在它后面的代码当且仅
    当一个叫做MACRO单行宏被定义时才会被会汇编。如果没有定义,那么
    %elif和%else块会被处理。

    比如,当调试一个程序时,你可能希望这样写代码:

    ; perform some function
    %ifdef DEBUG
    writefile 2,"Function performed successfully",13,10
    %endif
    ; go and do something else

    你可以通过使用命令行选项-dDEBUG来建立一个处理调试信息的程序,或
    不使用该选项来产生最终发布的程序。

    你也可以测试一个宏是否没有被定义,这可以使用%ifndef。你也可以在
    %elif块中测试宏定义,使用%elifdef和%elifndef即可。

    4.4.2 `ifmacro: 测试多行宏是否存在。

    除了是测试多行宏的存在的,%idmacro操作符的工作方式跟%ifdef是一
    样的。

    比如,你可能在编写一个很大的工程,而且无法控制存在链接库中的宏。你
    可能需要建立一个宏,但必须先确保这个宏没有被建立过,如果被建立过了,
    你需要为你的宏换一个名字。

    如果你定义的一个有特定参数个数与宏名的宏与现有的宏会产生冲突,那么
    %ifmacro会返回真。比如:

    %ifmacro MyMacro 1-3

    %error "MyMacro 1-3" causes a conflict with an existing macro.

    %else

    %macro MyMacro 1-3

    ; insert code to define the macro

    %endmacro

    %endif

    如果没有现有的宏会产生冲突,这会建立一个叫MyMacro 1-3"的宏,如果
    会有冲突,那么就产生一条警告信息。

    你可以通过使用%ifnmacro来测试是否宏不存在。还可以使用%elifmacro
    和%elifnmacro在%elif块中测试多行宏。

    4.4.3 `%ifctx: 测试上下文栈。

    当且仅当预处理器的上下文栈中的顶部的上下文的名字是ctxname时,条
    件汇编指令%ifctx ctxname会让接下来的语句被汇编。跟%ifdef一样,
    它也有%ifnctx,%elifctx,%elifnctx等形式。

    关于上下文栈的更多细节,参阅4.7, 关于%ifctx的一个例子,参阅4.7.5.

    4.4.4 `%if: 测试任意数值表达式。

    当且仅当数值表达式expr的值为非零时,条件汇编指令%if expr会让接
    下来的语句被汇编。使用这个特性可以确定何时中断一个%rep预处理器循
    环,例子参阅4.5。

    %if和%elif的表达式是一个临界表达式(参阅3.8)

    %if 扩展了常规的NASM表达式语法,提供了一组在常规表达式中不可用的
    相关操作符。操作符=,<,>,<=,>=和<>分别测试相等,小于,大
    于,小于等于,大于等于,不等于。跟C相似的形式==和!=作为=,<>
    的另一种形式也被支持。另外,低优先级的逻辑操作符&&,^^,和||作
    为逻辑与,逻辑异或,逻辑或也被支持。这些跟C的逻辑操作符类似(但C没
    有提供逻辑异或),这些逻辑操作符总是返回0或1,并且把任何非零输入看
    作1(所以,比如, ^^它会在它的一个输入是零,另一个非零的时候,总
    返回1)。这些操作符返回1作为真值,0作为假值。

    4.4.5 `%ifidn and `%ifidni: 测试文本相同。

    当且仅当text1和text2在作为单行宏展开后是完全相同的一段文本时,
    结构%ifidn text1,text2会让接下来的一段代码被汇编。两段文本在空格
    个数上的不同会被忽略。

    %ifidni和%ifidn相似,但是大小写不敏感。

    比如,下面的宏把一个寄存器或数字压栈,并允许你把IP作为一个真实的寄
    存器使用:

    %macro pushparam 1

    %ifidni %1,ip
    call %%label
    %%label:
    %else
    push %1
    %endif

    %endmacro

    就像大多数的%if结构,%ifidn也有一个%elifidn,并有它的反面的形
    式%ifnidn,%elifnidn.相似的,%ifidni也有%elifidni,`%ifnidni
    和`%elifnidni。

    4.4.6 `%ifid, `%ifnum, `%ifstr: 测试记号的类型。

    有些宏会根据传给它们的是一个数字,字符串或标识符而执行不同的动作。
    比如,一个输出字符串的宏可能会希望能够处理传给它的字符串常数或一
    个指向已存在字符串的指针。

    当且仅当在参数列表的第一个记号存在且是一个标识符时,条件汇编指令
    %ifid会让接下来的一段代码被汇编。%ifnum相似。但测试记号是否是
    数字;%ifstr测试是否是字符串。

    比如,4.3.3中定义的宏writefile可以用%ifstr作进一步改进,如下:

    %macro writefile 2-3+

    %ifstr %2
    jmp %%endstr
    %if %0 = 3
    %%str: db %2,%3
    %else
    %%str: db %2
    %endif
    %%endstr: mov dx,%%str
    mov cx,%%endstr-%%str
    %else
    mov dx,%2
    mov cx,%3
    %endif
    mov bx,%1
    mov ah,0x40
    int 0x21

    %endmacro

    这个宏可以处理以下面两种方式进行的调用:

    writefile [file], strpointer, length
    writefile [file], "hello", 13, 10

    在第一种方式下,strpointer是作为一个已声明的字符串的地址,而
    length作为它的长度;第二种方式中,一个字符串被传给了宏,所以
    宏就自己声明它,并为它分配地址和长度。

    注意,%ifstr中的%if的使用方式:它首先检测宏是否被传递了两个参
    数(如果是这样,那么字符串就是一个单个的字符串常量,这样db %2就
    足够了)或者更多(这样情况下,除了前两个参数,后面的全部参数都要被
    合并到%3中,这就需要db %2,%3了。)

    常见的%elifXXX,%ifnXXX和%elifnXXX/版本在%ifid,%ifnum,和
    %ifstr中都是存在的。

    4.4.7 `%error: 报告用户自定义错误。

    预处理操作符%error会让NASM报告一个在汇编时产生的错误。所以,如果
    别的用户想要汇编你的源代码,你必须保证他们用下面的代码定义了正确的
    宏:

    %ifdef SOME_MACRO
    ; do some setup
    %elifdef SOME_OTHER_MACRO
    ; do some different setup
    %else
    %error Neither SOME_MACRO nor SOME_OTHER_MACRO was defined.
    %endif

    然后,任何不理解你的代码的用户都会被汇编时得到关于他们的错误的警告
    信息,不必等到程序在运行时再出现错误却不知道错在哪儿。

    4.5 预处理器循环: `%rep

    虽然NASM的TIMES前缀非常有用,但是不能用来作用于一个多行宏,因为
    它是在NASM已经展开了宏之后才被处理的。所以,NASM提供了另外一种形式
    的循环,这回是在预处理器级别的:%rep。

    操作符%rep和%endrep(%rep带有一个数值参数,可以是一个表达式;
    %endrep不带任何参数)可以用来包围一段代码,然后这段代码可以被复制
    多次,次数由预处理器指定。

    %assign i 0
    %rep 64
    inc word [table+2*i]
    %assign i i+1
    %endrep

    这段代码会产生连续的64个INC指令,从内存地址[table]一直增长到
    [table+126]。

    对于一个复杂的终止条件,或者想要从循环中break出来,你可以使用
    %exitrep操作符来终止循环,就像下面这样:

    fibonacci:
    %assign i 0
    %assign j 1
    %rep 100
    %if j > 65535
    %exitrep
    %endif
    dw j
    %assign k j+i
    %assign i j
    %assign j k
    %endrep

    fib_number equ ($-fibonacci)/2

    上面的代码产生所有16位的Fibonacci数。但要注意,循环的最大次数还是要
    作为一个参数传给%rep。这可以防止NASM预处理器进入一个无限循环。在
    多任务或多用户系统中,无限循环会导致内存被耗光或其他程序崩溃。

    4.6 包含其它文件。

    又一次使用到一个跟C预处理器语法极其相似的操作符,它可以让你在你的代
    码中包含其它源文件。这可以通过%include来实现:

    %include "macros.mac"

    这会把文件macros.mac文件中的内容包含到现在的源文件中。

    被包含文件会被在当前目录下寻找(就是你在运行NASM时所在的目录,并不是
    NASM可执行文件所在的目录或源程序文件所在的目录),你可以在NASM的命令行
    上使用选项-i来增加搜索路径。


    C语言中防止文件被重复包含的习惯做法在NASM中也适用:如果文件
    macros.mac中有如下形式的代码:

    %ifndef MACROS_MAC
    %define MACROS_MAC
    ; now define some macros
    %endif

    这样多次包含该文件就不会引起错误,因为第二次包含该文件时,什么
    也不会发生,因为宏MACROS_MAC已经被定义过了。

    在没用%include操作符包含一个文件时,你可以强制让这个文件被包含
    进来,做法是在NASM命令行上使用-p选项

    4.7 上下文栈。

    那些对一个宏定义来讲是本地的Labels有时候还不够强大:有时候,你需
    要能够在多个宏调用之间共享label。比如一个REPEAT...UNTIL循
    环,REPEAT宏的展开可能需要能够去引用UNTIL中定义的宏。而且在
    使用这样的宏时,你可能还会嵌套多层循环。

    NASM通过上下文栈提供这个层次上的功能。预处理器维护了一个包含上下
    文的栈,每一个上下文都有一个名字作为标识。你可以通过指令%push
    往上下文栈中加一个新的上下文,或通过%pop去掉一个。你可以定义一
    些只针对特定上下文来说是本地的labels。

    4.7.1 `%push and `%pop: 创建和删除上下文。

    %push操作符用来创建一个新的上下文,然后把它放在上下文栈的顶端。
    %push需要一个参数,它是这个上下文的名字,例如:

    %push foobar

    这会把一个新的叫做foobar的上下文放到栈顶。你可以在一个栈中拥有
    多个具有相同名字的上下文:它们之间仍旧是可以区分的。

    操作符%pop不需要参数,删除栈顶的上下文,并把它销毁,同时也删除
    跟它相关的labels。

    4.7.2 Context-Local Labels

    就像%%foo会定义一个对于它所在的那个宏来讲是本地的label一样,
    %$foo会定义一个对于当前栈顶的上下文来讲是本地的lable。所以,上
    文提到的REPEAT,UNTIL的例子可以以下面的方式实现:

    %macro repeat 0

    %push repeat
    %$begin:

    %endmacro

    %macro until 1

    j%-1 %$begin
    %pop

    %endmacro

    然后象下面这样使用它:

    mov cx,string
    repeat
    add cx,3
    scasb
    until e

    它会扫描每个字符串中的第四个字节,以查找在al中的字节。

    如果你需要定义,或存取对于不在栈顶的上下文本地的label,你可以使用
    %$$foo,或%$$$foo来存取栈下面的上下文。

    4.7.3 Context-Local单行宏。

    NASM也允许你定义对于一个特定的上下文是本地的单行宏,使用的方式大致
    相面:

    %define %$localmac 3

    这会定义一个对于栈顶的上下文本地的单行宏%$localmax,当然,在又一个
    %push操作之后,它还是可以通过%$$localmac来存取。

    4.7.4 `%repl: 对一个上下文改名。

    如果你需要改变一个栈顶上下文的名字(比如,为了响应%ifctx),你可以
    在%pop之后紧接着一个%push;但它会产生负面效应,会破坏所有的跟栈
    顶上下文相关的context-local labels和宏。

    NASM提供了一个操作符%repl,它可以在不影响相关的宏与labels的情况下,
    为一个上下文换一个名字,所以你可以把下面的破坏性代码替换成另一种形
    式:

    %pop
    %push newname

    换成不具破坏性的版本: `%repl newname.

    4.7.5 使用上下文栈的例子: Block IFs

    这个例子几乎使用了所有的上下文栈的特性,包括条件汇编结构%ifctx,
    它把一个块IF语句作为一套宏来执行:

    %macro if 1

    %push if
    j%-1 %$ifnot

    %endmacro

    %macro else 0

    %ifctx if
    %repl else
    jmp %$ifend
    %$ifnot:
    %else
    %error "expected `if before `else"
    %endif

    %endmacro

    %macro endif 0

    %ifctx if
    %$ifnot:
    %pop
    %elifctx else
    %$ifend:
    %pop
    %else
    %error "expected `if or `else before `endif"
    %endif

    %endmacro

    这段代码看上去比上面的`REPEAT和`UNTIL宏要饱满多了。因为它使用了
    条件汇编去验证宏以正确的顺序被执行(比如,不能在if之间调用endif
    )如果出现错误,执行%error。

    另外,endif宏要处理两种不同的情况,即它可能直接跟在if后面,也
    可能跟在else后面。它也是通过条件汇编,判断上下文栈的栈顶是if还
    是else,并据此来执行不同的动作。

    else宏必须把上下文保存到栈中,好让if宏跟endif宏中定义的
    %$ifnot引用。但必须改变上下文的名字,这样endif就可以知道这中间
    还有一个else。这是通过%repl来做这件事情的。

    下面是一个使用这些宏的例子:

    cmp ax,bx

    if ae
    cmp bx,cx

    if ae
    mov ax,cx
    else
    mov ax,bx
    endif

    else
    cmp ax,cx

    if ae
    mov ax,cx
    endif

    endif

    通过把在内层if中描述的另一个上下文压栈,放在外层if中的上下文
    的上面,这样,else和endif总能引用到匹配的if或else。这个
    块-IF宏处理嵌套的能力相当好,

    4.8 标准宏。

    NASM定义了一套标准宏,当开始处理源文件时,这些宏都已经被定义了。
    如果你真的希望一个程序在执行前没有预定义的宏存在,你可以使用
    %clear操作符清空预处理器的一切。

    大多数用户级的操作符(第五章)是作为宏来运行的,这些宏进一步调用原
    始的操作符;这些在第五章介绍。剩余的标准宏在这里进行描述。

    4.8.1 `__NASM_MAJOR__, `__NASM_MINOR__, `__NASM_SUBMINOR__和
    `___NASM_PATCHLEVEL__: NASM版本宏。

    单行宏`__NASM_MAJOR__, `__NASM_MINOR__,`__NASM_SUBMINOR__和
    `___NASM_PATCHLEVEL__被展开成当前使用的NASM的主版本号,次版本号,
    子次版本号和补丁级。所在,在NASM 0.98.32p1版本中,`__NASM_MAJOR__
    被展开成0,`__NASM_MINOR__被展开成98,`__NASM_SUBMINOR__被展开成
    32,`___NASM_PATCHLEVEL__被定义为1。

    4.8.2 `__NASM_VERSION_ID__: NASM版本ID。

    单行宏`__NASM_VERSION_ID__被展开成双字整型数,代表当前使用的版本
    的NASM的全版本数。这个值等于把`__NASM_MAJOR__,`__NASM_MINOR__,
    `__NASM_SUBMINOR__和`___NASM_PATCHLEVEL__连结起来产生一个单个的
    双字长整型数。所以,对于0.98.32p1,返回值会等于

    dd 0x00622001

    或者

    db 1,32,98,0

    注意,上面两行代码产生的是完全相同的代码,第二行只是用来指出内存
    中存在的各个值之间的顺序。

    4.8.3 `__NASM_VER__: NASM版本字符串。

    单行宏`__NASM_VER__被展开成一个字符串,它定义了当前使用的NASM的
    版本号。所以,在NASM0.98.32下:

    db __NASM_VER__

    会被展开成:

    db "0.98.32"

    4.8.4 `__FILE__ and `__LINE__: 文件名和行号。

    就像C的预处理器,NASM允许用户找到包含有当前指令的文件的文件名和行
    数。宏__FILE__展开成一个字符串常量,该常量给出当前输入文件的文件
    名(如果含有%include操作符,这个值会在汇编的过程中改变),而
    `__LINE__会被展开成一个数值常量,给出在输入文件中的当前行的行号。

    这些宏可以使用在宏中,以查看调试信息,当在一个宏定义中包含宏
    __LINE__时(不管 是单行还是多行),会返回宏调用,而不是宏定义处
    的行号。这可以用来确定是否在一段代码中发生了程序崩溃。比如,某人
    可以编写一个子过程stillhere,它通过EAX传递一个行号,然后输出一
    些信息,比如:"line 155: still here。你可以这样编写宏:

    %macro notdeadyet 0

    push eax
    mov eax,__LINE__
    call stillhere
    pop eax

    %endmacro

    然后,在你的代码中插入宏调用,直到你发现发生错误的代码为止。

    4.8.5 `STRUC and `ENDSTRUC: 声明一个结构体数据类型。

    在NASM的内部,没有真正意义上的定义结构体数据类型的机制; 取代它的
    是,预处理器的功能相当强大,可以把结构体数据类型以一套宏的形式来
    运行。宏 ‘STRUCT’ 和ENDSTRUC是用来定义一个结构体数据类型的。

    ‘STRUCT’带有一个参数,它是结构体的名字。这个名字代表结构体本身,它
    在结构体内的偏移地址为零,名字加上一个_size后缀组成的符号,用一个
    EQU给它赋上结构体的大小。一旦‘STRUC被执行,你就开始在定义一个
    结构体,你可以用RESB类伪指令定义结构体的域,然后使用ENDSTRUC来
    结束定义。

    比如,定义一个叫做mytype的结构体,包含一个longword,一个word,一个
    byte,和一个字符串,你可以这样写代码:

    struc mytype

    mt_long: resd 1
    mt_word: resw 1
    mt_byte: resb 1
    mt_str: resb 32

    endstruc

    上面的代码定义了六个符号:m_long在地置0(从结构体mytype开头开始
    到这个longword域的偏移),`mt_word在地置4, `mt_byte6, `mt_str7,
    `mytype_size是39,而`mytype 自己在地置0

    之所以要把结构体的名字定义在地址零处,是因为要让结构体可以使用本地
    labels机制的缘故:如果你想要在多个结构体中使用具有同样名字的成员,
    你可以把上面的结构体定义成这个样子:

    struc mytype

    .long: resd 1
    .word: resw 1
    .byte: resb 1
    .str: resb 32

    endstruc

    在这个定义中,把结构体域的偏移值定义成了:mytype.long,
    `mytype.word, `mytype.byte and `mytype.str.

    NASM因此而没有内部的结构体支持,也不支持以句点形式引用结构体中的成
    员,所以代码mov ax, [mystruc.mt_word]’是非法的,mt_word是一个常数,
    就像其它类型的常数一样,所以,正确的语法应该是
    mov ax,[mystruc+mt_word]或者`mov ax,[mystruc+mytype.word].

    4.8.6 `ISTRUC, `AT and `IEND: 声明结构体的一个实例。

    定义了一个结构体类型以后,你下一步要做的事情往往就是在你的数据段
    中声明一个结构体的实例。NASM通过使用ISTRUC机制提供一种非常简单
    的方式。在程序中声明一个mytype结构体,你可以象下面这样写代码:

    mystruc:
    istruc mytype

    at mt_long, dd 123456
    at mt_word, dw 1024
    at mt_byte, db x
    at mt_str, db hello, world, 13, 10, 0

    iend

    ‘AT’宏的功能是通过使用TIMES前缀把偏移位置定位到正确的结构体域上,
    然后,声明一个特定的数据。所以,结构体域必须以在结构体定义中相同的
    顺序被声明。

    如果为结构体的域赋值要多于一行,那接下的内容可直接跟在AT行后面,比
    如:

    at mt_str, db 123,134,145,156,167,178,189
    db 190,100,0

    按个人的喜好不同,你也可以不在AT行上写数据,而直接在第二行开始写
    数据域:

    at mt_str
    db hello, world
    db 13,10,0

    4.8.7 `ALIGN and `ALIGNB: 数据对齐

    宏ALIGN和ALIGNB提供一种便捷的方式来进行数据或代码的在字,双字,段
    或其他边界上的对齐(有些汇编器把这两个宏叫做EVEN),有关这两个宏的语法
    是:

    align 4 ; align on 4-byte boundary
    align 16 ; align on 16-byte boundary
    align 8,db 0 ; pad with 0s rather than NOPs
    align 4,resb 1 ; align to 4 in the BSS
    alignb 4 ; equivalent to previous line

    这两个个参数都要求它们的第一个参数是2的幂;它们都会计算需要多少字节来
    来存储当前段,当然这个字节数必须向上对齐到一个2的幂值。然后用它们的第
    二个参数来执行TIMES前缀进行对齐。

    如果第二个参数没有被指定,那ALIGN的缺省值就是NOP,而ALIGNB的缺省
    值就是RESB 1.当第二个参数被指定时,这两个宏是等效的。通常,你可以在
    数据段与代码段中使用ALIGN,而在BSS段中使用ALIGNB,除非有特殊用途,一
    般你不需要第二个参数。

    作为两个简单的宏,ALIGN与ALIGNB不执行错误检查:如果它们的第一个参
    数不是2的某次方,或它们的第二个参数大于一个字节的代码,他们都不会有警
    告信息,这两种情况下,它们都会执行错误。

    ALIGNB(或者,ALIGN带上第二个参数RESB 1)可以用在结构体的定义中:

    struc mytype2

    mt_byte:
    resb 1
    alignb 2
    mt_word:
    resw 1
    alignb 4
    mt_long:
    resd 1
    mt_str:
    resb 32

    endstruc

    这可以保证结构体的成员被对齐到跟结构体的基地址之间有一个正确的偏移值。

    最后需要注意的是,ALIGN和ALIGNB都是以段的开始地址作为参考的,而
    不是整个可执行程序的地址空间。如果你所在的段只能保证对齐到4字节的边
    界,那它会对齐对16字节的边界,这会造成浪费,另外,NASM不会检测段的对
    齐特性是否可被ALIGN和ALIGNB使用。

    4.9 TASM兼容预处理指令。

    接下来的预处理操作符只有在用-t命令行开关把TASM兼容模式打开的情况下
    才可以使用(这个开关在2.1.16介绍过)

    (*) `%arg (见4.9.1节)

    (*) `%stacksize (见4.9.2节)

    (*) `%local (见4.9.3节)

    4.9.1 `%arg操作符

    %arg操作符用来简化栈上的参数传递操作处理。基于栈的参数传递在很多高
    级语言中被使用,包括C,C++和Pascal。

    而NASM企图通过宏来实现这种功能(参阅7.4.5),它的语法使用上不是很舒服,
    而且跟TASM之间是不兼容的。这里有一个例子,展示了只通过宏%arg来处理:

    some_function:

    %push mycontext ; save the current context
    %stacksize large ; tell NASM to use bp
    %arg i:word, j_ptr:word

    mov ax,[i]
    mov bx,[j_ptr]
    add ax,[bx]
    ret

    %pop ; restore original context

    这跟在7.4.5中定义的过程很相似,把j_ptr指向的值加到i中,然后把相加的结
    果在AX中返回,对于push和pop的展开请参阅4.7.1关于上下文栈的使用。

    4.9.2 `%stacksize指令。

    %stacksize指令是跟%arg和%local指令结合起来使用的。它告诉NASM
    为%arg和%local使用的缺省大小。%stacksize指令带有一个参数,它
    是flat,large或small。

    %stacksize flat

    这种形式将使NASM使用相对于ebp的基于栈的参数地址。它假设使用一个
    近调用来得到这个参数表。(比如,eip被压栈).

    %stacksize large

    而这种形式会用bp来进行基于栈的参数寻址,假设使用了一个远调用来
    获得这个地址(比如,ip和cs都会被压栈)。

    %stacksize small

    这种形式也使用bp来进行基于栈的参数寻址,但它跟large不同,因为
    他假设bp的旧值已经被压栈。换句话说,你假设bp,ip和cs正在栈顶,在它们
    下面的所有本地空间已经被ENTER指令开辟好了。当和%local指令结合的
    时候,这种形式特别有用。

    4.9.3 `%local指令。

    %local指令用来简化在栈框架中进行本地临时栈变量的分配。C语言中的自
    动本地变量是这种类型变量的一个例子。%local指令跟%stacksize一起
    使用的时候特别有用。并和%arg指令保持兼容。它也允许简化对于那些用
    ENTER指令分配在栈中的变量的引用(关于ENTER指令,请参况B.4.65)。这
    里有一个关于它们的使用的例子:

    silly_swap:

    %push mycontext ; save the current context
    %stacksize small ; tell NASM to use bp
    %assign %$localsize 0 ; see text for explanation
    %local old_ax:word, old_dx:word

    enter %$localsize,0 ; see text for explanation
    mov [old_ax],ax ; swap ax & bx
    mov [old_dx],dx ; and swap dx & cx
    mov ax,bx
    mov dx,cx
    mov bx,[old_ax]
    mov cx,[old_dx]
    leave ; restore old bp
    ret ;

    %pop ; restore original context

    变量%$localsize是在%local的内部使用,而且必须在%local指令使用前,
    被定义在当前的上下文中。不这样做,在每一个%local变量声明的地方会引
    发一个表达式语法错误。它然后可以用在一条适当的ENTER指令中。

    4.10 其他的预处理指令。

    NASM还有一些预处理指令允许从外部源中获取信息,现在,他们包括:

    下面的预处理指令使NASM能正确地自理C++/C语言预处理器的输出。

    (*) `%line 使NASM能正确地自理C++/C语言预处理器的输出。(参阅4.10.1)

    (*) `%! 使NASM从一个环境变量中读取信息,然后这些信息就可以在你的程序
    中使用了。(4.10.2)

    4.10.1 `%line操作符。

    %line操作符被用来通知NASM,输入行与另一个文件中指定的行号相关。一般
    这另一个文件会是一个源程序文件,它作为现在NASM的输入,但它是一个预处理
    器的输出。%line指令允许NASM输出关于在这个源程序文件中的指定行号的信
    息,而不是被NASM读进来的整个文件。

    这个预处理指令通常不会被程序员用到,但会让预处理器的作者感兴趣,%line
    的使用方法如下:

    %line nnn[+mmm] [filename]

    在这个指令中,nnn指定源程序文件中与之相关的特定行,mmm是一个可选的
    参数,它指定一个行递增的值;每一个被读进来的源文件行被认为与源程序文件
    中的mmm行相关。最终,filename可选参数指定源程序文件的文件名。

    在读到一条%line预处理指令后,NASM会报告与指定的值相关的所有的文件名和
    行号

    4.10.2 `%!`: 读取一个环境变量。

    `%! 操作符可以在汇编时读取一个环境变量的值,这可以用在一个环境变量
    的内容保存到一个字符串中。该字符串可以用在你程序的其他地方。

    比如,假设你有一个环境变量FOO,你希望把FOO的值嵌入到你的程序中去。你可
    以这样做:

    %define FOO %!FOO
    %define quote

    tmpstr db quote FOO quote

    在写的时候,在定义quote时,它会产生一个没有结束的字符串的警告信息,
    它会自己在读进来的字符串的前后加上一个空格。我没有办法找到一个简单的
    工作方式(尽管可以通过宏来创建),我认为,你没有必要学习创建更为复杂
    的宏,或者如果你用这种方式使用这个特性,你没有必要使用额外的空间。

    第五章: 汇编器指令。
    -------------------------------

    尽管NASM极力避免MASN和TASM中的那些庸肿复杂的东西,但还是不得不支持少
    量的指令,这些指令在本章进行描述。

    NASM的指令有两种类型:用户级指令和原始指令。一般地,每一条指令都有一
    个用户级形式和原始形式。在大多数情况下,我们推荐用户使用有户级指令,
    它们以宏的形式运行,并去调用原始形式的指令。

    原始指令被包含在一个方括号中;用户级指令没有括号。

    除了本章所描述的这些通用的指令,每一种目标文件格式为了控制文件格式
    的一些特性,可以使用一些另外的指令。这些格式相关的指令在第六章中跟
    相关的文件格式一起进行介绍。

    5.1 `BITS: 指定目标处理器模式。

    BITS指令指定NASM产生的代码是被设计运行在16位模式的处理器上还是运行
    在32位模式的处理器上。语法是BITS 16或BITS 32

    大多数情况下,你可能不需要显式地指定BITS。aout,coff,elf和
    win32目标文件格式都是被设计用在32位操作系统上的,它们会让NASM缺
    省选择32位模式。而obj目标文件格式允许你为每一个段指定USE16或
    USE32,然后NASM就会按你的指定设定操作模式,所以多次使用BITS是
    没有必要的。

    最有可能使用BITS的场合是在一个纯二进制文件中使用32位代码;这是因
    为bin输出格式在作为DOS的.COM程序,DOS的.SYS设备驱动程序,或引
    导程序时,默认都是16位模式。

    如果你仅仅是为了在16位的DOS程序中使用32位指令,你不必指定BITS 32,
    如果你这样做了,汇编器反而会产生错误的代码,因为这样它会产生运行在
    16位模式下,却以32位平台为目标的代码。

    当NASM在BITS 16状态下时,使用32位数据的指令可以加一个字节的前缀
    0x66,要使用32位的地址,可以加上0x67前缀。在BITS 32状态下,相反的
    情况成立,32位指令不需要前缀,而使用16位数据的指令需要0x66前缀,使
    用16位地址的指令需要0x67前缀。

    BITS指令拥有一个等效的原始形式:[BITS 16]和[BITS 32]。而用户级的
    形式只是一个仅仅调用原始形式的宏。

    5.1.1 `USE16 & `USE32: BITS的别名。

    USE16和USE32指令可以用来取代BITS 16和BITS 32,这是为了和其他
    汇编器保持兼容性。

    5.2 `SECTION或`SEGMENT: 改变和定义段。

    SECTION指令(SEGMENT跟它完全等效)改变你正编写的代码将被汇编进的段。
    在某些目标文件格式中,段的数量与名称是确定的;而在别一些格式中,用户
    可以建立任意多的段。因此,如果你企图切换到一个不存在的段,SECTION有
    时可能会给出错误信息,或者定义出一个新段,

    Unix的目标文件格式和bin目标文件格式,都支持标准的段.text,.data
    和bss段,与之不同的的,obj格式不能辩识上面的段名,并需要把段名开
    头的句点去掉。

    5.2.1 宏 `__SECT__

    SECTION指令跟一般指令有所不同,的用户级形式跟它的原始形式在功能上有
    所不同,原始形式[SECTION xyz],简单地切换到给出的目标段。用户级形式,
    SECTION xyz先定义一个单行宏__SECT__,定义为原始形式[SECTION],这正
    是要执行的指令,然后执行它。所以,用户级指令:

    SECTION .text

    被展开成两行:

    %define __SECT__ [SECTION .text]
    [SECTION .text]

    用户会发现在他们自己的宏中,这是非常有用的。比如,4.3.3中定义的宏
    writefile以下面的更为精致的写法会更有用:

    %macro writefile 2+

    [section .data]

    %%str: db %2
    %%endstr:

    __SECT__

    mov dx,%%str
    mov cx,%%endstr-%%str
    mov bx,%1
    mov ah,0x40
    int 0x21

    %endmacro

    这个形式的宏,一次传递一个用出输出的字符串,先用原始形式的SECTION切
    换至临时的数据段,这样就不会破会宏__SECT__。然后它把它的字符串声明在
    数据段中,然后调用__SECT__切换加用户先前所在的段。这样就可以避免先前
    版本的writefile宏中的用来跳过数据的JMP指令,而且在一个更为复杂的格
    式模型中也不会失败,用户可以把这个宏放在任何独立的代码段中进行汇编。

    5.3 `ABSOLUTE: 定义绝对labels。

    ABSOLUTE操作符可以被认为是SECTION的另一种形式:它会让接下来的代码不
    在任何的物理段中,而是在一个从给定地址开始的假想段中。在这种模式中,你
    唯一能使用的指令是RESB类指令。

    `ABSOLUTE可以象下面这样使用:

    absolute 0x1A

    kbuf_chr resw 1
    kbuf_free resw 1
    kbuf resw 16

    这个例子描述了一个关于在段地址0x40处的PC BIOS数据域的段,上面的代码把
    kbuf_chr定义在0x1A处,kbuf_free定义在地址0x1C处,kbuf定义在地址
    0x1E。

    就像SECTION一样,用户级的ABSOLUTE在执行时会重定义__SECT__宏。

    STRUC和ENDSTRUC被定义成使用ABSOLUTE的宏(同时也使用了__SECT__)

    ABSOLUTE不一定需要带有一个绝对常量作为参数:它也可以带有一个表达式(
    实际上是一个临界表达式,参阅3.8),表达式的值可以是在一个段中。比如,一
    个TSR程序可以在用它重用它的设置代码所占的空间:

    org 100h ; its a .COM program

    jmp setup ; setup code comes last

    ; the resident part of the TSR goes here
    setup:
    ; now write the code that installs the TSR here

    absolute setup

    runtimevar1 resw 1
    runtimevar2 resd 20

    tsr_end:

    这会在setup段的开始处定义一些变量,所以,在setup运行完后,它所占用的内存
    空间可以被作为TSR的数据存储空莘而得到重用。符号tsr_end可以用来计算TSR
    程序所需占用空间的大小。

    5.4 `EXTERN: 从其他的模块中导入符中。

    EXTERN跟MASM的操作符EXTRN,C的关键字extern极其相似:它被用来声明一
    个符号,这个符号在当前模块中没有被定义,但被认为是定义在其他的模块中,但
    需要在当前模块中对它引用。不是所有的目标文件格式都支持外部变量的:bin文
    件格式就不行。

    EXTERN操作符可以带有任意多个参数,每一个都是一个符号名:

    extern _printf
    extern _sscanf,_fscanf

    有些目标文件格式为EXTERN提供了额外的特性。在所有情况下,要使用这些额外
    特性,必须在符号名后面加一个冒号,然后跟上目标文件格式相关的一些文字。比如
    obj文件格式允许你声明一个以外部组dgroup为段基址一个变量,可以象下面这样
    写:

    extern _variable:wrt dgroup

    原始形式的EXTERN跟用户级的形式有所不同,因为它只能带有一个参数:对于多个参
    数的支持是在预处理器级上的特性。

    你可以把同一个变量作为EXTERN声明多次:NASM会忽略掉第二次和后来声明的,只采
    用第一个。但你不能象声明其他变量一样声明一个EXTERN变量。

    5.5 `GLOBAL: 把符号导出到其他模块中。

    GLOBAL是EXTERN的对立面:如果一个模块声明一个EXTERN的符号,然后引用它,
    然后为了防止链接错误,另外某一个模块必须确实定义了该符号,然后把它声明为
    GLOBAL,有些汇编器使用名字PUBLIC。

    GLOBAL操作符所作用的符号必须在GLOBAL之后进行定义。

    GLOBAL使用跟EXTERN相同的语法,除了它所引用的符号必须在同一样模块中已经被
    定义过了,比如:

    global _main
    _main:
    ; some code

    就像EXTERN一样,GLOBAL允许目标格式文件通过冒号定义它们自己的扩展。比如
    elf目标文件格式可以让你指定全局数据是函数或数据。

    global hashlookup:function, hashtable:data

    就象EXTERN一样,原始形式的GLOBAL跟用户级的形式不同,仅能一次带有一个参


    5.6 `COMMON: 定义通用数据域。

    COMMON操作符被用来声明通用变量。一个通用变量很象一个在非初始化数据段中定义
    的全局变量。所以:

    common intvar 4

    功能上跟下面的代码相似:

    global intvar
    section .bss

    intvar resd 1

    不同点是如果多于一个的模块定义了相同的通用变量,在链接时,这些通用变量会被
    合并,然后,所有模块中的所有的对intvar的引用会指向同一片内存。

    就角GLOBAL和EXTERN,COMMON支持目标文件特定的扩展。比如,obj文件格式
    允许通用变量为NEAR或FAR,而elf格式允许你指定通用变量的对齐需要。

    common commvar 4:near ; works in OBJ
    common intarray 100:4 ; works in ELF: 4 byte aligned

    它的原始形式也只能带有一个参数。

    5.7 `CPU: 定义CPU相关。

    CPU指令限制只能运行特定CPU类型上的指令。

    选项如下:

    (*) `CPU 8086 只汇编8086的指令集。

    (*) `CPU 186 汇编80186及其以下的指令集。

    (*) `CPU 286 汇编80286及其以下的指令集。

    (*) `CPU 386 汇编80386及其以下的指令集。

    (*) `CPU 486 486指令集。

    (*) `CPU 586 Pentium指令集。

    (*) `CPU PENTIUM 同586。

    (*) `CPU 686 P6指令集。

    (*) `CPU PPRO 同686

    (*) `CPU P2 同686

    (*) `CPU P3 Pentium III and Katmai指令集。

    (*) `CPU KATMAI 同P3

    (*) `CPU P4 Pentium 4 (Willamette)指令集

    (*) `CPU WILLAMETTE 同P4

    (*) `CPU IA64 IA64 CPU (x86模式下)指令集

    所有选项都是大小写不敏感的,在指定CPU或更低一级CPU上的所有指令都会
    被选择。缺省情况下,所有指令都是可用的。

    第六章: 输出文件的格式。
    -------------------------

    NASM是一个可移植的汇编器,它被设计为可以在任何ANSI C编译器支持的平台
    上被编译,并可以产生在各种intel x86系例的操作系统上运行的代码。为了
    做到这一点,它拥有大量的可用的输出文件格式,使用命令行上的选项-f
    可以选择。每一种格式对于NASM的语法都有一定的扩展,关于这部分内容,
    本章将详细介绍。

    就象在2.1.1中所描述的,NASM基于输入文件的名字和你选择的输出文件的格
    式为你的输出文件选择一个缺省的名字。这是通过去掉源文件的扩展名(.asm
    或.s或者其他你使用的扩展名),然后代之以一个由输出文件格式决定的扩
    展名。这些输出格式相关的扩展名会在下面一一给出。

    6.1 `bin: 纯二进制格式输出。

    bin格式不产生目标文件:除了你编写的那些代码,它不在输出文件中产生
    任何东西。这种纯二进制格式的文件可以用在MS-DOS中:.COM可执行文件
    和.SYS设备驱动程序就是纯二进制格式的。纯二进制格式输出对于操作系
    统和引导程序开发也是很有用的。

    bin格式支持多个段名。关于NASM处理bin格式中的段的细节,请参阅
    6.1.3。

    使用bin格式会让NASM进入缺省的16位模式 (参阅5.1)。为了能在bin格
    式中使用32位代码,比如在操作系统的内核代码中。你必须显式地使用
    BITS 32操作符。

    bin没有缺省的输出文件扩展名:它只是把输入文件的扩展名去掉后作为
    输出文件的名字。这样,NASM在缺省模式下会把binprog.asm汇编成二进
    制文件binprog。

    6.1.1 `ORG: 二进制程序的起点位置。

    bin格式提供一个额外的操作符,这在第五章已经给出ORG.ORG的功能
    是指定程序被载入内存时,它的起始地址。

    比如,下面的代码会产生longword: `0x00000104:

    org 0x100
    dd label
    label:

    跟MASM兼容汇编器提供的ORG操作符不同,它们允许你在目标文件中跳转,
    并覆盖掉你已经产生的代码,而NASM的ORG就象它的字面意思“起点”所
    表示的,它的功能就是为所有内部的地址引用增加一个段内偏移值;它不允
    许MASM版本的org的任何其他功能。

    6.1.2 `bin对`SECTION操作符的扩展。

    bin输出格式扩展了SECTION(或者SEGMENT)操作符,允许你指定段的
    对齐请求。这是通过在段定义行的后面加上ALIGN限定符实现的。比如:

    section .data align=16

    它切换到段.data,并指定它必须对齐到16字节边界。

    ALIGN的参数指定了地址值的低位有多少位必须为零。这个对齐值必须为
    2的幂。

    6.1.3 `Multisection 支持BIN格式.

    bin格式允许使用多个段,这些段以一些特定的规则进行排列。

    (*) 任何在一个显式的SECTION操作符之前的代码都被缺省地加到.text
    段中。

    (*) 如果.text段中没有给出ORG语句,它被缺省地赋为ORG 0。

    (*) 显式地或隐式地含有ORG语句的段会以ORG指定的方式存放。代码
    前会填充0,以在输出文件中满足org指定的偏移。

    (*) 如果一个段内含有多个ORG语句,最后一条ORG语句会被运用到整
    个段中,不会影响段内多个部分以一定的顺序放到一起。

    (*) 没有ORG的段会被放到有ORG的段的后面,然后,它们就以第一次声
    明时的顺序被存放。

    (*) .data段不像.text段和.bss段那样,它不遵循任何规则,

    (*) .bss段会被放在所有其他段的后面。

    (*) 除非一个更高级别的对齐被指定,所有的段都被对齐到双字边界。

    (*) 段之间不可以交迭。

    6.2 `obj: 微软OMF目标文件

    obj文件格式(因为历史的原因,NASM叫它obj而不是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 ; get segment address of data
    mov ds,ax ; and move it into DS
    inc word [dvar] ; now this reference will work
    ret

    obj格式中也可以使用SEG和WRT操作符,所以你可以象下面这样编写代码:

    extern foo

    mov ax,seg foo ; get preferred segment of foo
    mov ds,ax
    mov ax,data ; a different segment
    mov es,ax
    mov ax,[ds:foo] ; this accesses `foo
    mov [es:foo wrt data],bx ; so does this

    6.2.1 `obj 对`SEGMENT操作符的扩展。

    obj输出格式扩展了SEGMENT(或SECTION)操作符,允许你指定段的多个
    属性。这是通过在段定义行的末尾添加额外的限定符来实现的,比如:

    segment code private align=16

    这定义了一个段code,但同时把它声明为一个私有段,同时,它所描述的
    这个部分必须被对齐到16字节边界。

    可用的限定符如下:

    (*) `PRIVATE, `PUBLIC, `COMMON和`STACK 指定段的联合特征。`PRIVATE
    段在连接时不和其他的段进行连接;PUBLIC和STACK段都会在连接时
    连接到一块儿;而COMMON‘段都会在同一个地址相互覆盖,而不会一接一
    个连接好。

    (*) 就象上面所描述的,ALIGN是用来指定段基址的低位有多少位必须为零,
    对齐的值必须以2的乘方的形式给出,从1到4096;实际上,真正被支持的
    值只有1,2,4,16,256和4096,所以如果你指定了8,它会自动向上对齐
    到16,32,64会对齐到128等等。注意,对齐到4096字节的边界是这种格式
    的PharLap扩展,可能所有的连接器都不支持。

    (*) CLASS可以用来指定段的类型;这个特性告诉连接器,具有相同class的
    段应该在输出文件中被放到相近的地址。class的名字可以是任何字。比如
    CLASS=CODE。

    (*) 就象`CLASS, `OVERLAY通过一个作为参数的字来指定,为那些有覆盖能力
    的连接器提供覆盖信息。

    (*) 段可以被声明为USE16或USE32,这种选择会对目标文件产生影响,同时
    在段内16位或32位代码分开的时候,也能保证NASM的缺省汇编模式

    (*) 当编写OS/2目标文件的时候,你应当把32位的段声明为FLAT,它会使缺省
    的段基址进入一个特殊的组FLAT,同时,在这个组不存在的时候,定义这
    个组。

    (*) obj文件格式也允许段在声明的时候,前面有一个定义的绝对段地址,尽管没
    有连接器知道这个特性应该怎么使用;但如果你需要的话,NASM还是允许你
    声明一个段如下面形式:`SEGMENT SCREEN ABSOLUTE=0xB800`ABSOLUTE和
    `ALIGN关键字是互斥的。

    NASM的缺省段属性是`PUBLIC, `ALIGN=1, 没有class,没有覆盖, 并 `USE16.

    6.2.2 `GROUP: 定义段组。

    obj格式也允许段被分组,所以一个单独的段寄存器可以被用来引用一个组中的
    所有段。NASM因此提供了GROUP操作符,据此,你可以这样写代码:

    segment data

    ; some data

    segment bss

    ; some uninitialised data

    group dgroup data bss

    这会定义一个叫做dgroup的组,包含有段data和bss。就象SEGMENT,
    GROUP会把组名定义为一个符号,所以你可以使用var wrt data或者var
    wrt dgroup来引用data段中的变量var,具体用哪一个取决于哪一个段
    值在你的当前段寄存器中。

    如果你只是想引用var,同时,var被声明在一个段中,段本身是作为一个
    组的一部分,然后,NASM缺省给你的var的偏移值是从组的基地址开始的,
    而不是段基址。所以,SEG var会返回组基址而不是段基址。

    NASM也允许一个段同时作为多个组的一个部分,但如果你真这样做了,会产生
    一个警告信息。段内同时属于多个组的那些变量在缺省状况下会属于第一个被
    声明的包含它的组。

    一个组也不一定要包含有段;你还是可以使用WRT引用一个不在组中的变量。
    比如说,OS/2定义了一个特殊的组FLAT,它不包含段。

    6.2.3 `UPPERCASE: 在输出文件中使大小写敏感无效。

    尽管NASM自己是大小写敏感的,有些OMF连接器并不大小写敏感;所以,如果
    NASM能输出大小写单一的目标文件会很有用。UPPERCASE操作符让所有的写
    入到目标文件中的组,段,符号名全部强制为大写。在一个源文件中,NASM
    还是大小写敏感的;但目标文件可以按要求被整个写成是大写的。

    UPPERCASE写在单独一行中,不需要任何参数。

    6.2.4 `IMPORT: 导入DLL符号。

    如果你正在用NASM写一个DLL导入库,IMPORT操作符可以定义一个从DLL库中
    导入的符号,你使用IMPORT操作符的时候,你仍旧需要把符号声明为EXTERN.

    IMPORT操作符需要两个参数,以空格分隔,它们分别是你希望导入的符号的名
    称和你希望导入的符号所在的库的名称,比如:

    import WSAStartup wsock32.dll

    第三个参数是可选的,它是符号在你希望从中导入的链接库中的名字,这样的话,
    你导入到你的代码中的符号可以和库中的符号不同名,比如:

    import asyncsel wsock32.dll WSAAsyncSelect

    6.2.5 `EXPORT: 导出DLL符号.

    EXPORT也是一个目标格式相关的操作符,它定义一个全局符号,这个符号可以被
    作为一个DLL符号被导出,如果你用NASM写一个DLL库.你可以使用这个操作符,在
    使用中,你仍旧需要把符号定义为GLOBAL.

    EXPORT带有一个参数,它是你希望导出的在源文件中定义的符号的名字.第二个
    参数是可选的(跟第一个这间以空格分隔),它给出符号的外部名字,即你希望让使
    用这个DLL的应用程序引用这个符号时所用的名字.如果这个名字跟内部名字同名,
    可以不使用第二个参数.

    还有一些附加的参数,可以用来定义导出符号的一些属性.就像第二个参数, 这些
    参数也是以空格分隔.如果要给出这些参数,那么外部名字也必须被指定,即使它跟
    内部名字相同也不能省略,可用的属性如下:

    (*) resident表示某个导出符号在系统引导后一直常驻内存.这对于一些经常使用
    的导出符号来说,是很有用的.

    (*) `nodata表示导出符号是一个函数,这个函数不使用任何已经初始化过的数据.

    (*) `parm=NNN, 这里NNN是一个整型数,当符号是一个在32位段与16位段之间的
    调用门时,它用来设置参数的尺寸大小(占用多少个wrod).

    (*) 还有一个属性,它仅仅是一个数字,表示符号被导出时带有一个标识数字.

    比如:

    export myfunc
    export myfunc TheRealMoreformalLookingFunctionName
    export myfunc myfunc 1234 ; export by ordinal
    export myfunc myfunc resident parm=23 nodata

    6.2.6 `..start: 定义程序的入口点.

    OMF链接器要求被链接进来的所有目标文件中,必须有且只能有一个程序入口点,
    当程序被运行时,就从这个入口点开始.如果定义这个入口点的目标文件是用
    NASM汇编的,你可以通过在你希望的地方声明符号..start来指定入口点.

    6.2.7 `obj对`EXTERN操作符的扩展.

    如果你以下面的方式声明了一个外部符号:

    extern foo

    然后以这样的方式引用mov ax,foo,这样只会得到一个关于foo的偏移地址,而且
    这个偏移地址是以foo的首选段基址为参考的(在foo被定义的这个模块中指定
    的段).所以,为了存取foo的内容,你实际上需要这样做:

    mov ax,seg foo ; get preferred segment base
    mov es,ax ; move it into ES
    mov ax,[es:foo] ; and use offset `foo from it

    这种方式显得稍稍有点笨拙,实际上如果你知道一个外部符号可以通过给定的段
    或组来进行存的话,假定组dgroup已经在DS寄存器中,你可以这样写代码:

    mov ax,[foo wrt dgroup]

    但是,如果你每次要存取foo的时候,都要打这么多字是一件很痛苦的事情;所以
    NASM允许你声明foo的另一种形式:

    extern foo:wrt dgroup

    这种形式让NASM假定foo的首选段基址是dgroup;所以,表达式seg foo现在会
    返回dgroup,表达式foo等同于foo wrt dgroup.

    缺省的WRT机制可以用来让外部符号跟你程序中的任何段或组相关联.他也可以被
    运用到通用变量上,参阅6.2.8.

    6.2.8 `obj对`COMMON操作符的扩展.

    obj格式允许通用变量为near或far;NASM允许你指定你的变量属于哪一类,语法如
    下:

    common nearvar 2:near ; `nearvar is a near common
    common farvar 10:far ; and `farvar is far

    Far通用变量可能会大于64Kb,所以OMF可以把它们声明为一定数量的指定的大小的元
    素.比如,10byte的far通用变量可以被声明为10个1byte的元素,5个2byte的元素,或
    2个5byte的元素,或1个10byte的元素.

    有些OMF链接器需要元素的size,同时需要变量的size,当在多个模块中声明通用变
    量时可以用来进行匹配.所以NASM必须允许你在你的far通用变量中指定元素的size.
    这可以通过下面的语法实现:

    common c_5by2 10:far 5 ; two five-byte elements
    common c_2by5 10:far 2 ; five two-byte elements

    如果元素的size没有被指定,缺省值是1.还有,如果元素size被指定了,那么far关键
    字就不需要了,因为只有far通用变量是有元素size的.所以上面的声明等同于:

    common c_5by2 10:5 ; two five-byte elements
    common c_2by5 10:2 ; five two-byte elements

    这种扩展的特性还有,obj中的COMMON操作符还可以象EXTERN那样支持缺省的
    WRT指定,你也可以这样声明:

    common foo 10:wrt dgroup
    common bar 16:far 2:wrt data
    common baz 24:wrt data:6

    6.3 `win32: 微软Win32目标文件

    win32输出格式产生微软win32目标文件,可以用来给微软连接器进行连接,比如
    Visual C++.注意Borland Win32编译器不使用这种格式,而是使用obj格式(参阅
    6.2)

    win32提供缺省的输出文件扩展名.obj.

    注意,尽管微软声称Win32目标文件遵循COFF标准(通用目标文件格式),但是微软
    的Win32编译器产生的目标文件和一些COFF连接器(比如DJGPP)并不兼容,反过来也
    一样.这是由一些PC相关的语义上的差异造成的. 使用NASM的coff输出格式,可以
    产生能让DJGPP使用的COFF文件; 而这种coff格式不能产生能让Win32连接器正确
    使用的代码.

    6.3.1 `win32对`SECTION的扩展.

    就象obj格式,win32允许你在SECTION操作符的行上指定附加的信息,以用来控
    制你声明的段的类型与属性.对于标准的段名.text,.data,和.bss,类型和属
    性是由NASM自动产生的,但是还是可以通过一些限定符来重新指定:

    可用的限定符如下:

    (*) code或者text,把一个段定义为一个代码段,这让这个段可读并可执行,但是
    不能写,同时也告诉连接器,段的类型是代码段.

    (*) data和bss定义一个数据段,类似code,数据段被标识为可读可写,但不可执
    行,data定义一个被初始化过的数据段,bss定义一个未初始化的数据段.

    (*) rdata声明一个初始化的数据段,它可读,但不能写.微软的编译器把它作为一
    个存放常量的地方.

    (*) info定义一个信息段,它不会被连接器放到可执行文件中去,但可以传递一些
    信息给连接器.比如,定义一个叫做.drectve信息段会让连接器把这个段内的
    内容解释为命令行选项.

    (*) align=跟上一个数字,就象在obj格式中一样,给出段的对齐请求.你最大可
    以指定64:Win32目标文件格式没有更大的段对齐值.如果对齐请求没有被显式
    指定,缺省情况下,对于代码段,是16byte对齐,对于只读数据段,是8byte对齐,对
    于数据段,是4byte对齐.而信息段缺省对齐是1byte(即没有对齐),所以对它来说,
    指定的数值没用.

    如果你没有指定上述的限定符,NASM的缺省假设是:

    section .text code align=16
    section .data data align=4
    section .rdata rdata align=8
    section .bss bss align=4

    任何其的段名都会跟.text一样被对待.

    6.4 `coff: 通用目标文件格式.

    coff输出类型产生COFF目标文件,可以被DJGPP用来连接.

    coff提供一个缺省的输出文件扩展名.o.

    coff格式支持跟win32同样的对于SECTION的扩展,除了align和info限
    定符不被支持.

    6.5 `elf: 可执行可连接格式目标文件.

    elf输出格式产生ELF32(可执行可连接格式)目标文件,这种格式用在Linux,
    Unix System V中,包括Solaris x86, UnixWare和SCO Unix. elf提供一个缺
    省的输出文件扩展名.o.

    6.5.1 `elf对`SECTION操作符的扩展.

    就象obj格式一样,elf允许你在SECTION操作符行上指定附加的信息,以控制
    你声明的段的类型与属性.对于标准的段名.text,.data,.bss,NASM都会产
    生缺省的类型与属性.但还是可以通过一些限定符与重新指定.

    可用的限定符如下:

    (*) alloc定义一个段,在程序运行时,这个段必须被载入内存中,noalloc正好
    相反,比如信息段,或注释段.

    (*) exec把段定义为在程序运行的时候必须有执行权限.noexec正好相反.

    (*) `write把段定义为在程序运行时必须可写,nowrite正好相反.

    (*) `progbits把段定义为在目标文件中必须有实际的内容,比如象普通的代码段
    与数据段,nobits正好相反,比如bss段.

    (*) `align=跟上一个数字,给出段的对齐请求.

    如果你没有指定上述的限定符信息,NASM缺省指定的如下:

    section .text progbits alloc exec nowrite align=16
    section .rodata progbits alloc noexec nowrite align=4
    section .data progbits alloc noexec write align=4
    section .bss nobits alloc noexec write align=4
    section other progbits alloc noexec nowrite align=1

    (任何不在上述列举范围内的段,在缺省状况下,都被作为other段看待).

    6.5.2 地址无关代码: `elf特定的符号和 `WRT

    ELF规范含有足够的特性以允许写地址无关(PIC)的代码,这可以让ELF非常
    方便地共享库.尽管如此,这也意味着NASM如果想要成为一个能够写PIC的汇
    编器的话,必须能够在ELF目标文件中产生各种奇怪的重定位信息,

    因为ELF不支持基于段的地址引用,WRT操作符不象它的常规方式那样被
    使用,所以,NASM的elf输出格式中,对于WRT有特殊的使用目的,叫做:
    PIC相关的重定位类型.

    elf定义五个特殊的符号,它们可以被放在WRT操作符的右边用来实现PIC
    重定位类型.它们是`..gotpc, `..gotoff, `..got, `..plt and `..sym.
    它们的功能简要介绍如下:

    (*) 使用wrt ..gotpc来引用以global offset table为基址的符号会得到
    当前段的起始地址到global offset table的距离.(
    `_GLOBAL_OFFSET_TABLE_是引用GOT的标准符号名).所以你需要在返回
    结果前面加上$$来得到GOT的真实地址.

    (*) 用wrt ..gotoff来得到你的某一个段中的一个地址实际上得到从GOT的
    的起始地址到你指定的地址之间的距离,所以这个值再加上GOT的地址为得
    到你需要的那个真实地址.

    (*) 使用wrt ..got来得到一个外部符号或全局符号会让连接器在含有这个
    符号的地址的GOT中建立一个入口,这个引用会给出从GOT的起始地址到这
    个入口的一个距离;所以你可以加上GOT的地址,然后从得到的地址处载入,
    就会得到这个符号的真实地址.

    (*) 使用wrt ..plt来引用一个过程名会让连接器建立一个过程连接表入口,
    这个引用会给出PLT入口的地址.你可以在上下文使用这个引用,它会产生
    PC相关的重定位信息,所以,ELF包含引用PLT入口的非重定位类型

    (*) 略

    在8.2章中会有一个更详细的关于如何使用这些重定位类型写共享库的介绍

    6.5.3 `elf对`GLOBAL操作符的扩展.

    ELF目标文件可以包含关于一个全局符号的很多信息,不仅仅是一个地址:他
    可以包含符号的size,和它的类型.这不仅仅是为了调试的方便,而且在写共享
    库程序的时候,这确实是非常有用的.所以,NASM支持一些关于GLOBAL操作符
    的扩展,允许你指定这些特性.

    你可以把一个全局符号指定为一个函数或一个数据对象,这是通过在名字后面
    加上一个冒号跟上function或data实现的.(object可以用来代替data)
    比如:

    global hashlookup:function, hashtable:data

    把全局符号hashlookup指定为一个函数,把hashtable指定为一个数据对象.

    你也可以指定跟这个符号关联的数据的size,可以一个数值表达式(它可以包含
    labels,甚至前向引用)跟在类型后面,比如:

    global hashtable:data (hashtable.end - hashtable)

    hashtable:
    db this,that,theother ; some data here
    .end:

    这让NASM自动计算表的长度,然后把信息放进ELF的符号表中.

    声明全局符号的类型和size在写共享库代码的时候是必须的,关于这方面的更多
    信息,参阅8.2.4.

    6.5.4 `elf对`COMMON操作符的扩展.

    ELF也允许你指定通用变量的对齐请求.这是通过在通用变量的名字和size的
    后面加上一个以冒号分隔的数字来实现的,比如,一个doubleword的数组以
    4byte对齐比较好:

    common dwordarray 128:4

    这把array总的size声明为128bytes,并确定它对齐到4byte边界.

    6.5.5 16位代码和ELF

    ELF32规格不提供关于8位和16位值的重定位,但GNU的连接器ld把这作为
    一个扩展加进去了.NASM可以产生GNU兼容的重定位,允许16位代码被ld以
    ELF格式进行连接.如果NASM使用了选项-w+gnu-elf-extensions,如果一
    个重定位被产生的话,会有一条警告信息.

    6.6 `aout: Linux `a.out 目标文件

    aout格式产生a.out目标文件,这种格式在早期的Linux系统中使用(现在的
    Linux系统一般使用ELF格式,参阅6.5),这种格式跟其他的a.out目标文件有
    所不同,文件的头四个字节的魔数不一样;还有,有些版本的a.out,比如NetBSD
    的,支持地址无关代码,这一点,Linux的不支持.

    a.out提供的缺省文件扩展名是.o.

    a.out是一种非常简单的目标文件格式.它不支持任何特殊的操作符,没有特殊
    的符号,不使用SEG或WRT,对于标准的操作符也没有任何扩展.它只支持三个
    标准的段名.text,.data,.bss.

    6.7 `aoutb: NetBSD/FreeBSD/OpenBSD `a.out目标文件.

    aoutb格式产生在BSD unix,NetBSD,FreeBSD,OpenBSD系统上使用的a.out目
    标文件. 作为一种简单的目标文件,这种格式跟aout除了开头四字节的魔数不
    一样,其他完全相同.但是,aoutb格式支持跟elf格式一样的地址无关代码,所以
    你可以使用它来写BSD共享库.

    aoutb提供的缺省文件扩展名是.o.

    aoutb不支持特殊的操作符,没有特殊的符号,只有三个殊殊的段名.text,
    .data和.bss.但是,它象elf一样支持WRT的使用,这是为了提供地址无关的
    代码重定位类型.关于这部分的完整文档,请参阅6.5.2

    aoutb也支持跟elf同样的对于GLOBAL的扩展:详细信息请参阅6.5.3.

    6.8 `as86: Minix/Linux `as86目标文件.

    Minix/Linux 16位汇编器as86有它自己的非标准目标文件格式. 虽然它的链
    接器ld86产生跟普通的a.out非常相似的二进制输出,在as86跟ld86之
    间使用的目标文件格式并不是a.out.

    NASM支持这种格式,因为它是有用的,as86提供的缺省的输出文件扩展名是.o

    as86是一个非常简单的目标格式(从NASM用户的角度来看).它不支持任何特殊
    的操作符,符号,不使用SEG或WRT,对所有的标准操作符也没有任何扩展.它只
    支持三个标准的段名:.text,.data,和.bss.

    6.9 `rdf: 可重定位的动态目标文件格式.

    rdf输出格式产生RDOFF目标文件.RDOFF(可重定位的动态目标文件格式)
    `RDOFF是NASM自产的目标文件格式,是NASM自已设计的,它被反映在汇编器的内
    部结构中.

    RDOFF在所有知名的操作系统中都没有得到应用.但是,那些正在写他们自己的
    操作系统的人可能非常希望使用RDOFF作为他们自己的目标文件格式,因为
    RDOFF被设计得非常简单,并含有很少的冗余文件头信息.

    NASM的含有源代码的Unix包和DOS包中都含有一个rdoff子目录,里面有一套
    RDOFF工具:一个RDF连接器,一个RDF静态库管理器,一个RDF文件dump工具,还有
    一个程序可以用来在Linux下载入和执行RDF程序.

    rdf只支持标准的段名.text,.data,.bss.

    6.9.1 需要一个库: `LIBRARY操作符.

    RDOFF拥有一种机制,让一个目标文件请求一个指定的库被连接进模块中,可以
    是在载入时,也可以是在运行时连接进来.这是通过LIBRARY操作符完成的,它带
    有一个参数,即这个库的名字:

    library mylib.rdl

    6.9.2 指定一个模块名称: `MODULE操作符.

    特定的RDOFF头记录被用来存储模块的名字.它可以被用在运行时载入器作动
    态连接.MODULE操作符带有一个参数,即当前模块的名字:

    module mymodname

    注意,当你静态连接一个模块,并告诉连接器从输出文件中除去符号时,所有的模
    块名字也会被除去.为了避免这种情况,你应当在模块的名字前加一个$,就像:

    module $kernel.core

    6.9.3 `rdf对`GLOBAL操作符的扩展.

    RDOFF全局符号可以包含静态连接器需要的额外信息.你可以把一个全局符号
    标识为导出的,这就告诉连接器不要把它从目标可执行文件中或库文件中除去.
    就象在ELF中一样,你也可以指定一个导出符号是一个过程或是一个数据对象.

    在名字的尾部加上一个冒号和exporg,你就可以让一个符号被导出:

    global sys_open:export

    要指定一个导出符号是一个过程(函数),你要在声明的后南加上proc或function

    global sys_open:export proc

    相似的,要指定一个导出的数据对象,把data或object加到操作符的后面:

    global kernel_ticks:export data

    6.10 `dbg: 调试格式.

    在缺省配置下,dbg输出格式不会被构建进NASM中.如果你是从源代码开始构建你
    自己的NASM可执行版本,你可以在outform.h中定义OF_DBG或在编译器的命令
    行上定义,这样就可以得到dbg输出格式.

    dbg格式不输出一个普通的目标文件;它输出一个文本文件,包含有一个关于到输
    出格式的最终模块的转化动作的列表.它主要是用于帮助那些希望写自己的驱动程
    序的用户,这样他们就可以得到一个关于主程序的各种请求在输出中的形式的完整
    印象.

    对于简单的文件,可以简单地象下面这样使用:

    nasm -f dbg filename.asm

    这会产生一个叫做filename.dgb的诊断文件.但是,这在另一些目标文件上可能工
    作得并不是很好,因为每一个目标文件定义了它自己的宏(通常是用户级形式的操作
    符),而这些宏在dbg格式中并没有定义.因此,运行NASM两遍是非常有用的,这是为
    了对选定的源目标文件作一个预处理:

    nasm -e -f rdf -o rdfprog.i rdfprog.asm
    nasm -a -f dbg rdfprog.i

    这先把rdfprog.asm先预处理成rdfprog.i,让RDF特定的操作符被正确的转化成
    原始形式.然后,被预处理过的源程序被交给dbg格式去产生最终的诊断输出.

    这种方式对于obj格式还是不能正确工作的,因为obj的SEGMENT和GROUP操
    作符在把段名与组名定义为符号的时候会有副作用;所以程序不会被汇编.如果你
    确实需要trace一个obj的源文件,你必须自己定义符号(比如使用EXTERN)

    dbg接受所有的段名与操作符,并把它们全部记录在自己的输出文件中.

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

    本章将介绍一些在编写运行在MS-DOS和Windows 3.x下的16位代码的时候需要
    用到的一些常见的知识.涵兽了如果连接程序以生成.exe或.com文件,如果编写
    .sys设备驱动程序,以及16位的汇编语言代码与C编译器和Borland Pascal编译器
    之间的编程接口.

    7.1 产生.EXE文件.

    DOS下的任何大的程序都必须被构建成.EXE文件,因为只有.EXE文件拥有一种
    内部结构可以突破64K的段限制.Windows程序也需要被构建成.EXE文件,因为
    Windows不支持.COM格式.

    一般的,你是通过使用一个或多个obj格式的.OBJ目标文件来产生.EXE文件
    的,用连接器把它们连接到一起.但是,NASM也支持通过bin输出格式直接产生一
    个简单的DOS .EXE文件(通过使用DB和DW来构建exe文件头),并提供了一组
    宏帮助做到这一点.多谢Yann Guidon贡献了这一部分代码.

    在NASM的未来版本中,可能会完全支持.EXE文件.

    7.1.1 使用obj格式来产生.EXE文件.

    本章选描述常见的产生.EXE文件的方法:把.OBJ文件连接到一起.

    大多数16位的程序语言包都附带有一个配套的连接器,如果你没有,有一个免费的
    叫做VAL的连接器,在`x2ftp.oulu.fi上可以以LZH包的格式得到.也可以在
    `ftp.simtel.net上得到. 另一个免费的LZH包(尽管这个包是没有源代码的),叫做
    FREELINK,可以在`www.pcorner.com上得到. 第三个是djlink,是由DJ Delorie写
    的,可以在`www.delorie.com上得到. 第四个 ALINK, 是由Anthony A.J. Williams
    写的,可以在`alink.sourceforge.net上得到.

    当把多个.OBJ连接进一个.EXE文件中的时候,你需要保证它们当中有且仅有一
    个含有程序入口点(使用obj格式定义的特殊符号..start参阅6.2.6).如果没有
    模块定义入口点,连接器就不知道在输出文件的文件头中为入口点域赋什么值,如
    果有多个入口被定义,连接器就不知道到底该用哪一个.

    一个关于把NASM源文件汇编成.OBJ文件,并把它连接成一个.EXE文件的例子在
    这里给出.它演示了定义栈,初始化段寄存器,声明入口点的基本做法.这个文件也
    在NASM的test子目录中有提供,名字是objexe.asm.

    segment code

    ..start:
    mov ax,data
    mov ds,ax
    mov ax,stack
    mov ss,ax
    mov sp,stacktop

    这是一段初始化代码,先把DS寄存器设置成指定数据段,然后把‘SS’和‘SP’寄存器
    设置成指定提供的栈。注意,这种情况下,在mov ss,ax后,有一条指令隐式地把
    中断关闭掉了,这样抗敌,在载入 SS和‘SP’的过程中就不会有中断发生,并且没
    有可执行的栈可用。

    还有,一个特殊的符号..start在这段代码的开头被定义,它表示最终可执行代
    码的入口点。

    mov dx,hello
    mov ah,9
    int 0x21

    上面是主程序:在DS:DX中载入一个指向欢迎信息的指针(hello隐式的跟段
    ‘data相关联,’data在设置代码中已经被载入到‘DS‘寄存器中,所以整个指针是
    有效的),然后调用DOS的打印字符串功能调用。

    mov ax,0x4c00
    int 0x21

    这两句使用另一个DOS功能调用结束程序。

    segment data

    hello: db hello, world, 13, 10, $

    数据段中含有我们想要显示的字符串。

    segment stack stack
    resb 64
    stacktop:

    上面的代码声明一个含有64bytes的未初始化栈空间的堆栈段,然后把指针
    ’stacktop指向它的顶端。操作符segment stack stack定义了一个叫做
    ‘stack的段,同时它的类型也是STACK.后者并不一定需要,但是连接串可
    能会因为你的程序中没有段的类型为STACK而发出警告。

    上面的文件在被编译为.OBJ文件中,会自动连接成为一个有效的.EXE文
    件,当运行它时会打印出hello world,然后退出。

    7.1.2 使用’bin格式来产生`.EXE文件。

    .EXE文件是相当简单的,所以可以通过编写一个纯二进制文件然后在前面
    连接上一个32bytes的头就可以产生一个.exe的文件了。这个文件头也是
    相当简单,它可以通过使用NASM自己的DB和DW命令来产生,所以你可以使
    用bin输出格式直接产生.EXE文件。

    在NASM的包中,有一个misc子目录,这是一个宏文件exebin.mac。它定义
    了三个宏`EXE_begin,`EXE_stack和`EXE_end.

    要通过这种方法产生一个.EXE文件,你应当开始的时候先使用%include载
    入exebin.mac宏包到你的源文件中。然后,你应当使用EXE_begin宏(不带
    任何参数)来产生文件头数据。然后像平常一样写二进制格式的代码-你可以
    使用三种标准的段.text,.data,.bss.在文件的最后,你应当调用
    EXE_end宏(还是不带任何参数),它定义了一些标识段size的符号,而这些宏
    会由EXE_begin产生的文件头代码引用。

    在这个模块中,你最后的代码是写在0x100开始的地址处的,就像是.COM文
    件-实际上,如果你剥去那个32bytes的文件头,你就会得到一个有效的.COM程
    序。所有的段基址是相同的,所以程序的大小被限制在64K的范围内,这还是跟
    一个.COM文件相同。ORG操作符是被EXE_begin宏使用的,所以你不必自己
    显式的使用它

    你可以直接使用你的段基址,但不幸的是,因为这需要在文件头中有一个重定
    位,事情就会变得更复杂。所以你应当从CS中拷贝出一个段基址。

    进入你的.EXE文件后,SS:SP已经被正确的指向一个2Kb的栈顶。你可以通过
    调用EXE_stack宏来调整缺省的2KB的栈大小。比如,把你的栈size改变到
    64bytes,你可以调用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:
    ; put your code here

    section .data

    ; put data items here

    section .bss

    ; put uninitialised data here

    bin格式会把.text段放在文件的最开始处,所以如果你需要,你可以在开始
    编写代码前先声明data和bss元素,代码段最终还是会放到文件的最开始处。

    BSS(未初始化过的数据)段本身在.COM文件中并不占据空间:BSS中的元素的地
    址是一个指向文件外面的一个空间的一个指针,这样做的依据是在程序运行中,
    这样可以节省空间。所以你不应当相信当你运行程序时,你的BSS段已经被初始
    化为零了。

    为了汇编上面的程序,你应当象下面这样使用命令行:

    nasm myprog.asm -fbin -o myprog.com

    如果没有显式的指定输出文件名,这个bin格式会产生一个叫做myprog的文
    件,所以你必须重新给它指定一个文件名。

    7.2.2 使用`obj格式产生`.COM文件

    如果你在写一个.COM文件的时候,产生了多于一个的模块,你可能希望汇编成
    多个.OBJ文件,然后把它们连接成一个.COM程序。如果你拥有一个能够输出
    .COM文件的连接器,你可以做到这一点。或者拥有一个转化程序(比如,
    EXE2BIN)把一个.EXE输出文件转化为一个.COM文件也可。

    如果你要这样做,你必须注意几件事情:

    (*) 第一个含有代码的目标文件在它的代码段中,第一句必须是:RESB 100h。
    这是为了保证代码在代码段基址的偏移100h处开始,这样,连接器和转化
    程序在产生.com文件时,就不必调整地址引用了。其他的汇编器是使用ORG
    操作符来达到此目的的,但是ORG在NASM中对于bin格式来说是一个格式相
    关的操作符,会表达不同的含义。

    (*) 你不必定义一个堆栈段。

    (*) 你的所有段必须放在一个组中,这样每次你的代码或数据引用一个符号偏移
    时,所有的偏移值都是相对于同一个段基址的。这是因为,当一个.COM文件
    载入时,所有的段寄存器含有同一个值。

    7.3 产生`.SYS文件

    MS-DOS设备驱动-SYS文件-是一些纯二进制文件,跟.com文件相似,但有一点,
    它们的起始地址是0,而不是100h。因此,如果你用bin格式写一个设备程序
    ,你不必使用ORG操作符,因为bin的缺省起始地址就是零。相似的,如果你
    使用obj,你不必在代码段的起始处使用RESB 100h

    .SYS文件拥有一个文件头,包含一些指针,这些指针指向设备中完成实际工
    作的不同的子过程。这个结构必须在代码段的起始处被定义,尽管它并不是
    实际的代码。

    要得到关于.SYS文件的更多信息,头结构中必须包含的数据,有一本以FAQ列
    表的形式给出的书可以在`comp.os.msdos.programmer得到。

    7.4 与16位C程序之间的接口。

    本章介绍编写调用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宏以相似的方式工作。

    7.4.2 内存模式。

    NASM没有提供支持各种C的内存模式的直接机制;你必须自己记住你在何
    种模式下工作。这意味着你自己必须跟踪以下事情:

    (*) 在使用单个代码段的模式中(tiny small和compact)函数都是near的,
    这表示函数指针在作为一个函数参数存入数据段或压栈时,有16位
    长并只包含一个偏移域(CS寄存器中的值从来不改变,总是给出函数
    地址的段地真正部分),函数调用就使用普通的nearCALL指令,返回
    使用RETN(在NASM中,它跟RET同义)。这意味着你在编写你自己的
    过程时,应当使用RETN返回,你调用外部C过程时,可以使用near的
    CALL指令。

    (*) 在使用多于一个代码段的模块中(medium, large和huge)函数是far的,
    这表示函数指针是32位长的(包含 16位的偏移值和紧跟着的16位段
    地址),这种函数使用CALL FAR进行调用(或者CALL seg:offset)
    而返回使用RETF。同样的,你编写自己的过程时,应当使用RETF,
    调用外部C过程应当使用CALL FAR。

    (*) 在使用单个数据段的模块中(tiny, small和medium),数据指针是16位
    长的,只包含一个偏移域(’DS‘寄存器的值不改变,总是给出数据元素
    的地址的段地址部分)。

    (*) 在使用多于一个数据段的模块中(compact, large和huge),数据指针
    是32位长的,包含一个16位的偏移跟上一佧16位的段地址。你还是应
    当小心,不要随便改变了ds的值而没有恢复它,但是ES可以被随便用
    来存取32位数据指针的内容。

    7.4.3 函数定义和函数调用。

    16位程序中的C调用转化如下所示。在下面的描述中,_caller_和_callee_
    分别表示调用者和被调用者。

    (*) caller把函数的参数按相反的顺序压栈,(从右到左,所以第一个参数
    被最后一个压栈)。

    (*) caller然后执行一个CALL指令把控制权交给callee。根据所使用的
    内存模式,CALL可以是near或far。

    (*) callee接收控制权,然后一般会(尽管在没有带参数的函数中,这不是
    必须的)在开始的时候把’SP‘的值赋给’BP‘,然后就可以把‘BP’
    作为一个基准指针用以寻找栈中的参数。当然,这个事情也有可能由
    caller来做,所以,关于BP的部分调用转化工作必须由C函数来完成
    。因此callee如果要把BP设为框架指针,它必须把先前的BP值压栈。

    (*) 然后callee可能会以BP相关的方式去存取它的参数。在[BP]中存有BP
    在压栈前的那个值;下一字word,在[BP+2]处,是返回地址的偏移域,
    由CALL指令隐式压入。在一个small模式的函数中。在[BP+4]处是参
    数开始的地方;在large模式的函数中,返回地址的段基址部分存在
    [BP+4]的地方,而参数是从[BP+6]处开始的。最左边的参数是被后一个被
    压入栈的,所以在BP的这点偏移值上就可以被取到;其他参数紧随其后,偏
    移地址是连续的.这样,在一个象printf这样的带有一定数量的参数的函
    数中,以相反的顺序把参数压栈意味着函数可以知道从哪儿获得它的第一个
    参数,这个参数可以告诉接接下来还有多少参数,和它们的类型分别是什么.

    (*) callee可能希望减小sp的值,以便在栈中分配本地变量,这些变量可以用
    BP负偏移来进行存取.

    (*) callee如果想要返回给caller一个值,应该根据这个值的大小放在AL,AX
    或DX:AX中.如果是浮点类型返回值,有时(看编译器而定)会放在ST0中.

    (*) 一旦callee结束了处理,它如果分配过了本地空间,就从BP中恢复SP的
    值,然后把原来的BP值出栈,然后依据使用的内存模式使用RETN或RETF
    返回值.

    (*) 如果caller从callee中又重新取回了控制权,函数的参数仍旧在栈中,所以它
    需要加一个立即常数到SP中去,以移除这些参数(不用执行一系列的pop指令
    来达到这个目的).这样,如果一个函数因为匹配的问题偶尔被以错误的参数个
    数来调用,栈还是会返回一个正常的状态,因为caller知道有多少个参数被压
    了,它会把它们正确的移除.

    这种调用转化跟Pascal程序的调用转化是没有办法比较的(在7.5.1描述).pascal
    拥有一个更简单的转化机制,因为没有函数拥有可变数目的参数.所以callee知道
    传递了多少参数,它也就有能力自己来通过传递一个立即数给RET或RETF指令
    来移除栈中的参数,所以caller就不必做这个事情了.同样,参数也是以从左到右
    的顺序被压栈的,而不是从右到左,这意味着一个编译器可以更方便地处理。


    这样,如果你想要以C风格定义一个函数,应该以下面的方式进行:这个例子是
    在small模式下的。

    global _myfunc

    _myfunc:
    push bp
    mov bp,sp
    sub sp,0x40 ; 64 bytes of local stack space
    mov bx,[bp+4] ; first parameter to function

    ; some more code

    mov sp,bp ; undo "sub sp,0x40" above
    pop bp
    ret

    在巨模式下,你应该把RET替换成RETF,然后应该在[BP+6]的位置处寻找第
    一个参数,而不是[BP+4].当然,如果某一个参数是一个指针的话,那参数序列
    的偏移值会因为内存模式的改变而改变:far指针作为一个参数时在栈中占用
    4bytes,而near指针只占用两个字节。

    另一方面,如果从你的汇编代码中调用一个C函数,你应该做下面的一些事情:

    extern _printf

    ; and then, further down...

    push word [myint] ; one of my integer variables
    push word mystring ; pointer into my data segment
    call _printf
    add sp,byte 4 ; `byte saves space

    ; then those data items...

    segment _DATA

    myint dw 1234
    mystring db This number -> %d <- should be 1234,10,0

    这段代码在small内存模式下等同于下面的C代码:

    int myint = 1234;
    printf("This number -> %d <- should be 1234\n", myint);

    在large模式下,函数调用代码可能更象下面这样。在这个例子中,假设DS已经
    含有段_DATA的段基址,你首先必须初始化它:

    push word [myint]
    push word seg mystring ; Now push the segment, and...
    push word mystring ; ... offset of "mystring"
    call far _printf
    add sp,byte 6

    这个整型值在栈中还是占用一个字的空间,因为large模式并不会影响到int
    数据类型的size.printf的第一个参数(最后一个压栈),是一个数据指针,所以
    含有一个段基址和一个偏移域。在内存中,段基址应该放在偏移域后面,所以,
    必须首先被压栈。(当然,PUSH DS是一个取代PUSH WORD SEG mystring的更
    短的形式,如果DS已经被正确设置的话)。然后,实际的调用变成了一个far调用,
    因为在large模式下,函数都是被far调用的;调用后,SP必须被加上6,而不是
    4,以释放压入栈中的参数。

    7.4.4 存取数据元素。

    要想获得一个C变量的内容,或者声明一个C语言可以存取的变量,你只需要把变
    量名声明为GLOBAL或EXTERN即可。(再次提醒,就象在7.4.1中所介绍的,变
    量名前需要加上一个下划线)这样,一个在C中声明的变量ini i可以在汇编语
    中以下述方式存取:

    extern _i

    mov ax,[_i]

    而要声明一个你自己的可以被C程序存取的整型变量如:extern int j,你可
    以这样做(确定你下在_DATA段中):

    global _j

    _j dw 0

    要存取C的数组,你需要知道数组元素的size.比如,int变量是2byte长,所以
    如果一个C程序声明了一个数组int a[10],你可象这样存取a[3]:mov ax,
    [_a+6].(字节偏移6是通过数组下标3乘上数组元素的size2得到的。) 基于C的
    16位编译器的数据size如下:1 for `char, 2 for `short and `int, 4
    for `long and `float, and 8 for `double.

    为了存取C的数据结构,你必须知道从结构的基地址到你所感兴趣的域的偏移地
    址。你可以通过把C结构定义转化为NASM的结构定义(使用STRUC),或者计算这
    个偏移地址然后进行相应操作。

    以上述任何一种方法实现,你必须得阅读你的C编译器的手册去找出他是如何组
    织数据结构的。NASM在它的宏STRUC中不给出任何对结构体成员的对齐操作,
    所以你可能会发现结构体类似下面的样子:

    struct {
    char c;
    int i;
    } foo;

    可能就是4字节长,而不是三个字,因为int域会被对齐到2byte边界。但是,这
    种排布的特性在C编译器中很可能只是一个配置选项,使用命令行选项或者
    #pragma行。所以你必须找出你的编译器是如何实现这个的。

    7.4.5 `c16.mac: 与16位C接口的帮助宏。

    在NASM包中,在misc子目录下,是一个宏文件c16.mac。它定义了三个宏
    proc,arg和endproc。这些被用在C风格的过程定义中,它们自动完成了
    很多工作,包括对调用转化的跟踪。

    (另外一种选择是,TASM兼容模式的arg现在也被编译进了NASM的预处理器,
    详见4.9)

    关于在汇编函数中使用这个宏的一个例子如下:

    proc _nearproc

    %$i arg
    %$j arg
    mov ax,[bp + %$i]
    mov bx,[bp + %$j]
    add ax,[bx]

    endproc

    这把_nearproc定义为一个带有两个参数的一个过程,第一个(i)是一个整
    型数,第二个(j)是一个指向整型数的指针,它返回i+*j。

    注意,arg宏展开的第一行有一个EQU,而且因为在宏调用的前面的那个
    label在宏展开后被加在了第一行的前面,所以EQU能否工作取决于%$i是否
    是一个关于BP的偏移值。同时一个对于上下文来说是本地的context-local变
    量被使用,它被proc宏压栈,然后被endproc宏出栈,所以,在后来的过程
    中,同样的参数名还是可以使用,当然,你不一定要这么做。

    宏在缺省状况下把过程代码设置为near函数(tiny,small和compact模式代码),
    你可以通过代码%define FARCODE产生far函数(medium, large和huge模式代
    码)。这会改变endproc产生的返回指令的类型,还会改变参数起始位置的偏移
    值。这个宏在设置内容时,本质上并依赖数据指针是near或far。

    arg可以带有一个可选参数,给出参数的size。如果没有size给出,缺省设置为
    2,因为绝大多数函数参数会是int类型。

    上面函数的large模式看上去应该是这个样子:

    %define FARCODE

    proc _farproc

    %$i arg
    %$j arg 4
    mov ax,[bp + %$i]
    mov bx,[bp + %$j]
    mov es,[bp + %$j + 2]
    add ax,[bx]

    endproc

    这利用了arg宏的参数定义参数的size为4,因为j现在是一个far指针。当我们
    从j中载入数据时,我们必须同时载入一个段基址和一个偏移值。

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

    本章主要介绍在编写运行在Win32或Unix下的32位代码,或与Unix风格的编译器,
    比如DJGPP连接的代码时,通常会碰到的一些问题。这里包括如何编写与32位C
    函数连接的汇编代码,如何为共享库编写地址无关的代码。

    几乎所有的32位代码,即在实际使用中的所有运行在Win32,DJGPP和所有PC
    Unix变体都运行 在_flat_内存模式下。这意味着段寄存器和页已经被正确设置,
    以给你一个统一的32位的4Gb的地址空间,而不管你当前工作在哪个段下,而且
    你应当完全忽略所有的段寄存器。当写一个平坦(flat)模式的程序代码时,你从
    来不必使用段重载或改变段寄存器,而且你传给CALL,和JMP的代码段地址,
    你存取你的变量时使用的数据段地址,你存取局部变量和函数参数时的堆栈段地
    址实际上都在同一个地址空间中。每一个地址都是32位长,只含有一个偏移域

    8.1 与32位C代码之间的接口。

    在7.4中有很多关于与16位C代码之间接口的讨论,这些东西有很多在32位代码中
    仍有用。但已经不必担心内存模式和段的问题了,这把问题简化了很多。

    8.1.1 外部符号名。

    大多数32位的C编译器共享16位编译器的转化机制,即它们定义的所有的全局符
    号(函数与数据)的名字在C程序中出现时由一个下划线加上名字组成。但是,并
    不是他们中的所有的都这样做::ELF标准指出C符号在汇编语言中不含有一个
    前导的下划线。

    老的Linuxa.outC编译器,所有的Win32编译器,‘DJGPP和NetBSD
    FreeBSD都使用前导的下划线;对于这些编译器来讲,7.4.1中给出的宏
    cextern和cglobal还会正常工作。对于ELF来讲,下划线是没有必要的。

    8.1.2 函数定义和函数调用。

    32位程序中的C调用转化如下所述。在下面的描述中,_caller_和_callee_用来o
    表示调用函数和被调用函数。

    (*) caller把函数的参数按相反的顺序(从右到左,这样的话,第一个参数被最
    后一个压栈)依次压栈

    (*) 然后,caller执行一个nearCALL指令把控制权传给callee。

    (*) callee接受控制权,然后一般会(但这实际上不是必须的,如果函数不需要
    存取它的参数就不用)开始先存储ESP的值到EBP中,这样就可以使用
    EBP作为一个基指针去栈中寻找参数。但是,这一点也可以放在caller中
    做,所以,调用转化中EBP必须被C函数保存起来。因为callee要把EBP
    设置为一个框架指针来使用,它必须把先前的值给保存起来。

    (*) 然后,callee就可以通过与EBP相关的方式来存取它的参数了。在[EBP]
    处的双字拥有刚刚被压栈的EBP的前一个值;接下来的双字,在[EBP+4]
    TH ,是被CALL指令隐式压入的返回地址。后面才是参数开始的地方,在
    [EBP+8]处。因为最左边的参数最后一个被压栈,在[EBP]的这个偏移地址
    上就可以被取得;剩下的参数依次存在后面,以连续增长的偏移值存放。
    这样,在一个如printf的带有一定数量参数的函数中,以相反的顺序把
    参数压栈意味着函数可以知道到哪儿去找它的第一个参数,这个参数可以
    告诉它总共有多少参数,它们的类型是什么。

    (*) callee可能也希望能够再次减小ESP的值,以为本地变量开辟本地空间,
    这些变量然后就可以通过EBP的负偏移来获取。

    (*) callee如果需要返回给caller一个值,需要根据这个值的size把它放在
    AL,AX或EAX中。浮点数在ST0中返回。

    (*) 一旦callee完成了处理,如果它定义的局部栈变量,它就从EBP中恢复
    ESP,然后弹出前一个EBP的值,并通过RET返回。

    (*) 当caller从callee那里取回了控制权,函数的参数还是放在栈中,所以,它
    通常给ESP加上一个立即常数以移除参数(而不是执行一系列的pop指令
    )。这样,如果一个函数如果因为意外,使用了错误的参数个数,栈还是
    会返回到正常状态,因为caller知道多少参数被压栈了,并可以正确的移
    除。

    对于Win32程序使用的Windows API调用,有另一个可选的调用转化,对于那些
    Windows API调用的函数(称为windows过程)也一样:他们遵循一个被微软叫
    做__stdcall的转化。这跟Pascal的转化比较接近,在这里,callee通过给
    RET指令传递一个参数来清除栈。但是,参数还是以从右到左的顺序被压栈。

    这样,你可以象下面这样定义一个C风格的函数:

    global _myfunc

    _myfunc:
    push ebp
    mov ebp,esp
    sub esp,0x40 ; 64 bytes of local stack space
    mov ebx,[ebp+8] ; first parameter to function

    ; some more code

    leave ; mov esp,ebp / pop ebp
    ret

    另一方面,如果你要从你的汇编代码中调用一个C函数,你可以象下面这样写代
    码:

    extern _printf

    ; and then, further down...

    push dword [myint] ; one of my integer variables
    push dword mystring ; pointer into my data segment
    call _printf
    add esp,byte 8 ; `byte saves space

    ; then those data items...

    segment _DATA

    myint dd 1234
    mystring db This number -> %d <- should be 1234,10,0

    这段代码等同于下面的C代码:

    int myint = 1234;
    printf("This number -> %d <- should be 1234\n", myint);

    8.1.3 获取数据元素。

    要想获取一个C变量的内容,或者声明一个C可以获取的变量,你必须把这个变
    量声明为GLOBAL或EXTERN(再次提醒,变量名前需要加上一个下划线,就象
    8.1.1中所描述的),这样,一个被声明为int i的C变量可以从汇编语言中这样
    获取:
    extern _i
    mov eax,[_i]

    而要定个一个C程序可以获取的你自己的变量extern int j,你可以这样做(确
    定你正在_DATA中)

    global _j
    _j dd 0

    要获取C数组,你必须知道数组的元素的size。比如,int变量是4bytes长,所
    以,如果一个C程序声明了一个数组int a[10],你可以使用代码mov ax,
    [_a+12]来存取变量a[3]。(字节偏移12是通过数组下标3乘上数组元素的size
    4得到的)。基于C的32位编译器上的数据的size如下:1 for `char,
    2 for `short, 4 for `int, `long and `float, and 8for `double.
    Pointers, 32位的地址也是4字节长。

    要获取C的数据结构体,你必须知道从结构体的基地址到你所需要的域之间的偏
    移值。你可以把C的结构体定义转化成NASM的结构体定义(使用STRUC),或者计
    算得到这个偏移值,然后使用它。

    以上面任何一种方式实现,你都需要阅读你的C编译器的手册找出它是如何组织
    结构体数据的。NASM在它的STRUC宏中没有给出任何特定的对齐规则,所以如
    果C编译器产生结构体,你必须自己指定对齐规则。你可能发现类似下面的结构
    体:

    struct {
    char c;
    int i;
    } foo;

    可能是8字节长,而不是5字节,因为int域会被对齐到4bytes边界。但是,这
    种排布特性有时会是C编译器的一个配置选项,可以使用命令行选项或#progma
    行来实现,所以你必须找出你自己的编译器是如何做的。

    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,[ebp + %$j]
    add eax,[ebx]

    endproc

    它把函数_proc32定义成一个带有两个参数的过程,第一个(i)是一个整型数,
    第二个(j)是一个指向整型数的指针,它返回i+*j。

    注意,宏arg展开后的第一行有个EQU,因为在宏调用行的前面的那个label
    被加到了第一行上,EQU行就可以正常工作了,它把%Si定义为一个以BP
    为基址的偏移值。一个context-local变量在这里被使用,被proc宏压栈,然
    后被endproc宏出栈,所以,同样的参数名在后来的过程中还是可以使用,当然
    你不一定要那样做。

    arg带有一个可选的参数,给出参数的size。如果没有size给出,缺省的是4,因
    为很多函数参数都会是int类型或者是一个指针。

    8.2 编写NetBSD/FreeBSD/OpenBSD和Linux/ELF共享库

    ELF在Linux下取代了老的a.out目标文件格式,因为它包含对于地址无关代码
    (PIC)的支持,这可以让编写共享库变得很容易。NASM支持ELF的地址无关代码
    特性,所以你可以用NASM来编写Linux的ELF共享库。

    NetBSD,和它的近亲FreeBSD,OpenBSD,采用了一种不同的方法,它们把PIC支持做
    进了a.out格式。NASM支持这些格式,把它们叫做aoutb输出格式,所以你可
    以在NASM下写BSD的共享库。

    操作系统是通过把一个库文件内存映射到一个运行进程的地址空间上的某一个点
    来实现载入PIC共享库的。所以,库的代码段内容必须不依赖于它被载入到了内
    存的什么地方。

    因此,你不能通过下面的代码得到你的变量:

    mov eax,[myvar] ; WRONG

    而是通过连接器提供一片内存空间,这片空间叫做全局偏移表(GOT);GOT被放到
    离你库代码的一个常量距离值的地方,所以如果你发现了你的库被载入到了什么
    地方(这可以通过使用CALL和POP指令而得到),你可以得到GOT中的地址,然
    后你就可以通过这个连接器产生的在GOT中的入口来载入你的变量的地址。

    而PIC共享库的数据段就没有这些限制了:因为数据段是可定局的,它必须被拷
    贝到内存中,而不是仅仅从库文件中作一个映射,所以一旦它被拷贝进来,它
    就可以被重定位。所以你可以把一些常规的在数据段中重定位的类型用进来,而
    不必担心会有什么错误发生。

    8.2.1 取得GOT中的地址。

    每个在你的共享库中的代码模块都应当把GOT定义为一个导出符号:

    extern _GLOBAL_OFFSET_TABLE_ ; in ELF
    extern __GLOBAL_OFFSET_TABLE_ ; in BSD a.out

    在你的共享库中,那些需要获取你的data或BSS段中数据的函数,你必须在它们
    的开头先计算GOT的地址。这一般以如下形式编写这个函数:

    func: push ebp
    mov ebp,esp
    push ebx
    call .get_GOT
    .get_GOT:
    pop ebx
    add ebx,_GLOBAL_OFFSET_TABLE_+$$-.get_GOT wrt ..gotpc

    ; the function body comes here

    mov ebx,[ebp-4]
    mov esp,ebp
    pop ebp
    ret

    (对于BSD, 符号`_GLOBAL_OFFSET_TABLE开头需要两个下划线。)

    这个函数的头两行只是简单的标准的C风格的开头,用于设置栈框架,最后的三
    行是标准的C风格的结尾,第三行,和倒数第四行,分别保存和恢复EBS寄存器
    ,因为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中含有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来得到。它工作的方式如下:

    lea eax,[ebx+myvar wrt ..gotoff]

    表达式myvar wrt ..gotoff在共享库被连接进来的时候被计算,得到从GOT
    地始地址开始的变量myvar的偏移值。所以,把它加到上面的EBX中,并把
    它放到EAX中.

    如果你把一些变量声明为GLOBAL,而没有指定它们的size的话,它们在库中的
    代码模块间会被共享,但不会被从库中导出到载入它们的程序中.但们还会存在
    于你的常规data和BSS段中,所以通过上面的..gotoff机制,你可以把它们作为
    局部变量那样存取

    注意,因为BSD的a.out格式处理这种重定位类型的一种方式,在你要存取的地
    址处的同一个段内必须至少有一个非本地的符号.

    8.2.3 寻址外部和通用数据元素.

    如果你的库需要得到一个外部变量(对库来说是外部的,并不是对它所在的一个
    模块),你必须使用..got类型得到它...got类型,并不给你从GOT基地址到
    变量的偏移,给你的是从GOT基地址到一个含有这个变量地址的GOT入口的偏移,
    连接器会在构建库时设置这个GOT入口,动态连接器会在载入时在这个入口放上
    正确的地址.所以,要得到一个外部变量extvar的地址,并放到EAX中,你可以
    这样写:

    mov eax,[ebx+extvar wrt ..got]

    这会在GOT的一个入口上载入extvar的地址.连接器在构建共享库的时候,会搜
    集每一个..got类型的重定位信息,然后构建GOT,保证它含有每一个必须的入


    通用变量也必须以这种方式被存取.

    8.2.4 把符号导出给库用户.

    如果你需要把符号导出给库用户,你必须把它们声明为函数或数据,如果它们是数
    据,你必须给出数据元素的size.这是因为动态连接器必须为每一个导出的函数
    构建过程连接表入口,还要把导出数据元素从库的数据段中移出.

    所以,导出一个函数给库用户,你必须这样:

    global func:function ; declare it as a function

    func: push ebp

    ; etc.

    而导出一个数据元素,比如数组,你必须这样写代码:

    global array:data array.end-array ; give the size too

    array: resd 128
    .end:

    小心:如果你希望通过把变量声明为GLOBAL并指定一个size,而导出给库用户,
    这个变量最终会存在于主程序的数据段中,而不是在你的库的数据段内,所以你
    必须通过使用..got机制来获取你自己的全局变量,而不是..gotogg,就象它
    是一个外部变量一样(实际上,它已经变成了外部变量).

    同样的,如果你需要把一个导出的全局变量的地址存入你的一个数据段中,你不能
    通过下面的标准方式实现:

    dataptr: dd global_data_item ; WRONG

    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的要简单得多:你只需要把调用CALL printf替换为PLT相关的版
    本:`CALL printf WRT ..plt.

    8.2.6 产生库文件.
    写好了一些代码模块并把它们汇编成.o文件后,你就可以产生你的共享库了,
    使用下面的命令就可以:

    ld -shared -o library.so module1.o module2.o # for ELF
    ld -Bshareable -o library.so module1.o module2.o # for 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.

    第九章: 混合16位与32位代码
    ------------------------------------

    本章将介绍一些跟非常用的地址与跳转指令相关的一些问题, 这些问题当你在
    编写操作系统代码时会常遇上,比如保护模式初始化过程,它需要代码操作混合
    的段size,比如在16位段中的代码需要去修改在32位段中的数据,或者在不同的
    size的段之间的跳转.

    9.1 混合Size的跳转.

    最常用的混合size指令的形式是在写32位操作系统时用到的:在16位模式中完成
    你的设置,比如载入内核,然后你必须通过切入到保护模式中引导它,然后跳转到
    32位的内核起始地址处.在一个完全32位的操作系统中,这是你唯一需要用到混合
    size指令的地方,因为在它之间的所有事情都可以在纯16位代码中完成,而在它之
    后的所在事情都在纯32位代码中.

    这种跳转必须指定一个48位的远地址,因为目标段是一个32位段.但是,它必须在
    16位段中被汇编,所以,仅仅如下面写代码:

    jmp 0x1234:0x56789ABC ; wrong!

    不会正常工作,因为地址的偏移域部分会被截断成0x9ABC,然后,跳转会是一个
    普通的16位远跳转.

    Linux内核的设置代码使用as86通过手工编码来产生这条指令,使用DB指令,
    NASM可以比它更好些,可以自己产生正确的指令,这里是正确的做法:

    jmp dword 0x1234:0x56789ABC ; right

    DWORD前缀(严格地讲,它应该放在冒后的后面,因为它只是把偏移域声明为
    doubleword;但是NASM接受任何一种形式,因为两种写法都是明确的)强制偏移域
    在假设你正从一个16段跳转到32位段的前提下,被处理为far.

    你可以完成一个相反的操作,从一个32位段中跳转到一个16位段,使用word
    前缀:

    jmp word 0x8765:0x4321 ; 32 to 16 bit

    如果WORD前缀在16位模式下被指定,或者DWORD前缀在32位模式下被指定,
    它们都会被忽略,因为它们每一个都显式强制NASM进入一个已进进入的模式.

    9.2 在不同size的段间寻址.

    如果你的操作系统是16位与32位混合的,或者你正在写一个DOS的扩展,你可能
    必须处理一些16位段和一些32位段.在某些地方,你可能最终要在一个16位段中
    编写能获取32位段中的数据的代码,或者相反.

    如果你要获取的32位段中的数据正好在段的前64K的范围内,你可以通过普通的
    16位地址操作来达到目的;但是或多或少,你会需要从16位模式中处理32位的寻
    址.

    最早的解决方案保证你使用了一个寄存器用于保存地址,因为任何在32位寄存器
    中的有效地址都被强制作为一个32位的地址,所以,你可以:

    mov eax,offset_into_32_bit_segment_specified_by_fs
    mov dword [fs:eax],0x11223344

    这个不错,但有些笨拙(因为它浪费了一条指令和一个寄存器),如果你已经知道
    你的目标所在的精确偏移.x86架构允许32位有效地址被指定为一个4bytes的偏
    移,所以,NASM为什么不为些产生一个最佳的指令呢?

    它可以,就象在9.1中一样,你只需要在地址前加上一个DWORD前缀,然后,它会
    被强制作为一个32位的地址:

    mov dword [fs:dword my_offset],0x11223344
    同样跟9.1中一样,NASM并不关心DWORD前缀是在段重载符前,还是这后,所以
    可以把代码改得好看一些:

    mov dword [dword fs:my_offset],0x11223344

    不要把DWROD前缀放在方括号外面,它是用来控制存储在那里的数据的size的,
    而在方括号内的话,它控制地址本身的长度.这两种方式可以被很容易地区分:

    mov word [dword 0x12345678],0x9ABC

    这把一个16位的数据放到了一个指定为32位偏移的地址中.

    你也可以把WORD或DWROD前缀跟FAR前缀放到一起,来间接跳转或调用,比
    如:

    call dword far [fs:word 0x4321]

    这条指令包含一个指定为16位偏移的地址,它载入了一个48位的远指针,(16位
    段和32位段偏移),然后调用这个地址.

    9.3 其他的混合size指令.

    你可能需要用于获取数据的其它的方式可能就是使用字符串指令(LODSx
    STOSx,等等)或XLATB指令.这些指令因为不带有任何参数,看上去好像很难
    在它们被汇编进16位段的时候使它们使用32位地址.

    而这正是NASM的a16和a32前缀的目的,如果你正在16位段中编写LODSB,
    但它是被用来获取一个32位段中的字符串的,你应当把目标地址载入ESI,然
    后编写:

    a32 lodsb

    这个前缀强制地址的size为32位,意思是LODSB从[DS:ESI]中载入内容,而不是
    从[DS:SI]中.要在编写32位段的时候,获取在16位段中的字符串,相应的前缀a16
    可以被使用.

    a16和a32前缀可以被运用到NASM指令表的任何指令上,但是他们中的大多数
    可以在没有这两个前缀的情况下产生所有有用的形式.这两个前缀只有在那些带
    有隐式地址的指令中是有效的: `CMPSx (section B.4.27),
    `SCASx (section B.4.286), `LODSx (section B.4.141), `STOSx
    (section B.4.303), `MOVSx (section B.4.178), `INSx (section
    B.4.121), `OUTSx (section B.4.195), and `XLATB (section B.4.334).
    还有,就是变量压栈与出栈指令,(`PUSHA和`POPF 和更常用的`PUSH和`POP)
    可以接受a16或a32前缀在堆栈段用在另一个不同size的代码段中的时候,强
    制一个特定的SP或ESP被用作栈指针,

    PUSH和POP,当在32位模式中被用在段寄存器上时,也会有一个不同的行为,
    它们会一次操作4bytes,而最高处的两个被忽略,而最底部的两个给出正被操作的
    段寄存器的值.为了强制push和pop指令的16位行为,你可以使用操作数前缀o16

    o16 push ss
    o16 push ds

    这段代码在栈空间中开辟一个doubleword用于存放两个段寄存器,而在一般情况
    下,这一个doubleword只会存放一个寄存器的值.

    (你也可以使用o32前缀在16位模式下强制32位行为,但这看上去并没有什么用
    处.)

    第十章: 答疑
    ---------------------------

    本章介绍一些用户在使用NASM时经常遇到的普遍性问题,并给出解答.同时,如果你
    发现了这儿还未列出的BUG,这儿也给出提交bug的方法.

    10.1 普遍性的问题.

    10.1.1 NASM产生了低效的代码.

    我得到了很多关于NASM产生了低效代码的BUG报告,甚至是产生错误代码,比如像
    指令ADD ESP,8产生的代码.其实这是一个经过深思熟虑设计特性,跟可预测的
    输出相关:NASM看到ADD ESP,8时,会产生一个预留32位偏移的指令形式.如果你
    希望产生一个节约空间的指令形式,你必须写上ADD ESP,BYTE 8.这不是一个BUG,
    至多也只能算是一个不好的特性,各人看法不同而已.

    10.1.2 我的jump指令超出范围.

    相似的,人们经常抱怨说在他们使用条件跳转指令时(这些指令缺省状况下是short
    的)经常需要跳转比较远,而NASM会报告说short jump out of range,而不作长
    远转.

    同样,这也是可预测执行的一个部分,但实际上还有一个更有实际的理由.NASM没有
    办法知道它产生的代码运行的处理器的类型;所以它自己不能决定它应该产生
    Jcc NEAR类型的指令,因为它不知道它正在386或更高一级的处理器上工作.相反,
    它把可能超出范围的短JNE指令替换成一个很短的JE指令,这个指令仅仅跳过
    一个JMP NEAR指令;对于低于386的处理器,这是一个可行的解决方案,但是对于
    有较好的分支预测功能的处理器很难有较好的效果,所以可代之以JNE NEAR.所
    以,产生什么的指令还是取决于用户,而不是汇编器本身.

    10.1.3 `ORG不正常工作.

    那些用bin格式写引导扇区代码的人们经常抱怨ORG没有按他们所希望的那样
    正常工作:为了把0xAA55放到512字节的引导扇区的末尾,使用NASM的人们会这样
    写:

    ORG 0

    ; some boot sector code

    ORG 510
    DW 0xAA55

    这不是NASM中使用ORG的正确方式,不会正常工作.解决这个问题的正确方法是使用
    TIMES操作符,就象这样:

    ORG 0

    ; some boot sector code

    TIMES 510-($-$$) DB 0
    DW 0xAA55

    TIME操作符会在输出中插入足够数量的零把汇编点移到510.这种办法还有一个
    好处,如果你意外地在你的引导扇区中放入了太多的内容,以致超出容量,NASM会
    在汇编时检测到这个错误,并报告.所以你最终就不必重汇编并去找出错误所在.

    10.1.4 `TIMES不正常工作.

    关于上面代码的另一个普遍性的问题是,有人这样写TIMES这一行:

    TIMES 510-$ DB 0

    因为$是一个纯数字,就像510,所以它们相减的值也是一个纯数字,可以很好地
    被TIMES使用.

    NASM是一个模块化的汇编器:不同的组成部分被设计为可以很容易的单独重用,所
    以它们不会交换一些不必要的信息.结果,BIN输出格式尽管被ORG告知.text
    段应当在0处开始,但是不会把这条信息传给表达式的求值程序.所以对求值程序
    来讲,$不是一个纯数值:它是一个从一个段基址开始的偏移值.因为$和510之
    间的计算结果也不是一个纯数,而是含有一个段基址.含有一个段基址的结果是不能
    作为参数传递给TIMES的.

    解决方案就象上一节所描述的,应该如下:

    TIMES 510-($-$$) DB 0

    在这里,$和$$是从同一个段基址的偏移,所以它们相减的结果是一个纯数,这
    句代码会解决上述问题,并产生正确的代码.

    10.2 Bugs

    我们还从来没有发布过一个带有已知BUG的NASM版本.但我们未知的BUG从来就是不
    停地出现.你发现了任何BUG,应当首先通过在
    `https://sourceforge.net/projects/nasm/(点击bug)的bugtracker提交给
    我们,如果上述方法不行,请通过1.2中的某一个联系方式.

    请先阅读2.2,请不要把列在那儿的作为特性的东西作为BUG提交给我们.(如果你认
    为这个特性很不好,请告诉我们你认为它应当被修改的原因,而不是仅仅给我们一
    个这是一个BUG)然后请阅读10.1,请不要把已经列在那里的BUG提交给我们.

    如果你提交一个bug,请给我们下面的所有信息.
    (这部分信息一般用户并不关心,在些省略,原文请参考NASM的英文文档.)

    附录A: Ndisasm
    -------------------

    反汇编器, NDISASM

    A.1 简介

    反汇编器是汇编器NASM的一个很小的附属品.我们已经拥有一个具有完整的指令
    表的x86汇编器,如果不把这个指令表尽最大可能地利用起来,似乎很可惜,所以
    我们又加了一个反汇编器,它共享NASM的指令表(并附加上一些代码)

    反汇编器仅仅产生二进制源文件的反汇编.NDISASM不理解任何目标文件格式,就
    象objdump,也不理解DOS .EXE文件,就象debug,它仅仅反汇编.

    A.2 开始: 安装.

    参阅1.3的安装指令.NDISASM就象NASM,也有一个帮助页,如果你在一个UNIX系统下,
    你可能希望把它放在一个有用的地方.

    A.3 运行NDISASM

    要反汇编一个文件,你可以象下面这样使用命令:

    ndisasm [-b16 | -b32] filename

    NDISASM可以很容易地反汇编16位或32位代码,当然,前提是你必须记得给它指定是
    哪种方式.如果-b开关没有,NDISASM缺省工作在16位模式下.-u开关也包含32位
    模式.

    还有两个命令行选项,-r打印你正运行的NDISASM的版本号,-h给你一个有关命
    令行选项的简短介绍.

    A.3.1 COM文件: 指定起点地址.

    要正确反汇编一个DOS.COM文件,反汇编器必须知道文件中的第一条指令是被装载
    到地址0x100处的,而不是0,NDISASM缺省地认为你给它的每一个文件都是装载到0
    处的,所以你必须告诉它这一点.

    -o选项允许你为你正反汇编的声明一个不同的起始地址.它的参数可以是任何
    NASM数值格式:缺省是十进制,如果它以$或0x开头,或以H结尾,它是十
    六进制的,如果以Q结尾,它是8进制的,如果是B结尾,它是二进制的.

    所以,反汇编一个.COM文件:

    ndisasm -o100h filename.com

    能够正确反汇编.

    A.3.2 代码前有数据: 同步.

    假设你正反汇编一个含有一些不是机器码的数据的文件,这个文件当然也含有一些
    机器码.NDISASM会很诚实地去研究数据段,尽它的能力去产生机器指令(尽管它们中
    的大多数看上去很奇怪,而且有些还含有不常见的前缀,比如:FS OR AX, 0x240A)
    然后,它会到达代码段处.

    假设NDISASM刚刚完成从一个数据段中产生一堆奇怪的机器指令,而它现在的位置正
    处于代码段的前面一个字节处.它完全有可能以数据段的最后一个字节为开始产生另
    一个假指令,然后,代码段中的第一条正确的指令就看不到了,因为起点已经跳过这条
    指令,这确实不是很理想.

    为了避免这一点,你可以指定一个同步点,或者可以指定你需要的同步点的数目(但
    NDISASM在它的内部只能处理8192个同步点).同步点的定义如下:NDISASM保证会到达
    这个同步点.如果它认为某条指令会跳过一个同步点,它会忽略这条指令,代之以一个
    DB.所以它会从同步点处开始反汇编,所以你可以看到你的代码段中的所有指令.

    同步点是用-s选项来指定的:它们以从程序开始处的距离来衡量,而不是文件位置.
    所以如果你要从32bytes后开始同步一个.COM文件,你必须这样做:

    ndisasm -o100h -s120h file.com

    而不是:

    ndisasm -o100h -s20h file.com

    就象上面所描述的,如果你需要,你可以指定多个同步记号,只要重复-s选项即可.

    A.3.3 代码和数据混合: 自动(智能)同步.

    假设你正在反汇编一个DOS软盘引导扇区(可能它含有病毒,而你需要理解病毒,这
    样你就可以知道它可能会对你的系统造成什么样的损害).一般的,里面会含有JMP
    指令,然后是数据,然后接下来才是代码,所以,这很可能会让NDISASM不能在数据与
    代码交接处找不到正确的点,所以同步点是必须的.

    另一方面,你为什么要手工指定同步点呢?你要找出来的同步点的地址,当然是可以
    从JMP中读取,然后可以用它的目标地址作为一个同步点,而NDISADM是否可以为你
    做到这一点?

    答案当然是可以:使用同步开关-a(自动同步)或-i(智能同步)会启用自动同步
    "模式.自动同步模式为PC相关的前向引用或调用指令自动产生同步点.(因为NDISASM
    是一遍的,如果它遇上一个目标地址已经被处理过的PC相关的跳转,它不能做什么.)

    只有PC相关的jump才会被处理,因为一个绝对跳转可能通过一个寄存器(在这种情况
    下,NDISASM不知道这个寄存器中含有什么)或含有一个段地址(在这种情况下,目标代
    码不在NDISASM工作的当前段中,所以同步点不能被正确的设置)

    对于一些类型的文件,这种机制会自动把同步点放到所有正确的位置,可以让你不必
    手工放置同步点.但是,需要强调的是自动模式并不能保证找出所有的同步点,你可能
    还是需要手工放置同步点.

    自动同步模式不会禁止你手工声明同步点:它仅仅只是把自动产生的同步点加上.同
    时指定-i和-s选项是完全可行的.

    关于自动同步模式,另一个需要提醒的是,如果因为一些讨厌的意外,你的数据段中的
    一些数据被反汇编成了PC相关的调用或跳转指令,NDISASM可能会很诚实地把同步点放
    到所有的这些位置,比如,在你的代码段中的某条指令的中间位置.同样,我们不能为此
    做什么,如果你有问题,你还是必须使用手工同步点,或使用-k选项(下面介绍)来禁
    止数据域的反汇编.

    A.3.4 其他选项.

    -e选项通过忽略一个文件开头的N个bytes来跳过一个文件的文件头.这表示在反汇
    编器中,文件头不被计偏移域中:如果你给出-e10 -o10,反汇编器会从文件开始的
    10byte处开始,而这会对偏称域给出10,而不是20.

    -k选项带有两个逗号--分隔数值参数,第一个是汇编移量,第二个是跳过的byte数.
    这是从汇编偏移量处开始计算的跳过字节数:它的用途是禁止你需要的数据段被反汇
    编.

    A.4 Bug和改进.

    现在还没有已知的bug.但是,如果你发现了,并有了补丁,请发往`jules@dsf.org.uk
    或`anakin@pobox.com, 或者在`https://sourceforge.net/projects/nasm/上的
    开发站点,我们会改进它们,请多多给我们改进和新特性的建议.

    将来的计划包括能知道特定的指令运行在那种处理器上,并能标出那些对于某些处理
    器来说过于高级的指令(或是FPU指令,或是没有公开的操作符, 或是特权保护模式
    指令,或是其它).

    感谢所有的人们!

    我希望NDISASM对一些人来说是有用的,包括我.:-)

    我不推荐把NDISASM单独出来,以考察一个反汇编器的工作性能,因为到目前为止,据
    我所知,它不是一个高性能的反汇编器,你应当明确这一点.