存储空间分配.doc

上传人:本田雅阁 文档编号:2520736 上传时间:2019-04-05 格式:DOC 页数:11 大小:50.52KB
返回 下载 相关 举报
存储空间分配.doc_第1页
第1页 / 共11页
存储空间分配.doc_第2页
第2页 / 共11页
存储空间分配.doc_第3页
第3页 / 共11页
存储空间分配.doc_第4页
第4页 / 共11页
存储空间分配.doc_第5页
第5页 / 共11页
点击查看更多>>
资源描述

《存储空间分配.doc》由会员分享,可在线阅读,更多相关《存储空间分配.doc(11页珍藏版)》请在三一文库上搜索。

1、第4章 存储空间分配$Revision: 2.3 $Date: 1999/06/15 03:30:36 $链接器或加载器的首要任务是存储分配.一旦分配了存储空间后,链接器就可以继续进行符号绑定和代码调整.在一个可链接目标文件中定义的多数符号都是相对于文件内的存储区域定义的,所以只有存储区域确定了才能够进行符号解析.与链接的其它方面情况相似,存储分配的基本问题是很简单的,但处理计算机体系结构和编程语言语义特性的细节让问题复杂起来.存储分配的大多数工作都可以通过优雅和相对架构无关的方法来处理,但总有一些细节需要特定机器的专门技巧来解决.段和地址每个目标或可执行文件都会采用目标地址空间的某种模式.通

2、常这里的目标是目标计算机的应用程序地址空间,但某些情况下(例如共享库)也会是其它东西.在一个重定位链接器或加载器中的基本问题是要确保程序中的所有段都被定义并具有地址,并且这些地址不能发生重叠(除非有意这样).每一个链接器输入文件都包含一系列各种类型的段.不同类型的段以不同的方式来处理.通常,所有相同类型的段,诸如可执行代码段,会在输出文件中被合并为一个段.有时候段是在其它段的基础上合并得到的(如Fortran的公共块),以及在越来越多的情况下(如共享库和C+专有特性),链接器本身会创建一些段并将其放置在输出中.存储布局是一个两遍的过程,这是因为每个段的地址在所有其它段的大小未确定前是无法分配的

3、.简单的存储布局在一种简单而不现实的情形下,链接器的输入文件包含一系列的模块,将它们称为M1, M2, . Mn,每一个模块都包含一个单独的段,从位置0开始长度依次为L1, L2, . Ln,并且目标地址空间也是从0开始.如图1所示.-图4-1:单独段的存储空间分配从位置0开始的多个段按照一个跟着另一个的方式重定位-链接器或加载器依次检查各个模块,按顺序分配存储空间.模块Mi的起始地址为从L1到Li-1相加的总和,链接得到的程序长度为从L1到Ln相加的总和.多数体系结构要求数据必须对齐于字边界,或至少在对齐时运行速度会更快些.因此链接器通常会将Li扩充到目标体系结构最严格的对齐边界(通常是4或

4、8个字节)的倍数.例1:假定一个称为main的主程序要与三个分别称为calif,mass和newyork的子例程链接(按照地理位置划分风险投资).每个例程的大小为(16进制数字):名称尺寸-ain1017calif 920ass 615newyork1390假定从16进制的地址1000处开始分配存储空间,并且要求4字节对齐,那么存储分配的结果可能是:名称位置-ain1000 - 2016calif2018 - 2937ass2938 - 2f4cnewyork2f50 - 42df由于对齐的原因,2017处的一个字节和2f4d处的三个字节被浪费了,但无须忧虑.多种段类型除最简单格式外所有的目标

5、格式,都具有多种段的类型,链接器需要将所有输入模块中相应的段组合在一起.在具有文本和数据段的UNIX系统上,被链接的文件需要将所有的文本段都集中在一起,然后跟着的是所有的数据,在后面是逻辑上的BSS(即使BSS在输出文件中不占空间,它仍然需要分配空间来解析BSS符号,并指明当输出文件被加载时要分配的BSS空间尺寸).这就需要两级存储分配策略.现在每一个模块Mi具有大小为Ti的文本段,大小为Di的数据段,以及大小为Bi的BSS段,如图2所示.-图4-2:多种段的存储分配按类型将文本,数据和BSS段分别归并-在读入每个输入模块时,链接器为每个Ti,Di,Bi按照(就像是)每个段都各自从位置0处开始

