24

这是一种装饰性的系法,形似五角星,除了“神奇”国度以外,五角星广泛地应用于各式各样的旗帜上,最打眼的便是美国国旗上的五十颗星星。这种系法对扁平的鞋带效果最好,因为有几个鞋孔需要穿插两次。


暗色部分在下面


控制“底角”将五角星居中


在这个图片中,顶部“横杠”上移了一个鞋孔以延长鞋带剩余部分



系法

1.从上方直接穿过(灰色部分)第三组鞋孔。

2.从底部穿越到最后一组鞋孔(跳过两个孔),这些是五角星的“底角”。

3.左路大军(蓝色)按如下路线“推进”:斜向进入右上方第三个鞋孔,然后从底部穿越至五角星的“右下角”,斜上左上方第三个鞋孔。

4.左路继续进军形成顶部的”横杠“:从底部穿越至上方鞋孔,然后推进到右边对应的位置,最终从底部浮出,到达右方鞋孔出口。

5.顶部“横杠”形成后,右路大军(黄色)斜上至“横杠”,穿越并再次迂回到左下角鞋孔。

6.右路最终从底部到达左方鞋孔出口。


性能

优点:好看

缺点:松散,很难勒紧


对比长度 = 135%

增加:鞋带利用面积(+35%)

       鞋带长度需求(+17%)

减少:鞋带剩余部分(-32%)


注意:

要得到一个整洁的五角星可能需要一些试验,这取决于鞋子宽度和鞋孔大小。

运用“底角”来减少鞋带不可见的部分,这样可以将五角星上移。顶部“横杠”同样可以上移来增加鞋带剩余部分。


翻译:龙之冰点
原文地址:http://www.fieggen.com/shoelace/pentagramlacing.htm

2009.5.26
这种系法经过我反复实验,还是不适合我的鞋子,主要是鞋孔不够大的问题,很难穿插两次,系好后很紧迫,不方便调整,而横杠部分总是达不到想要的效果,导致美观也出现问题。

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

                                                                 (完)
22
第七章: 编写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寄存器中的值从来不改变,总是给出函数  地址的段地真正部分),函数调用就使用普通的near'CALL'指令,返回  使用'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符号在汇编语言中不含有一个      前导的下划线。

      老的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'.