第九章: 混合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单独出来,以考察一个反汇编器的工作性能,因为到目前为止,据我所知,它不是一个高性能的反汇编器,你应当明确这一点.
(完)
近期评论