6、的方式分配空间.在读入了所有的输入文件后,链接器就可以知道这三种段各自总的大小Ttot,Dtot和Btot.由于数据段跟在文本段之后,链接器将Ttot加到每一个数据段所分配的地址上,接着,由于BSS跟在文本和数据段之后,所以链接器会将Ttot,Dtot的和加到每一个BSS段分配的地址上.同样,链接器通常会将分配的大小按照对齐要求扩充补齐.段与页面的对齐如果文本和数据被加载到独立的内存页中,这也是通常的情况,文本段的大小必须扩充为一个整页,相应的数据和BSS段的位置也要进行调整.很多UNIX系统都使用一种技巧来节省文件空间,即在目标文件中数据紧跟在文本的后面,并将那个(文本和数据共存的)页在虚拟

7、内存中映射两次,一次是只读的文本段,一次是写时复制(copy-on-write)的数据段.这种情况下,数据段在逻辑上起始于文本段末尾紧接着的下一页,这样就不需扩充文本段,数据段也可对齐于紧接着文本段后的4K(或者其它的页尺寸)页边界.例2:我们将例1扩展,使得每个例程都有文本,数据和BSS段.字对齐要求还是4个字节,但页大小为0x1000字节.名称文本段数据段BSS段-ain1017 32050calif 920 217100ass 615 300840newyork139012131400(均为16进制数字)链接器首先分配文本段,然后是数据段,接着是BSS.注意这里数据段起始于页边界0x50

8、00,但BSS紧跟在数据的后面,这是因为在运行时数据和BSS在逻辑上是一个段.名称文本段数据段BSS段-ain1000-20165000-531f695c-69abcalif2018-29375320-544669ac-6aabass2938-2f4c5448-57476aac-72ebnewyork2f50-42df5748-695a72ec-86eb在0x42e0到0x5000之间的页结尾处浪费了一些空间.虽然BSS段的结束位置在页面中部的0x86eb处,但程序们普遍都会紧跟其后分配堆空间.公共块和其它特殊段上面这种简单的段分配策略在链接器处理的80%的存储分配中都工作的很好,但剩下的那些

9、情况就需要用特殊的技巧来处理了.这里我们来看看比较常见的几个.公共块公共块存储是一个可以追溯到50年代Fortran I时的特性.在最初的Fortran系统中,每一个子程序(主程序,函数或者子例程)都有各自局部声明和分配的标量和数组变量.同时还有一个各例程都可以使用的存储标量和数组的公共区域.公共块存储被证明是非常有用的,并且在后续Fortran中单一的公共块(就是我们现在知道的空白公共块,即它的名称是空白的)已经普及为多个可命名的公共块,每一个子程序都可以声明它们所用的公共块.在最初的40年中,Fortran不支持动态存储分配,公共块是Fortran程序用来绕开这个限制的首要工具.标准For

10、tran允许在不同例程中声明不同大小的空白公共块,其中最大的尺寸最终生效.Fortran系统们无一例外的都将它扩展为允许以不同的大小来声明所有类型的公共块,同样还是最大的尺寸最终生效.大型的Fortran系统经常会超过它们所运行系统的内存容量限制,在没有动态内存分配时,程序员不得不频繁的重新创建软件包,压缩尺寸来解决软件包遇到的此类问题.在一个软件包中除一个之外的其它子程序都将公共块声明为只有一个元素的数组.剩下的那个子程序声明所有公共块的实际大小,并在程序启动时将这些尺寸都保存在其余软件包可以使用的(在另一个公共块中的)变量中.这样就可以通过修改和重新编译定义这些公共块的一个例程,来调整公共

11、块的尺寸,然后再重新链接.从60年代开始Fortran增加了BLOCK DATA数据类型来为任意公共块(空白公共块除外,这是为数不多的限制)的部分或全部来指明局部初始数据值,这在某种程度上更复杂了.通常用来初始化公共块的在BLOCK DATA中的公共块尺寸,也在链接时被用来当作该公共块的实际大小.在处理公共块时,链接器会将输入文件中声明的每个公共块当作一个段来处理,但并不会将这些段串联起来,而是将相同名称的公共块重叠在一起.这里会将声明的最大的尺寸作为段的大小,除非在某一个输入文件中存在该段的已初始化的版本.在某些系统上,已初始化的公共块是一个单独的段类型,而在另一些系统上它可能只是数据段的一

