《程序员的自我修养——链接、装载与库》读书笔记(2)

        接着上次的笔记说说《程序员的自我修养——链接、装载与库》的四到六章,谈谈程序链接和装载的过程。

        链接的过程和上次文末的猜测几乎一致,可执行文件和编译后的目标文件都采用ELF文件格式,所以链接的过程无非是将多个目标文件组合成一个大的ELF文件。核心问题有两个:不同目标文件中相同类型的段如何组合?彼此引用的变量与函数如何确定地址?由于采用ELF这一相当规范且高效的文件格式,上面两个问题的解决方法都十分简单。

        可以很自然得想到,段的合并方式有两种:每个目标文件的段放在一起,或者所有文件中相同类型的段放在一起。前一种方法看似并无不妥,但由于在实际内存管理机制中操作系统采取分页的方式,虚拟地址空间中的页面与物理页框一一对应,使得段起始地址存在对齐的问题(页面大小的整数倍),很小的段也会占用一个页面,这样过多的段落会使内存使用非常低效。所以后一种方式被普遍采用,即链接过程的第一步便是合并相同类型的段,进行空间和地址分配,计算出每个段的位置和大小。

        关于重定位的问题,由于ELF文件中已经记录了需要重定位的变量与函数,所以直接从重定位表中可以找到重定位入口。由于上一步空间和地址分配过程中,每个目标文件的符号表已经被合并成了一个大的全局符号表,每个变量的虚拟地址已经被确定下来,那么便可以从重定位表中找到需要修正的变量和函数的在代码段中的位置,以及它们在全局符号表中的下标,根据全局符号表中它们的虚拟地址来修正代码段中的指令代码(分为绝对寻址修正和相对寻址修正),这样符号解析与重定位过程就完成了。

        那么可执行文件与目标文件只有这些不同么?事实上可执行文件比目标文件还多出了一个很重要的段:程序头表,这之中包含了程序装载时的重要信息。

        所谓装载,有各种不同的定义,我觉得最好的说法是可执行文件从开始执行到真正运行用户代码的准备过程。曾经装载指的就是把可执行文件装入内存,即采用覆盖装入的方式,这种方法需要反复地访问硬盘,速度较慢。而现代操作系统采用分页的内存管理,利用了局部性原理,只有当发生页错误的时候才会到硬盘中读取文件。在这种情况下,装载的过程就不是简简单单将ELF文件读入内存了,而是将文件内容与虚拟内存建立映射。这样的映射与虚拟内存、物理内存之间的映射不同,这种映射是为了判断当页错误发生时到哪里去找这页该有的内容。建立可执行文件与虚拟地址空间的映射是装载的核心内容。

        而可执行文件在虚拟地址空间中如何映射,或者说是以什么形式分布呢?这就是程序头表的作用。上面说过,把不同文件中相同类型的段合并在一起以节省内存,然而段的数目还是很多,由于段地址对齐还是会造成内存使用效率低下,所以直接以段的分布布置虚拟地址空间并不理想。其实,站在操作系统的角度看,一个段的内容并不重要,重要的是它的权限,如text段可读可执行,数据段、bss段可读可写,只读数据只能读,将相同权限的段合并到一起映射到一片虚拟内存区域(一个VMA),这样可以大大减少内存碎片。这样相同权限的段的集合可以称为一个Segment,中文也是段,但是不同于前文所说的段(Section),存放Segment信息的Section就是可执行文件中的程序表头。

        即使有了上述映射机制,由于段地址对齐的原因(这里可以认为是Segment),还是会存在内存使用效率低的情况。这里采取了一些技巧,可以将不同Segment接壤处的物理页框在虚拟地址空间内映射两遍,分别给相连的两个Segment使用,这样可以大大节省物理内存空间。而需要注意的是,此时Segment 的起始地址不再是页面大小的整数倍,而需要重新计算。

        总结一下装载的过程:1、创建虚拟地址空间;  2、读取可执行文件头,建立可执行文件与虚拟内存映射关系; 3、将CPU指令寄存器设置为可执行文件入口地址。

        以上就是链接与装载的过程,可以感受到为了形成这么一套高效的标准化过程人们作出了多少思考与尝试,相信学习了这样一套机制及其背后的思考过程能够大大加深对计算机体系结构的理解。

你可能感兴趣的:(《程序员的自我修养——链接、装载与库》读书笔记(2))