Linux系统链接原理(二)

Computer Systems: Linking

《深入理解计算机系统》中Linking这一章对编译过程中链接器的作用做了非常详尽的介绍,对于程序员而言可能在算法、数据结构以及编程语言上花的时间比较多,然而对于计算机如何将代码编译为可执行文件的过程缺乏系统的了解,然而如果需要了解Linux下程序完整的编译执行过程,阅读本书的相关章节是十分必要的,笔记比较长所以分成两部分

可执行目标文件

可执行目标文件与前面提到的可重定向目标文件最大的区别在于,可执行目标文件必须将文件中的节映射到存储器中的段中,同时它还定义了一个.init函数用来初始化代码,由于可执行目标文件可以直接运行,因此该文件中不包含重定向信息

可执行目标文件的加载

将可执行目标文件中的代码和数据复制到存储器的过程称为加载,存储器映像为程序分配了的数据空间(包括堆栈)以及用来存储数据和代码的段,基本结构如下:

最上方是用户栈,主要用来存储局部变量等
下面的存储器区域用来存储共享库
再下面是堆空间(malloc时就会分配这部分空间)
最后是可执行文件中的数据和代码段

比较令我印象深刻的是这一章节介绍了C程序的启动细节,当加载器完成代码和数据段的加载后:

加载器跳转到程序入口点,即符号_start的地址,在该地址启动代码
调用.text和.init的初始化例程,启动代码调用atexit例程,该程序包含原程序正常终止时应该调用的程序
之后运行原程序中的main函数执行我们的C代码
最后exit函数调用_exit返回

从启动例程的伪代码中就可以看出main函数在C程序中必须出现,这也就是为什么我们把main函数称为程序入口的原因

对于习题7.5中第二个问题,无论直接调用exit还是使用return,抑或是什么都不写,最后都会调用_exit程序把控制返回给操作系统

动态链接共享库

静态库虽然能很好地管理大量公共函数的链接问题,但是如果有多个程序同时引用了一个函数,函数会被复制多份,显然这会占用多余的内存空间,因此共享库被设计来解决函数代码重复复制的问题

按照书上的说法,在运行时共享库可以加载到任意的存储器地址并和存储器中的程序链接起来,这个过程也被称为动态链接,有专门的动态链接器完成此任务,共享库独立于应用程序,因此在链接时链接器不会拷贝动态链接库中的代码和数据

在加载时,如果需要链接动态链接库,那么加载器会把控制权转交给动态链接器,动态链接器会执行如下重定位流程:

1.重定位每个动态链接库的文本和数据到存储器段
2.重定位可执行目标文件中的符号和引用

那么如何做到让多个进程共享一块存储器中的库代码呢?

书中介绍了编译库代码,使得链接器不需要修改代码就可以在任何地址加载和执行,相当于预先把库代码编译完成后,这里我们可以看出动态链接库与静态链接库的一个重要的不同点:动态链接库不需要链接器进行重定位,但是按照之前几节的说法,如果我们需要调用外部定义的数据或者函数,那么如何找到对应的地址呢?

书中提到了关键的一个事实:无论我们在存储器的何处加载目标模块,数据段总是被分配成紧随在代码段的后面这意味着如果代码段和数据段的长度固定,那么我们始终可以通过相对寻址找到我们需要的数据或指令。

全局偏移量表GOT

由于动态链接库中的代码不会编译到可执行目标文件中,因此我们需要给可执行目标文件一定的信息让加载起能够找到动态链接库的代码,因此引入GOT和PLT两张表,分别用来记录在这个目标模块中引用的全局变量和外部定义函数。

  • 通过GOT引用全局变量: 当程序需要引用某个全局变量时:

    首先将PC的值移到寄存器中
    接着通过变量在GOT中的偏移找到存有该变量地址的条目
    最后通过条目中提供的全局地址找到该变量

从C/C++程序员的角度而言,这相当于进行两次寻址,第一次找到全局变量的地址,第二次找到该变量,GOT就是一张提供了各变量地址的表 类似地,当程序需要调用某个外部函数时,同样执行上述的步骤

值得注意的是为了防止每次调用动态链接库的数据或函数时都要重复上述的步骤,编译器使用了一种称为“延迟绑定”的技术

过程链接表PLT

介绍延迟绑定之前,首先需要介绍过程链接表的概念,书中没有详细说明PLT存储的内容,因此我查阅了一些资料:

过程链接表用于把位置独立的函数调用重定向到绝对位置。PLT中的每个条目为本程序要调用的函数提供一个入口,PLT 的第1个入口PLT[0] 是一段访问动态链接器的特殊代码。程序对PLT入口的第1次访问都转到了PLT[0]

当第一次过程调用发生时,最后总会跳转到PLT[0]中,PLT[0]包含跳转到动态链接器的信息,待完成符号解析后,将符号的实际地址存入相应的GOT项,这样以后调用函数时可直接跳到实际的函数地址,不必再执行符号解析函数。延迟绑定指的就是在第一次引用动态链接库的指令时,通过PLT和GOT两张表寻找运行时的指令地址,之后修改GOT项的内容使得后面的调用不再需要重复相同的步骤。

Linux用一个全局的库映射信息结构struct link_map链表来管理和控制所有动态库的加载,动态库的加载过程实际上是映射库文件到内存中,并填充库映射信息结构添加到链表中的过程。觉得这部分书中讲得也不够清楚,还是有点似懂非懂。

但是读完动态链接的部分之后,觉得动态链接与链接的定义其实有点出入,按照本章开头的定义,链接强调组合成为单一文件,然而动态链接的本质却是通过间接的方式寻找调用的数据和代码,链接器在链接动态库时紧紧拷贝重定位和符号表信息,并不参与真正的链接过程,因此我认为链接与动态链接是两个并列的概念

总结

总的来说,综合符号解析静态库链接动态链接,一个完整的链接过程如下图:whole_process

总体而言链接的重点在于重定位与两类链接库的链接原理,理清这些概念之后整个链接的过程就比较清晰了。