12、部分.UNIX链接器总是一贯支持公共块,甚至从最早版本的UNIX都具有一个Fortran子集的编译器,并且UNIX版本的C语言传统上会将未初始化的全局变量作为公共块对待.但在ELF之前的UNIX目标文件只有文本,数据和BSS段,没有办法直接声明一个公共块.作为一个特殊技巧,链接器将未定义但具有非零初值的符号当作是公共块,而该值就是公共块的尺寸.链接器将遇到的此类符号中最大的数值作为该公共块的尺寸.对于每一个公共块,它在输出文件的BSS段中定义了相应的符号,在每一个符号的后面分配所需要的空间.-图4-3:Unix公共块在BSS末尾的公共块-C+重复代码消除在某些编译系统中,C+编译器会由于虚函数

13、表,模板和外部inline函数而产生大量的重复代码.这些特性的设计是隐含的期望那种程序所有部分都可以被运行的环境.一个虚函数表(通常简称为vtbl)包含一个类的所有虚函数(可以被子类覆盖的例程)的地址.每个带有任何虚函数的类都需要一个vtbl.模板本质上就是以数据类型为参数的宏,并能够根据特定的类型参数集可以扩展为特定的例程.确保是否存在一个对普通例程的引用可供调用是程序员的责任,就是说对如hash(int)和hash(char *)每一类hash函数都有确定的定义,hash(T)模板可以根据程序中使用hash函数时不同的参数数据类型创建对应的hash函数.在每个源代码文件都被单独编译的环境中

14、,最简单的方法就是将所有的vtbl都放入到每一个目标文件中,扩展所有该文件用到的模板例程和外部inline函数,这样做的结果就是产生大量的冗余代码.最简单的方法就是在链接时仍然将那些重复代码保留着.那么得到的程序肯定可以正确的工作,但代码会膨胀的比理想尺寸大三倍或者更多.在那些使用简单链接器的系统上,某些C+系统使用了一种迭代链接的方法,并采用独立的数据库来管理将哪些函数扩展到哪些地方,或者添加progma(向编译器提供信息的程序源代码)向编译器反馈足够的信息以仅仅产生必须的代码.我们将在第11章涉及这些.最近的很多C+系统已经正面解决了这个问题,要么是让链接器更聪明一些,要么就是将链接器整合

15、到程序开发环境的其它部分中(后一种方法我们在第11章还会涉及到).链接器的方法是让编译器在每个目标文件中生成所有可能的重复代码,然后让链接器来识别和消除重复的代码.MS Windows链接器为代码区段定义了COMDAT标志来告诉链接器忽略除明确命名区段外的所有重复区段.编译器会根据模板给每个区段命名,名字中包含了参数类型,如图4所示.-图4-4:WindowsIMAGE_COMDAT_SELECT_NODUPLICATES 1 Warn if multiple identically namedsections occur.IMAGE_COMDAT_SELECT_ANY 2 Link one

16、identically named section,discard the rest.IMAGE_COMDAT_SELECT_SAME_SIZE 3 Link one identically named section,discard the rest. Warn if a discardedsection isnt the same size.IMAGE_COMDAT_SELECT_EXACT_MATCH 4 Link one identically named section,discard the rest. Warn if a discardedsection isnt identic

17、al in size andcontents. (Not implemented.)IMAGE_COMDAT_SELECT_ASSOCIATIVE 5 Link this section if another specifiedsection is also linked.-GNU链接器是通过定义一个link once类型的区段(与公共块很相似)来解决这个模板的问题的.如果链接器看到诸如.gnu.linkonce.name之类的区段名称,它会将第一个明确命名的此类区段保留下来并忽略其它冗余区段.同样编译器会将模板扩展到一个采用简化模板名称的.gnu.linkonce区段中.这种策略工作的相当不

18、错,但它并不是万能的.例如,它不能保护功能上并不完全相同的vtbl和扩展模板.一些链接器尝试去检查被忽略的和保留的区段是否是每个字节都相同.这种方法是很保守的,但是如果两个文件采用了不同的优化选项,或编译器的版本不同,就会产生报错信息.另外,它也不能尽可能多的忽略冗余代码.在多数C+系统中,所有的指针都具有相同的内部表示,这意味着一个模板的具有指向int类型指针参数的实例和指向float类型指针参数的实例会产生相同的代码(即使它们的C+数据类型不同).某些链接器也尝试忽略那些和其它区段每个字节都相同的link-once区段,哪怕它们的名字并不是完全的相同,但这个问题仍然没有得到满意的解决.虽然

