CSAPP第7章 链接
链接的通俗概念
链接就是将各种代码和数据片段收集并组合为一个单一文件的过程。
因为链接器的存在,它使得分离式编译成为可能。
静态链接
静态链接的输入通常是一组可重定位的目标文件,输出是一个完全链接、可以加载和运行的可执行目标文件。
链接的两个主要任务:
- 符号解析
- 重定位
符号解析的过程是将符号引用和定义关联的过程。这里符号解析单纯连接符号定义和符号引用,而不是编译原理中的解析语法符号。
符号可以指:函数、全局变量、静态变量
重定位是将符号定义与内存地址关联的过程,并且每个符号引用也要替换为相应的地址(重新定位符号的地址)。
可重定位目标文件
首先了解一下典型的ELF可执行可链接文件的格式,因为链接器的输入就是它。
ELF 全称 “Executable and Linkable Format”,即可执行可链接文件格式,目前常见的Linux、 Android可执行文件、共享库(.so)、目标文件( .o)以及Core 文件(吐核)均为此格式。
典型的ELF文件由一个ELF header、若干个节以及Section Header Table(节头部表)组成。
ELF header主要是一些文件的元信息,而Section Header Table节头部表描述了不同节的大小和位置。
ELF其中重要的是若干个节,我们现在需要了解的是symtab符号表,它存放了程序中定义和引用的函数和全局变量的信息。
符号和符号表
每个ELF文件(模块m)都有一个符号表,它包含了当前文件定义和应用的符号信息,这些符号可以分为:
全局符号:在m中定义,能被其他模块引用的符号。
比如非静态的C函数和全局变量。
外部符号:由其他模块定义,但是被m使用的符号。
比如其他模块定义的非静态的C函数和全局变量。
局部符号:只被m定义和使用的符号。
比如带static的C函数的全局变量。
需要注意到函数内部的局部变量不归符号表管辖,因为它们由运行时的栈管理。
而静态的局部变量还是会被符号表记录。
符号解析
解析的过程就是将符号引用和符号表中的定义相关联(具体怎么关联母鸡)。
局部符号的解析简单明了,全局符号和外部符号的解析比较麻烦。
在编译器生成目标文件的过程中,如果它遇到一个不再当前模块定义的符号(变量/函数名),那么它就会生成一个链接器符号条目(编译器会假设这个符号由其他模块定义,并生成了一个填空让链接器来填)。
链接器解析多重定义符号
链接器的输入是一组可重定位目标模块。每个模块定义一组符号,有些是局部的(只对定义该符号的模块可见),有些是全局的(对其他模块也可见)。
如果多个模块定义同名的全局符号,会发生什么呢?下面是 Linux编译系统采用的方法。
在编译时,编译器向汇编器输出每个全局符号,或者是强( strong)或者是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。 根据强弱符号的定义, Linux链接器使用下面的规则来处理多重定义的符号名
规则1:不允许有多个同名的强符号。
规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
一强一弱的符号会产生意想不到的错误,建议编译时带上GCC-fno-common表示来使得任何遇见多重定义的符号时,编译器能发出警告信息。
静态库
静态库概念是指将所有的目标文件打包成一个共享文件,它可以作为链接器的输入。
它的概念起源于一些标准函数的调用,用户希望能直接使用。一种方式是让编译器认出标准函数,但这么做的代价就是库函数的开发和编译器的开发耦合。另一种方式,是将所有的标准C函数都放在一个可重定位的目标模块中。但这么做有两个缺点:1、牵一发而动全身(对单个函数的改动需要重新编译整个文件)2、冗余复制(每个可执行文件将不需要的目标函数也一起链接了)。
所以,静态库的概念就是将每个标准函数单独编译为目标模块,然后再将这些目标模块再次打包成一个静态库文件。链接器链接时,只复制被程序引用的目标模块。
链接器使用静态库解析引用
在符号解析阶段,链接器会从左往右按照它们在编译器驱动程序命令行上的出现顺序来扫描文件。
在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接就会失败。
链接准则:符号定义的库要放在符号引用的文件之后。
所以将静态库放在命令行最尾部。还有一方面,如果库函数互相引用,可以在命令行上重复库来满足依赖关系。
重定位
在重定位中,将合并输入模块,并为每个符号分配运行时地址。具体来说,有两个步骤:
重定位节和符号定义
链接器将所有相同类型的节合并为同一类型的节。然后将运行时地址赋给每个聚合节以及符号定义。
重定位节中的符号引用
在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址(这主要依靠重定位条目)。
重定位条目
在符号解析的过程中,在编译器生成目标文件的过程中,如果它遇到一个不再当前模块定义的符号(变量/函数名),那么它就会生成一个链接器符号条目,这里其实就是重定位条目。
重定位这块跳过
加载可执行目标文件
加载的过程其实就是搭建进程的内存映像。
在程序头部表的引导下,加载器将可执行文件的片( chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是 _start 函数的地址。这个函数是在系统目标文件ctrl.o中定义的,对所有的C程序都是一样的。 _start 函数调用系统启动函数 __libc_start_main,该函数定义在libc.so中。它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。
动态链接
静态库的缺点
- 更新麻烦,需要显式的下载最新库,然后再与更新的库链接
- 内存浪费(几乎每个C程序都用标准库函数,这些代码会被复制到每个运行进程的文本段中,这会造成内存资源的极大浪费)
动态链接是怎么解决静态库缺点的
动态库的思想就是共享,而不是复制和嵌入。明白这一点需要理解虚拟内存以及内存映射。
在链接时,链接器只复制一些重定位和符号表信息,在运行时解析这些对于代码和数据的引用。
具体的工作由动态链接器执行,可执行文件包含一个.interp节,这个节包含了动态链接器的路径名。当加载程序时,这个动态链接器就会执行,它来重定位动态库的文本和数据。