花了一周的时间,学习了链接、加载和库的相关内容。阅读了《链接器和加载器》、《程序员的自我修养-链接、加载和库》这两本书(这个话题相关的资料很少见,这两本一个国外一个国内的,算是比较经典),当然,一遍是肯定不够的,计划是完成五遍。

    这一些系列的文章,可算是学习笔记,但我想抛开书本,凭借自己的记忆和理解来写这部分内容。

    对于磁盘上所存储的数以千计的可执行文件,普通程序员往往是心存敬畏的。我们不知道这些文件的面貌,只知道他们如同有生命一样能完成各种各样的功能,而这些来自于我们从编辑器敲出的字符,至于这些字符最后生成什么,如何加载到内存并启动执行,并不清楚。提到可执行文件,脑子里出现一个词叫做“二进制”,也是因为这“二进制”,让我们有了一种从心底的恐惧。提到库(动态库、静态库)更是只知道我们的程序依赖它,因为没有它链接就不通过,至于怎么依赖、为什么依赖,仍然是迷迷糊糊。

    这篇文章,我想搞清楚的是链接、加载这两个过程都干了些什么;我们谈之色变的二进制可执行文件里面到底有些什么内容,有什么作用。

    为什么要学习这个?首先:好奇心,神秘的东西往往最具有吸引力;其次:能解决我们的问题,我们写的程序编译、链接为什么不通过,从这儿可以找到答案;再次:逆向、破解这些高深的技术要求对这些过程,还有ELF,PE文件格式等熟悉和理解

    一,为什么要链接?链接过程做了什么事儿?

    只要不是实例性质的小程序,我们的程序往往不可能写到一个源文件中,一个大型的程序往往分成无数的模块,更无数的文件,这些模块和文件单独编译生成目标文件,这是编译过程;这些文件文件并不能单独执行,因为他们之间必然存在变量、函数的引用和调用,单独编译的时候,这些引用的地址并无法确定

    所以,链接的一个重要过程就是为我们引用的变量和函数找到定义的地方,确定其地址(单独编译的时候,由于不能确定,这些地址都处于未定义状态,以一个特殊的地址作为替代),让实际的工作可以执行;此外,当生成最终的可执行文件的时候,我们需要按照文件加载到内存的虚拟地址来进行布局和地址的指定并进行重定位,32位linux默认加载位置是0x08048000

    其次,各模块单独编译,都会产生各种各样的称之为“段”的一些区域,链接过程需要将相同类型的段进行合并,方便与加载到内存执行;

    由于各模块单独编译的时候,模块内的指令、变量、函数定义的地址都是相对于本模块来确定,当将段合并之后,各指令、变量等地址需要重新指定,这个过程叫做重定位;这个过程是为了确定所有undefined的变量、函数,如果不能将所有undefined的符号都正确的解析,就会出现常见的链接错误

二、加载,为什么加载?加载的过程?

    上面链接的概况阐述了其目的,读入并合并各目标对象,地址重定位(伴随符号解析),最后输出的是可执行文件。也就是存储在我们磁盘上的“二进制老虎”。

    现在,我们该让他执行起来,大家可能觉得这很简单,我们直接在命令行启动执行就ok了。真的如此简单么?

    现在来看看一个可执行文件加载需要经历哪些过程。

    存储在磁盘的可执行文件,要由操作系统将其映射到进程的虚拟地址空间内;系统为其分配堆栈、堆的环境,填充环境变量,main函数参数,执行.init段的全局内容,然后跳转到程序main入口

    在加载的过程中,如果程序使用到了动态链接库,还要查找并映射相应的动态库,并进行动态库相关的地址重定位,这又涉及到地址无关代码等内容,涉及到got/plt,动态链接重定位等内容

    至此,程序才真正的能够运行起来,去做他想做的事情,可main退出后,还要帮他打扫战场,例如.finit段的执行

    可见,链接主要是对目标对象文件的一个调整操作,读入文件,合并各种段,如果有静态库,还要将其也算作在内,然后对各文件中的未解析符号进行地址重定位;加载的主要内容是为进程分配空间(链接过程也设计到分配空间,这主要是指的在文件中分配空间,分配各段所占用的空间),将可执行文件和动态库映射到进程的虚拟地址空间,然后为程序的执行准备执行环境,如堆栈初始化等,如果涉及到动态链接还要进行加载时重定位(为了避免简单的加载时重定位所带来的不能共享代码段的问题,还牵扯到地址无关代码,延迟加载技术等内容)

 

以上大致分析了链接和加载的内容,下面说说我们的可执行文件里面都有些什么东西

    可执行文件在大家心里或多或少还是有一个概念,至少我们知道我们的c/c++代码最终会转换成二进制指令,肯定是存在于这个文件咯,只是不知道怎么存储而已。

    没错,二进制里面的确存储着我们的指令、当然还有数据,我们的指令存储于.text段,具有可读可执行属性,可共享,不能修改;数据存储于.data 、 .bss段,具有可写属性,各进程不能共享,当然.bss段存储的是未初始化数据,在文件中并不占用存储空间,加载到内存后会占用存储空间

    除了这些显而易见的段之外,我们的可执行文件中还存储有很多辅助段,比如符号表、字符串表、区段头部表、重定位表等等,这些段主要是用于进行地址解析之用,也就是用于我们的链接过程(其实加载和链接都涉及到地址重定位);另外还可能有调试信息等相关的段

    每个undefined的符号,是一个重定位入口,都对应这一个符号表项,符号的字符串表示存储在字符串表中,未定义的符号会存储于称为导入符号的表中(定义这些符号的表会生成一份叫做导出符号表的东东),在链接过程这些导入符号需要被确定,并将相应的地址在符号表中做调整;重定位表中记录的是需要进行重定位的符号

 

   链接和加载的主要内容如上,有不全的地方,以后会逐一进行解释;比如ELF和PE文件格式,地址重定位方案,这些都是链接和加载部分的关键点