19、我们在这里只是讨论了模板的问题,但相同的问题也会发生在外部inline函数,缺省构造,复制和赋值例程中,也可以采用相同的方法处理.初始化和终结另一个问题并不仅限于C+,但在C+上尤为严重,就是初始化和终结代码(initializers and finalizers).一般来说,如果它们可以在程序启动的时候可以运行一个初始化例程,并在程序结束的时候运行一个终结例程,那把它们写成库会更容易些.C+允许静态变量.如果一个变量的类具有构造函数,那这个构造函数在程序启动时会被调用来对初始化变量,同样如果一个变量的类具有析构函数,那析构函数也会在程序退出时被调用.有很多办法可以在不需要链接器支持的情况下做

20、到这一点,我们将会在第11章讨论到,但现代链接器通常都会直接支持该特性.通常的方法是将每个目标文件中的初始化代码都放入一个匿名的例程中,然后将指向该例程的指针放置在名为.init(或其它相近名字)的段中.链接器将所有的.init段串联在一起,因此就创建了一个指向所有这些初始化例程的指针列表.程序的初始化部分只需要遍历该列表依次调用所有例程即可.退出时的代码可以采用相同方法,只是段的名字改为了.fini.实践证明这种方法也不是完全令人满意的,因为有一些初始化代码要求比另外一些更早的运行.C+定义指出应用程序级的构造函数运行顺序是不确定的,但I/O和其它系统库的构造函数需要在应用程序自己的构造函数

21、之前执行.完美的方法应当是让每一个初始化例程都精确的列出它们的依赖关系,并在此基础上进行拓扑排序.BeOS操作系统的动态链接器就是这么做的,使用到了库的引用依赖关系(如果库A依赖于库B,那么库B的初始化代码就可能需要先运行).一个更简单的近似方法是设置多个用于初始化的段,如.init和.ctor,这样启动程序首先为所有库级初始化调用.init中的例程,然后为C+的构造函数调用.ctor中的例程.同样的问题出现在程序结束时,对应的段为.dtor和.fini.有一个系统甚至还允许程序员设置优先级编号,0至127为用户代码,128至255是系统库,链接器在合并代码之前会先将初始化和终结代码按优先级编

22、号排序,最高优先级的初始化代码最先运行.但这仍不能令人完全满意,因为构造函数之间会存在顺序依赖关系,从而产生非常难以调试的错误,但在这里C+将避免这些错误的责任交给了程序员.该策略的一个变种是将实际的初始化代码放在.init段中,当链接器合并它们的时候该段会成为完成所有初始化工作的inline代码.只有少量系统进行了这种尝试,但在不支持直接寻址的计算机上是很难让它工作的,因为从每个目标文件中提取出来的代码块还要能够对它们原本文件中的数据进行寻址,通常这都需要寄存器来指向可以指向寻址数据的表.匿名例程采用和其它例程相同的方式来初始化它们的寻址过程,借助已有的方案来减少寻址的问题.IBM伪寄存器I

23、BM主机系统的链接器提供了一种称为外部模拟(external dummy)区段或伪寄存器(pseudo-registers)的有趣特性.360是较早的无直接寻址的主机架构之一,这就意味着实现小数据区域共享要付出昂贵的开销.每一个引用全局对象的例程都需要一个4字节的指针指向该对象,如果这个对象只有开头4个字节那么大的话,这将是相当大的开销.例如PL/1程序对每一个打开的文件和其它全局对象都需要一个指针(虽然PL/1应用程序的程序员无法访问伪寄存器,但它是唯一使用伪寄存器的高级语言.它使用伪寄存器指向打开文件的控制块这样应用程序就可以包括进那些对I/O系统的inline调用).一个相关的问题是OS

24、/360不支持我们现在所说的那种称为进程/任务级本地存储的东西,并且对共享库只提供非常有限的支持.如果两个作业运行同样的程序,或者这个程序被标注为可重入(这时它们共享整个程序,代码和数据),或者标注为不可重入(这时不共享任何东西).所有的程序都被加载到相同的地址空间,因此相同程序的多个实例必须标注出实例本身数据的范围(360系统不具备硬件内存重定位功能,尽管370支持了,但也知道OS/VS操作系统的若干个版本之后系统才提供进程独立的地址空间).伪寄存器可以帮助解决这些问题,如图5所示.每一个输入文件都可以声明(多个)伪寄存器,也称为外部模拟区段(360系统的汇编语言中,它与结构体的声明很相似)

