本章主要介绍在编写运行在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符号在汇编语言中不含有一个
前导的下划线。
老的Linux'a.out'C编译器,所有的'Win32'编译器,‘DJGPP'和'NetBSD'
'FreeBSD'都使用前导的下划线;对于这些编译器来讲,7.4.1中给出的宏
'cextern'和'cglobal'还会正常工作。对于'ELF'来讲,下划线是没有必要的。
8.1.2 函数定义和函数调用。
32位程序中的C调用转化如下所述。在下面的描述中,_caller_和_callee_用来o
表示调用函数和被调用函数。
(*) caller把函数的参数按相反的顺序(从右到左,这样的话,第一个参数被最
后一个压栈)依次压栈
(*) 然后,caller执行一个near'CALL'指令把控制权传给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单独出来,以考察一个反汇编器的工作性能,因为到目前为止,据
我所知,它不是一个高性能的反汇编器,你应当明确这一点.
(完)