存储空间分配11页文档资料文档格式.docx
- 文档编号:22478383
- 上传时间:2023-02-04
- 格式:DOCX
- 页数:27
- 大小:32.21KB
存储空间分配11页文档资料文档格式.docx
《存储空间分配11页文档资料文档格式.docx》由会员分享,可在线阅读,更多相关《存储空间分配11页文档资料文档格式.docx(27页珍藏版)》请在冰豆网上搜索。
在一种简单而不现实的情形下,链接器的输入文件包含一系列的模块,将它们称为M1,
M2,...Mn,每一个模块都包含一个单独的段,从位置0开始长度依次为L1,L2,...Ln,并
且目标地址空间也是从0开始.如图1所示.
---------------------------------------------------------------------------
图4-1:
单独段的存储空间分配
从位置0开始的多个段按照一个跟着另一个的方式重定位
链接器或加载器依次检查各个模块,按顺序分配存储空间.模块Mi的起始地址为从L1
到Li-1相加的总和,链接得到的程序长度为从L1到Ln相加的总和.
多数体系结构要求数据必须对齐于字边界,或至少在对齐时运行速度会更快些.因此
链接器通常会将Li扩充到目标体系结构最严格的对齐边界(通常是4或8个字节)的倍数.
例1:
假定一个称为main的主程序要与三个分别称为calif,mass和newyork的子例程
链接(按照地理位置划分风险投资).每个例程的大小为(16进制数字):
名称尺寸
-------------------
ain1017
calif920
ass615
newyork1390
假定从16进制的地址1000处开始分配存储空间,并且要求4字节对齐,那么存储分配
的结果可能是:
名称位置
-------------------------
ain1000-2019
calif2018-2937
ass2938-2f4c
newyork2f50-42df
由于对齐的原因,2019处的一个字节和2f4d处的三个字节被浪费了,但无须忧虑.
多种段类型
除最简单格式外所有的目标格式,都具有多种段的类型,链接器需要将所有输入模块
中相应的段组合在一起.在具有文本和数据段的UNIX系统上,被链接的文件需要将所有的
文本段都集中在一起,然后跟着的是所有的数据,在后面是逻辑上的BSS(即使BSS在输出
文件中不占空间,它仍然需要分配空间来解析BSS符号,并指明当输出文件被加载时要分配
的BSS空间尺寸).这就需要两级存储分配策略.
现在每一个模块Mi具有大小为Ti的文本段,大小为Di的数据段,以及大小为Bi的BSS
段,如图2所示.
图4-2:
多种段的存储分配
按类型将文本,数据和BSS段分别归并
在读入每个输入模块时,链接器为每个Ti,Di,Bi按照(就像是)每个段都各自从位置
0处开始的方式分配空间.在读入了所有的输入文件后,链接器就可以知道这三种段各自总
的大小Ttot,Dtot和Btot.由于数据段跟在文本段之后,链接器将Ttot加到每一个数据段所分
配的地址上,接着,由于BSS跟在文本和数据段之后,所以链接器会将Ttot,Dtot的和加到每
一个BSS段分配的地址上.
同样,链接器通常会将分配的大小按照对齐要求扩充补齐.
段与页面的对齐
如果文本和数据被加载到独立的内存页中,这也是通常的情况,文本段的大小必须扩
充为一个整页,相应的数据和BSS段的位置也要进行调整.很多UNIX系统都使用一种技巧
来节省文件空间,即在目标文件中数据紧跟在文本的后面,并将那个(文本和数据共存的)
页在虚拟内存中映射两次,一次是只读的文本段,一次是写时复制(copy-on-write)的数
据段.这种情况下,数据段在逻辑上起始于文本段末尾紧接着的下一页,这样就不需扩充文
本段,数据段也可对齐于紧接着文本段后的4K(或者其它的页尺寸)页边界.
例2:
我们将例1扩展,使得每个例程都有文本,数据和BSS段.字对齐要求还是4个
字节,但页大小为0x1000字节.
名称文本段数据段BSS段
-------------------------------------------------------------
ain101732050
calif920217100
ass615300840
newyork139********0
(均为16进制数字)
链接器首先分配文本段,然后是数据段,接着是BSS.注意这里数据段起始于页边界0x
5000,但BSS紧跟在数据的后面,这是因为在运行时数据和BSS在逻辑上是一个段.
ain1000-20195000-531f695c-69ab
calif2018-29375320-544669ac-6aab
ass2938-2f4c5448-57476aac-72eb
newyork2f50-42df5748-695a72ec-86eb
在0x42e0到0x5000之间的页结尾处浪费了一些空间.虽然BSS段的结束位置在页面中
部的0x86eb处,但程序们普遍都会紧跟其后分配"
堆"
空间.
公共块和其它特殊段
上面这种简单的段分配策略在链接器处理的80%的存储分配中都工作的很好,但剩下的
那些情况就需要用特殊的技巧来处理了.这里我们来看看比较常见的几个.
公共块
公共块存储是一个可以追溯到50年代FortranI时的特性.在最初的Fortran系统中,
每一个子程序(主程序,函数或者子例程)都有各自局部声明和分配的标量和数组变量.同
时还有一个各例程都可以使用的存储标量和数组的公共区域.公共块存储被证明是非常有用
的,并且在后续Fortran中单一的公共块(就是我们现在知道的空白公共块,即它的名称是
空白的)已经普及为多个可命名的公共块,每一个子程序都可以声明它们所用的公共块.
在最初的40年中,Fortran不支持动态存储分配,公共块是Fortran程序用来绕开这个
限制的首要工具.标准Fortran允许在不同例程中声明不同大小的空白公共块,其中最大的
尺寸最终生效.Fortran系统们无一例外的都将它扩展为允许以不同的大小来声明所有类型
的公共块,同样还是最大的尺寸最终生效.
大型的Fortran系统经常会超过它们所运行系统的内存容量限制,在没有动态内存分配
时,程序员不得不频繁的重新创建软件包,压缩尺寸来解决软件包遇到的此类问题.在一个
软件包中除一个之外的其它子程序都将公共块声明为只有一个元素的数组.剩下的那个子程
序声明所有公共块的实际大小,并在程序启动时将这些尺寸都保存在其余软件包可以使用的
(在另一个公共块中的)变量中.这样就可以通过修改和重新编译定义这些公共块的一个例
程,来调整公共块的尺寸,然后再重新链接.
从60年代开始Fortran增加了BLOCKDATA数据类型来为任意公共块(空白公共块除外,
这是为数不多的限制)的部分或全部来指明局部初始数据值,这在某种程度上更复杂了.通
常用来初始化公共块的在BLOCKDATA中的公共块尺寸,也在链接时被用来当作该公共块的
实际大小.
在处理公共块时,链接器会将输入文件中声明的每个公共块当作一个段来处理,但并
不会将这些段串联起来,而是将相同名称的公共块重叠在一起.这里会将声明的最大的尺寸
作为段的大小,除非在某一个输入文件中存在该段的已初始化的版本.在某些系统上,已初
始化的公共块是一个单独的段类型,而在另一些系统上它可能只是数据段的一部分.
UNIX链接器总是一贯支持公共块,甚至从最早版本的UNIX都具有一个Fortran子集的
编译器,并且UNIX版本的C语言传统上会将未初始化的全局变量作为公共块对待.但在ELF
之前的UNIX目标文件只有文本,数据和BSS段,没有办法直接声明一个公共块.作为一个
特殊技巧,链接器将未定义但具有非零初值的符号当作是公共块,而该值就是公共块的尺寸.
链接器将遇到的此类符号中最大的数值作为该公共块的尺寸.对于每一个公共块,它在输出
文件的BSS段中定义了相应的符号,在每一个符号的后面分配所需要的空间.
图4-3:
Unix公共块
在BSS末尾的公共块
C++重复代码消除
在某些编译系统中,C++编译器会由于虚函数表,模板和外部inline函数而产生大量的
重复代码.这些特性的设计是隐含的期望那种程序所有部分都可以被运行的环境.一个虚函
数表(通常简称为vtbl)包含一个类的所有虚函数(可以被子类覆盖的例程)的地址.每
个带有任何虚函数的类都需要一个vtbl.模板本质上就是以数据类型为参数的宏,并能够
根据特定的类型参数集可以扩展为特定的例程.确保是否存在一个对普通例程的引用可供调
用是程序员的责任,就是说对如hash(int)和hash(char*)每一类hash函数都有确定的定义,
hash(T)模板可以根据程序中使用hash函数时不同的参数数据类型创建对应的hash函数.
在每个源代码文件都被单独编译的环境中,最简单的方法就是将所有的vtbl都放入到
每一个目标文件中,扩展所有该文件用到的模板例程和外部inline函数,这样做的结果就
是产生大量的冗余代码.
最简单的方法就是在链接时仍然将那些重复代码保留着.那么得到的程序肯定可以正
确的工作,但代码会膨胀的比理想尺寸大三倍或者更多.
在那些使用简单链接器的系统上,某些C++系统使用了一种迭代链接的方法,并采用独
立的数据库来管理将哪些函数扩展到哪些地方,或者添加progma(向编译器提供信息的程序
源代码)向编译器反馈足够的信息以仅仅产生必须的代码.我们将在第11章涉及这些.
最近的很多C++系统已经正面解决了这个问题,要么是让链接器更聪明一些,要么就是
将链接器整合到程序开发环境的其它部分中(后一种方法我们在第11章还会涉及到).链
接器的方法是让编译器在每个目标文件中生成所有可能的重复代码,然后让链接器来识别和
消除重复的代码.
MSWindows链接器为代码区段定义了COMDAT标志来告诉链接器忽略除明确命名区段外
的所有重复区段.编译器会根据模板给每个区段命名,名字中包含了参数类型,如图4所示.
图4-4:
Windows
IMAGE_COMDAT_SELECT_NODUPLICATES1Warnifmultipleidenticallynamed
sectionsoccur.
IMAGE_COMDAT_SELECT_ANY2Linkoneidenticallynamedsection,
discardtherest.
IMAGE_COMDAT_SELECT_SAME_SIZE3Linkoneidenticallynamedsection,
discardtherest.Warnifadiscarded
sectionisn'
tthesamesize.
IMAGE_COMDAT_SELECT_EXACT_MATCH4Linkoneidenticallynamedsection,
tidenticalinsizeand
contents.(Notimplemented.)
IMAGE_COMDAT_SELECT_ASSOCIATIVE5Linkthissectionifanotherspecified
sectionisalsolinked.
GNU链接器是通过定义一个"
linkonce"
类型的区段(与公共块很相似)来解决这个
模板的问题的.如果链接器看到诸如.gnuonce.name之类的区段名称,它会将第一个明
确命名的此类区段保留下来并忽略其它冗余区段.同样编译器会将模板扩展到一个采用简化
模板名称的.gnuonce区段中.
这种策略工作的相当不错,但它并不是万能的.例如,它不能保护功能上并不完全相
同的vtbl和扩展模板.一些链接器尝试去检查被忽略的和保留的区段是否是每个字节都相
同.这种方法是很保守的,但是如果两个文件采用了不同的优化选项,或编译器的版本不同,
就会产生报错信息.另外,它也不能尽可能多的忽略冗余代码.在多数C++系统中,所有的
指针都具有相同的内部表示,这意味着一个模板的具有指向int类型指针参数的实例和指向
float类型指针参数的实例会产生相同的代码(即使它们的C++数据类型不同).某些链接
器也尝试忽略那些和其它区段每个字节都相同的link-once区段,哪怕它们的名字并不是完
全的相同,但这个问题仍然没有得到满意的解决.
虽然我们在这里只是讨论了模板的问题,但相同的问题也会发生在外部inline函数,
缺省构造,复制和赋值例程中,也可以采用相同的方法处理.
初始化和终结
另一个问题并不仅限于C++,但在C++上尤为严重,就是初始化和终结代码(initializ
ersandfinalizers).一般来说,如果它们可以在程序启动的时候可以运行一个初始化例
程,并在程序结束的时候运行一个终结例程,那把它们写成库会更容易些.C++允许静态变
量.如果一个变量的类具有构造函数,那这个构造函数在程序启动时会被调用来对初始化变
量,同样如果一个变量的类具有析构函数,那析构函数也会在程序退出时被调用.有很多办
法可以在不需要链接器支持的情况下做到这一点,我们将会在第11章讨论到,但现代链接
器通常都会直接支持该特性.
通常的方法是将每个目标文件中的初始化代码都放入一个匿名的例程中,然后将指向
该例程的指针放置在名为.init(或其它相近名字)的段中.链接器将所有的.init段串联在
一起,因此就创建了一个指向所有这些初始化例程的指针列表.程序的初始化部分只需要遍
历该列表依次调用所有例程即可.退出时的代码可以采用相同方法,只是段的名字改为了.f
ini.
实践证明这种方法也不是完全令人满意的,因为有一些初始化代码要求比另外一些更
早的运行.C++定义指出应用程序级的构造函数运行顺序是不确定的,但I/O和其它系统库
的构造函数需要在应用程序自己的构造函数之前执行.完美的方法应当是让每一个初始化例
程都精确的列出它们的依赖关系,并在此基础上进行拓扑排序.BeOS操作系统的动态链接
器就是这么做的,使用到了库的引用依赖关系(如果库A依赖于库B,那么库B的初始化代
码就可能需要先运行).
一个更简单的近似方法是设置多个用于初始化的段,如.init和.ctor,这样启动程序
首先为所有库级初始化调用.init中的例程,然后为C++的构造函数调用.ctor中的例程.同
样的问题出现在程序结束时,对应的段为.dtor和.fini.有一个系统甚至还允许程序员设置
优先级编号,0至127为用户代码,128至255是系统库,链接器在合并代码之前会先将初始
化和终结代码按优先级编号排序,最高优先级的初始化代码最先运行.但这仍不能令人完全
满意,因为构造函数之间会存在顺序依赖关系,从而产生非常难以调试的错误,但在这里C
++将避免这些错误的责任交给了程序员.
该策略的一个变种是将实际的初始化代码放在.init段中,当链接器合并它们的时候该
段会成为完成所有初始化工作的inline代码.只有少量系统进行了这种尝试,但在不支持
直接寻址的计算机上是很难让它工作的,因为从每个目标文件中提取出来的代码块还要能够
对它们原本文件中的数据进行寻址,通常这都需要寄存器来指向可以指向寻址数据的表.匿
名例程采用和其它例程相同的方式来初始化它们的寻址过程,借助已有的方案来减少寻址的
问题.
IBM伪寄存器
IBM主机系统的链接器提供了一种称为"
外部模拟(externaldummy)"
区段或"
伪寄
存器(pseudo-registers)"
的有趣特性.360是较早的无直接寻址的主机架构之一,这就
意味着实现小数据区域共享要付出昂贵的开销.每一个引用全局对象的例程都需要一个4字
节的指针指向该对象,如果这个对象只有开头4个字节那么大的话,这将是相当大的开销.
例如PL/1程序对每一个打开的文件和其它全局对象都需要一个指针(虽然PL/1应用程序的
程序员无法访问伪寄存器,但它是唯一使用伪寄存器的高级语言.它使用伪寄存器指向打开
文件的控制块这样应用程序就可以包括进那些对I/O系统的inline调用).
一个相关的问题是OS/360不支持我们现在所说的那种称为进程/任务级本地存储的东西,
并且对共享库只提供非常有限的支持.如果两个作业运行同样的程序,或者这个程序被标注
为可重入(这时它们共享整个程序,代码和数据),或者标注为不可重入(这时不共享任何
东西).所有的程序都被加载到相同的地址空间,因此相同程序的多个实例必须标注出实例
本身数据的范围(360系统不具备硬件内存重定位功能,尽管370支持了,但也知道OS/VS
操作系统的若干个版本之后系统才提供进程独立的地址空间).
伪寄存器可以帮助解决这些问题,如图5所示.每一个输入文件都可以声明(多个)伪
寄存器,也称为外部模拟区段(360系统的汇编语言中,它与结构体的声明很相似).每个
伪寄存器都有名字,长度和对齐要求.在链接时,链接器将所有的伪寄存器都收集到一个逻
辑段中,将最大的尺寸和最严格的对齐要求施加于每个伪寄存器,并为它们分配在该逻辑段
中不会相互重叠的偏移量.
但链接器不会为伪寄存器段分配空间.它只是计算该段的大小,并将其存储在程序的
数据段中以特殊的CXD(cumulativeexternaldummy,即重定位项)标识的位置.当引用一
个伪寄存器时,程序代码还需要另一个特殊的XD(externaldummy),它是用来指示将偏移
量放置在哪一个该伪寄存器所属逻辑段内的重定位类型.
程序的初始化代码为伪寄存器动态的分配空间,使用CXD可以知道需要多大的空间,并
按惯例将这个空间的地址存放在寄存器12中,在整个程序运行期间都不会改变.程序中的
任何一部分都可以通过将寄存器12的值与某个伪寄存器对应的XD的值相加得到该伪寄存器
的地址.一般都是通过load和store指令来完成的,将R12(寄存器12)作为索引寄存器与
嵌入到指令的地址替换域中的XD项相加(地址替换域只有12位,但由于XD将16位半字的
高4位保持为0,即基址寄存器为0,所以仍然可以产生正确的结果).
图4-5:
精灵寄存器
通过R12指向一串地址块.各种例程通过偏移量引用它们.
这样的结果就是程序的所有部分都可以load,store和其它RX格式指令来直接访问所
有的伪寄存器.如果一个程序存在多个活动的实例,每个实例就可以通过采用不同的R12值
来分配独立的空间.
尽管最初引用伪寄存器的原因现在大多数都已经被废弃了,但为链接器提供可以高效
访问线程本地地址的方法确实一个非常好的思想,并且仍然出现在很多现代操作系统中,其
中最著名的就是Windows.同样,现代的RISC机器也分享了360系统有限的寻址范围,因此
需要使用内存指针表来寻址任意的内存地址.在很多RISCUNIX系统上,编译器为每个模块
创建两个数据段,一个是通常的数据段,另一个是"
小(small)"
数据段,即大小低于某
一个尺寸阀值的静态对象.链接器将所有的小数据段收集在一起,然后让程序的启动代码将
合并的小数据段的地址放入一个保留的寄存器中.这样就可以通过和这个寄存器相关的基址
寻址来直接引用这些小数据.要注意,与伪寄存器不同,小数据的存储空间既会被链接器分
配,也会被链接器放置到输出中,在每个程序中只有一份小数据.某些UNIX系统支持线程,
但线程级的存储是特定的程序代码完成的,不需要链接器的特殊帮助.
特殊的表
链接器分配存储的最后一个资源是链接器本身.尤其是当应用程序使用共享库或者重
叠技术时,链接器会创建由指针,符号或其它别的数据构成的多个段来在运行时支持库或者
重叠.一旦这些库被建立了,链接器会按照对待任何其它段的方式来为它们分配存储空间.
X86分段的存储分配
8086和80286的分段内存寻址的怪癖要求导致了少量特殊的东西.x86OMF目标文件给
每个段都有一个名字和可选的类别.所有具有相同名字的段,会根据由编译器或者汇编器设
置的一些标志位来合并到一个大的段中,并且所有类别相同的段都会被连续的分配在一个块
中.编译器或汇编器使用类别名来标注段的类型(诸如代码或静态数据),因此链接器可
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 存储空间 分配 11 文档 资料
![提示](https://static.bdocx.com/images/bang_tan.gif)