25、.每个伪寄存器都有名字,长度和对齐要求.在链接时,链接器将所有的伪寄存器都收集到一个逻辑段中,将最大的尺寸和最严格的对齐要求施加于每个伪寄存器,并为它们分配在该逻辑段中不会相互重叠的偏移量.但链接器不会为伪寄存器段分配空间.它只是计算该段的大小,并将其存储在程序的数据段中以特殊的CXD(cumulative external dummy,即重定位项)标识的位置.当引用一个伪寄存器时,程序代码还需要另一个特殊的XD(external dummy),它是用来指示将偏移量放置在哪一个该伪寄存器所属逻辑段内的重定位类型.程序的初始化代码为伪寄存器动态的分配空间,使用CXD可以知道需要多大的空间,并按惯

26、例将这个空间的地址存放在寄存器12中,在整个程序运行期间都不会改变.程序中的任何一部分都可以通过将寄存器12的值与某个伪寄存器对应的XD的值相加得到该伪寄存器的地址.一般都是通过load和store指令来完成的,将R12(寄存器12)作为索引寄存器与嵌入到指令的地址替换域中的XD项相加(地址替换域只有12位,但由于XD将16位半字的高4位保持为0,即基址寄存器为0,所以仍然可以产生正确的结果).-图4-5:精灵寄存器通过R12指向一串地址块.各种例程通过偏移量引用它们.-这样的结果就是程序的所有部分都可以load,store和其它RX格式指令来直接访问所有的伪寄存器.如果一个程序存在多个活动的

27、实例,每个实例就可以通过采用不同的R12值来分配独立的空间.尽管最初引用伪寄存器的原因现在大多数都已经被废弃了,但为链接器提供可以高效访问线程本地地址的方法确实一个非常好的思想,并且仍然出现在很多现代操作系统中,其中最著名的就是Windows.同样,现代的RISC机器也分享了360系统有限的寻址范围,因此需要使用内存指针表来寻址任意的内存地址.在很多RISC UNIX系统上,编译器为每个模块创建两个数据段,一个是通常的数据段,另一个是小(small)数据段,即大小低于某一个尺寸阀值的静态对象.链接器将所有的小数据段收集在一起,然后让程序的启动代码将合并的小数据段的地址放入一个保留的寄存器中.这

28、样就可以通过和这个寄存器相关的基址寻址来直接引用这些小数据.要注意,与伪寄存器不同,小数据的存储空间既会被链接器分配,也会被链接器放置到输出中,在每个程序中只有一份小数据.某些UNIX系统支持线程,但线程级的存储是特定的程序代码完成的,不需要链接器的特殊帮助.特殊的表链接器分配存储的最后一个资源是链接器本身.尤其是当应用程序使用共享库或者重叠技术时,链接器会创建由指针,符号或其它别的数据构成的多个段来在运行时支持库或者重叠.一旦这些库被建立了,链接器会按照对待任何其它段的方式来为它们分配存储空间.X86分段的存储分配8086和80286的分段内存寻址的怪癖要求导致了少量特殊的东西.x86 OM

29、F目标文件给每个段都有一个名字和可选的类别.所有具有相同名字的段,会根据由编译器或者汇编器设置的一些标志位来合并到一个大的段中,并且所有类别相同的段都会被连续的分配在一个块中.编译器或汇编器使用类别名来标注段的类型(诸如代码或静态数据),因此链接器可以将给定类别的所有的段分配在一起.当某个类别的所有段总长小于64K时,它们可以被当作使用一个段寄存器的单独寻址组来对待,这样可以节省客观的时间和空间.图6所示为一个由三个输入文件链接而成的程序,三个输入文件依次为main,able和baker.文件main中包含段MAINCODE和MAINDATA,able中包含段ABLECODE和ABLEDATA

30、,baker中包含段BAKERCODE,BAKERDATA和BAKERLDATA.每一个代码段都是CODE类别,数据段都是DATA类别,但大数据BAKERLDATA不赋予类别.在链接好的程序中,假定CODE段最大64K,它们在运行时可以当作单独的段来对待,可以使用short(而不是far)调用和跳转指令,以及一个不变的CS段代码寄存器.同样如果所有的DATA段可以装在64K中,则它们也可以当作单一的段来对待,使用short的内存引用指令和一个不变的DS数据段寄存器.BAKERLDATA段在运行时作为一个独立的段处理,程序代码会加载一个段寄存器(通常是ES)来指向它.-图4-6:X86CODE类

