在阅读完《深入理解计算机系统》第一章(计算机系统漫游)、第七章(链接)以及第十章(虚拟存储器)和《程序员的自我修养——链接、装载与库》后,历时悠久的梦想终于要实现了。开篇之初,首先提出一个迷惑了很久的一个问题:什么是虚拟存储器?它跟进程的虚拟地址空间有什么关系?
虚拟存储器是建立在主存--辅存物理结构基础上,有附加的硬件装置及操作系统存储管理软件组成的一种存储体系。
顾名思义,虚拟存储器是虚拟的存储器,它其实是不存在的,而仅仅是由一些硬件和软件管理的一种“系统”。他提供了三个重要的能力:1,它将主存看成一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据(这里存在“交换空间”以及“页面调度”等概念),通过这种方式,高效地利用主存;2,它为每个进程提供了统一的地址空间(以虚拟地址编址),从而简化了存储器管理;3,操作系统会为每个进程提供独立的地址空间,从而保护了每个进程的地址空间不被其他进程破坏。
虚拟存储器与虚拟地址空间是两个不同的概念:虚拟存储器是假想的存储器,而虚拟存储空间是假想的内存。它们之间的关系应该与主存储器与内存空间之间的关系类似。
编译部分:
很多时候,从示例入手比较简单。我们写两个小程序a.c和b.c
[cpp] view plain copy
- /* a.c */
- extern
int shared; - int main()
- {
- int a = 100;
- swap(&a,&shared);
- }
[cpp] view plain copy
- /* b.c */
- int shared = 1;
- void swap(int *a,int *b)
- {
- *a ^= *b ^= *a ^= *b;
- }
编译这两个文件得到“a.o”和“b.o”两个目标文件
§gcc -c a.c b.c
可重定位目标文件:
EF头L以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统字节顺序.ELF头剩下的部分包含帮助链接器解析和解释目标文件的信息.其中包括ELF头的大小,目标文件的类型(比如,可重定位,可执行,共享目标文件),机器类型,节头部表的文件偏移,以及节头部表中的表目大小和数量.不同节的位置和大小是节头部表描述的,其中目标文件中的每个节都有一个固定大小的表目.ELF格式的可重定位目标文件结构如下图:
.text:已编译程序的机器代码
.rodata:只读数据
.data:已初始化的全局C变量
.bss:未初始化的全局C变量.在目标文件中这个节不占实际空间,仅是一个占位符.
.sysmtab:一个符号表,存放在程序中被定义和引用的函数和全局变量的信息.
.rel.text:当链接器把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改.一般而言,任何调用外部函数或者引用全局变量的指令都要修改.另一个方面,调用本地函数的指令则不需要修改.
.rel.data:被模块定义或引用的任何全局变量的信息.
.debug:一个调试符号表
.line:原始C源程序中的行号和.text节中机器指令之间的映射.
.strtab:一个字符串表,其中内容包括.symtab和.debug节中的符号表,以及节头部中的节名字.
链接部分:
链接就是将不同部分的代码和数据收集和组合成一个单一文件的过程,也就是把不同目标文件合并成最终可执行文件的过程。当然,务必知道:这个过程不涉及内存。链接可以分为三种情形:1,编译时链接,也就是我们常说的静态链接;2,装载时链接;3,运行时链接。装载时链接和运行时链接合称为动态链接。在此,我们的链接部分将主要讲述静态链接,而装载时链接我们放在装载部分讲,运行时链接忽略。
从代码中可以看到三个符号:share,swap和main。
静态链接的整个过程分为两步:
第一步:空间和地址分配。扫描所有的输入目标文件,获得他们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这样,连接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置(ar不带任何选项打包成.a文件),并建立映射关系(ranlib更新静态库的符号索引表,如果.o文件有更新且ar之后需要重新ranlib,否则链接时会找不到符号而报链接错误)。
这里可能会有一个问题:建立了什么样的映射关系。看了下面图,你可能就会有所了解。映射关系就是指可执行文件与进程虚拟地址空间之间的映射。那么,这里程序还没有执行,更不会出现进程,哪里来的进程地址空间呢?此时虚拟存储器便发挥了很大的作用:虽然此时没有进程,但是每个进程的虚拟地址空间的格式都是一致的。所以,为可执行文件的每个段甚至每个符号符号分配地址也就不会有什么错了。注意:在链接之前,目标文件中的所有段的虚拟地址都是0,因为虚拟空间还没有被分配,默认都为0.等到链接之后,可执行文件中的各个段已经都被分配到了相应的虚拟地址。仍然看下图。。。
综上所述。链接后可执行文件中的各个段的虚拟地址都已经确定了。那么,各个符号的地址呢?因为各个符号在段中相对位置是固定的,所以这个时候“main”,“share”及“swap”的地址也都确定了。
其中,“main”位于“text”段的最开始处,偏移量为0,所以“main”这个符号在最终的输出文件中的地址应该是0x08048094 + 0 即0x08048094;同理,“swap”的偏移量为0x34,“swap”这个符号在最终的输出文件中的地址应该是0x08048094 + 0x34 即0x080480c8;“shared”相对于“data”的偏移量为0,在最终的输出文件中的地址应该是0x08049108。
这里可能会有点小小的疑问:shared怎么在.data段中呢?刚开始不是未初始化吗?是的,但是我们这里现在是已经合并好的段,它已经是合并后的文件格式状况,shared已经知道它的值为1了,仔细看上图。
如下表所示
符号
| 类型
| 虚拟地址
| main
| 函数
| 0x08048094
| swap
| 函数
| 0x080480c8
| shared
| 变量
| 0x08049108
|
第二步:符号解析与重定位
首先,符号解析。解析符号就是将每个符号引用与它输入的可重定位目标文件中的符号表中的一个确定的符号定义联系起来。若找不到,则出现编译时错误。
解释一下什么是符号定义和什么是符号引用吧:须知,这样的区分是源于某一特定的模块而言。如上所示,对a.o而言,它里面的shared即为符号定义而b.o里面的shared为符号引用;相对地,
对b.o而言,它里面的shared即为符号定义而a.o里面的shared为符号引用;
其次,重定位。 |