31、别的MAINCODE,ABLECODE和BAKERCODE段DATA类别的MAINDATA,ABLEDATA和BAKERDATA段单独的BAKERLDATA段-实模式和286保护模式的程序几乎是以相同的方式来链接的.主要的不同在于链接器一旦在保护模式程序中生成链接好的段,链接器就完成工作了,只有在程序加载时才会赋予实际的内存地址和段号.在实模式中,链接器还有额外的一步就是为段分配线性地址,并相对于程序起始位置为这些段分配段落(paragraph)号.然后在加载的时候,程序加载器必须调整实模式程序中所有的段落号(paragraph number)或者保护模式程序中所有的段号(segent num

32、ber)以反映程序被加载的实际位置.链接器控制脚本传统上链接器可以允许用户对输出数据进行有限的控制.由于链接器已经开始要面对内存组织非常复杂的目标环境,诸如众多的嵌入式处理器和目标环境,因此就非常必要对目标地址空间和输出文件中的数据提供更加精确的控制.具有一系列固定段的简单链接器通常具有可以指定各个段基地址的开关参数,这样程序就可以被加载到非标准的应用环境中(操作系统内核通常会用到这些开关参数).有一些链接器具有数量庞大的命令行开关参数,由于系统经常会限制命令行的长度,因此经常将这些命令行逻辑上连续的放置在一个文件中.例如,微软的链接器在文件中为每个区段设置特性时最多可以采用大约50个命令行开

33、关选项,包括输出的基地址和一系列其它输出相关的细节.其它的链接器定义了可以控制链接器输出的脚本语言.GNU链接器,也定义了这么一种具有一长串命令行参数的语言.图7所示为可以在系统5版本3.2(System V Release 3.2)的系统上(如SCO UNIX)产生COFF可执行程序的一个简单链接脚本示例.-图4-7:生成COFF可执行程序的GNU链接器控制脚本OUTPUT_FORMAT(coff-i386)SEARCH_DIR(/usr/local/lib);ENTRY(_start)SECTIONS.text SIZEOF_HEADERS : *(.init)*(.text)*(.fin

34、i)etext = .;.data 0x400000 + (. & 0xffc00fff) : *(.data)edata = .;.bss SIZEOF(.data) + ADDR(.data) :*(.bss)*(COMMON)end = .;.stab 0 (NOLOAD) : .stab .stabstr 0 (NOLOAD) : .stabstr -开始的几行描述了输出的格式(必须是编译进链接器的格式表中存在的),查找目标代码库的位置,和缺省入口点的名称(本示例中为_start).然后它列出了输出文件中的区段.在区段名后面是一个指明区段开始地址的可选数值.因此可以看出,.text区段

35、紧跟在文件头部后面,输出文件中的.text区段包含了所有输入文件中的.init区段,所有的.text区段和所有的.fini区段.链接器定义了符号etext作为.fini区段后面的地址.然后脚本设置了.data区段,强制将其起始地址设置为文本区段后面的4KB对齐的地址0x400000,该区段中包含了所有输入文件中的.data区段,并紧跟其后定义了edata符号.然后是紧跟在数据段后面的.bss区段,它包括了所有输入文件中的.bss区段和公共块,并将bss段的末尾用符号end标识(COMMON是该链接语言的一个关键字).在那之后的两个区段是从输入文件相应位置收集的众多符号表项,但只有调试器会查看这

36、些符号,因此在运行时不会被加载.链接器脚本语言比这个简单的例子要复杂得多,足以描述从简单的DOS可执行程序到Windows PE可执行程序以至到复杂的重叠管理的各种类型.嵌入式系统的存储分配嵌入式系统的存储分配与我们到现在为止已看到的策略相近,只是由于程序所运行的复杂的地址空间而复杂了一些.嵌入式系统链接器提供的脚本语言可以让程序员地址空间的区域,并将特定的段或目标文件分配到这些区域中,并可以指明各区域中每个段的地址对齐要求.诸如DSP这样的专用处理器的链接器还要支持各处理器的特殊特性.例如,Motorola 5600X DSP系列支持循环缓冲区必须对齐在不小于缓冲区大小的2的幂次的地址上.5

37、6K目标格式为这些缓冲区有一个特殊的段类型,链接器会自动的将它们分配到正确的边界上,并尽量减小(作者笔误 )未使用的空间.实际中的存储分配现在我们看看几种流行链接器的存储分配策略,作为本章的结束.Unix a.out链接器的存储分配策略ELF之前的UNIX链接器的存储分配策略只比本章开头的理想实例稍微复杂一点,这是因为各个段在链接之前已经知道了,如图8所示.每个输入文件具有文本,数据和BSS段,也可能有伪装为外部符号的公共块.链接器从每个输入文件和库目标文件中收集文本,数据和BSS的大小.在读取了所有的目标文件之后,任何未解析的具有非零值外部符号都被放入公共块中,并在BSS尾部分配空间.-图4

38、-8:a.out链接从输入目标代码和库目标代码中各文本,数据和BSS/公共块合并而成的三个大段-这里,链接器可以为各个段直接赋予地址.文本段根据所创建的不同a.out格式起始于一个固定的位置,或者是0位置(最老的格式),或者是0位置的下一页(NMAGIC格式),或者是一页再加上a.out头部(QMAGIC).数据段可以直接跟在文本段后面(旧的非共享a.out格式),或起始于文本段后下一页的边界处(NMAGIC格式).在每种格式中,BSS都紧跟在数据段后面.在每一个段内部,将各输入文件中的段排列在前一个段后面字对齐的边界处.ELF中的存储分配策略ELF链接要比a.out复杂一些,因为输入文件中的

39、各个段可以是任意大小的,链接器必须将输入段(ELF术语中的段)转换为可加载的段(ELF术语中的段).链接器还要创建程序加载器需要的程序头部,和动态链接所需的一些特殊区段,如图9所示.-图4-9:ELF链接摘自TIS ELF文档页2-7和2-8所示为输入中的段转换到输出中的段-ELF目标文件具有传统的文本,数据和BSS区段,现在拼写为.text,.data和.bss.经常还会包含.init和.fini(启动和退出时的代码),和其它一些琐碎的东西.rodata和.data1在某些编译器中被用来表示只读数据和out-of-line数据(有些编译器也有对应只读out-of-line数据的.rodata

40、1区段).在诸如MIPS这样地址偏移量受限的RISC系统中,还有.sbss和.scommon区段,即小的BSS和公共块,有利于小的对象组合到单个可以直接寻址的区域,就像我们在上面讨论伪寄存器时说到的那样.在GNU C+系统中,还可以会有可以被括入文本,只读数据和数据段中的linkonce区段.如果不考虑众多的区段类型,那么链接过程都是一样的.链接器将各个输入文件和库目标文件中的同类型区段收集在一起.链接器还会标注出哪些符号会在运行时从共享库中解析,并创建.interp,.got,.plt和符号表区段来支持运行时链接(我们将细节的讨论推迟到第9章).一旦这些都完成了,链接器会按照传统的顺序来分配

41、空间.与a.out不同,ELF格式不会从0位置加载任何东西,而是从地址空间的中间部位来加载,这样栈可以在文本段以下向下增长,堆可以在数据段末尾以上向上增长,以更加紧凑的利用地址空间.在386系统上,文本的基地址是0x08048000,这样既可以允许位于文本以下的合理大的栈空间,同时将0x08000000以上的空间留出来,允许多数程序将它用来创建单一的二级页表(回想一下在386上,每一个二级页表可以映射大小为0x00400000的地址空间).ELF使用QMAGIC的技巧将头部包括到文本段内,所以实际的文本段起始于ELF头部和程序头部表之后,典型的位于文件偏移量0x100处.然后再将.interp

42、(动态链接器的逻辑链接,需要首先被运行),动态链接器符号表区段,.init,.text,以及link-once文本和只读数据分配到文本段中.接下来是数据段,逻辑上起始于文本段末尾的下一个页(因为在运行时该页会同时被映射为文本段的最后一页和数据段的第一页).链接器分配各种.data区段和link-once数据和.got区段,以及一些平台上会用到的.sdata小数据和.got全局偏移量表.最后是BSS区段,逻辑上紧跟在数据的后面,由.sbss开始(如果有的话,将它放在.sdata和.got的后面),然后是BSS段和公共块.Windows链接器的存储分配策略Windows Pe文件的存储分配策略比ELF文件还要简单一点,这是因为PE的动态链接模式需要

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 其他


经营许可证编号:宁ICP备18001539号-1