CSAPP笔记04链接(上)
CSAPP笔记04链接(上)
0x00 概述
链接(linking)是将各种代码和数据片段收集并组合称一个单一文件的过程,这个文件能被加载(复制)到内存并执行。
链接可以执行于编译时(compile time),也就是在源代码被翻译成机器码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。在早期的的计算机系统中,链接是手动执行的。而在现代系统中,链接是由被称为链接器(linker)的程序自动执行的。
使用链接器的最大好处是,它使得分离编译(separate compilation)成为可能。也就是说,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将其分解为更小、更好管理的模块,我们可以单独修改编译这些模块,然后链接,而不是重新编译整个程序的所有代码。
学习链接的好处:
①理解链接器将帮助我们构建大型程序,构造大型程序经常会遇见由于缺少模块、缺少库或者不兼容的库版本引起的链接器错误。理解链接器如何解析引用、什么是库、以及链接器是如何使用库来解析引用的,将有助于我们解决这些链接错误。
②理解链接器有助于我们避免一些危险的编程错误,linux链接器解析符号引用时的决定可能会影响程序的正确性。比如,错误地定义了多个全局变量的程序仍可能会通过链接,并且不会产生任何警告信息。由此得到的程序会产生令人迷惑的运行时行为,并且非常难调试,我们将在这一届了解到这是如何发生的,以及如何避免。
③理解链接将帮助我们理解语言的作用域规则,例如全局变量和局部变量之间的区别(在链接时)?定义一个具有static属性的变量或者函数时,实际意味着什么(链接时发生了什么?)
④理解链接有助于我们理解其他重要的系统概念,链接器产生的可执行目标文件在重要的系统功能中扮演者关键角色,比如加载和运行程序、虚拟内存、分页(分页机制是实现虚拟内存的关键)、内存映射。
⑤理解链接将使我们能够利用动态链接库(我确实没用过),随着共享库和动态链接在现代操作系统中越来越多被使用,链接成为一个较为复杂的过程,并给掌握链接的程序员带来强大的能力。比如,许多软件产品在运行时使用共享库来升级压缩包装的(shrink-wrapped)二进制程序。另外,大多数web服务器都依赖共享库的动态链接来提供动态内容。
本章提供了关于链接的全面讨论,从传统的静态链接,到加载时的共享库的动态链接,以及运行时的共享库的动态链接。并且指出了链接时影响程序性能和正确性的情况。本章的内容基于x86-64的、使用标准的elf-64目标文件格式的linux系统。
0x01 编译器驱动程序
对于下面c语言文件,要生成一个可执行的目标文件,需要调用编译驱动程序(compiler driver),也就是从c语言文件到生成最终可以执行文件时调用的一全套程序程:语言预处理器、编译器、汇编器和链接器。
//main.c
int sum(int *a,int n);
int array[2]={1,2};
int main(){
int val=sum(array,2);
return val;
}
//sum.c
int sum(int* a,int n){
int i,s=0;
for(i=0;i<n;i++){
s+=a[i];
}
return s;
}
使用如下命令调用gcc编译驱动程序,将上述c语言文件生成目标可执行文件。
gcc -Og main.c sum.c -o prog.out
上图概括了驱动程序将示例程序从c语言源码文件生成可执行目标文件时的行为。可以在gcc命令里加上-v来显示具体生成步骤。
由图上我们知道,从c语言到最终目标可执行文件中间由多个过程,前面的命令是调用多个命令的结果:
①预处理器,c语言文件预处理过程:
cpp [other arguments] main.c -o main.i
②编译器,将预处理后的文件翻译成汇编代码:
cc1 [other arguments] main.i -o main.s
③汇编器,将汇编代码汇编成一个可重定位目标文件(relocatable object file),main.o:
as [other arguments] main.s -o main.o
④链接器,经过相同的过程生成sum.o,然后与main.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件(executable object file)prog.out:
ld -o prog [other arguments] main.o sum.o -o prog.out
要运行可执行文件prog,使用./prog.out
这个时候,shell调用操作系统中的加载器(loader),将可执行文件prog.out中的代码和数据复制到内存,然后将控制转移到程序的开头。
0x02 静态链接
静态链接器(static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。
输入的可重定位目标文件由各种不同的代码和数据节(section)组成,每一节都是一个连续的字节序列。(例如指令在一节中,初始化了的全局变量在另一节,而未初始化的变量又在另一节中)。
为了构造可执行文件,链接器必须完成的两个主要任务(也是其主要功能)是:
①符号解析(symbol resolution)
目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或者一个静态变量(即c语言中所有以static声明的变量)。符号解析的目的是,将每个对符号的引用,都正好找到一个对应的定义。
②重定位(relocation)
编译器和汇编器生成的每个目标文件中的每个节,地址都是从0开始的。每个节都在符号表中对应一个符号,通过修改符号表内的偏移,来重定位节,然后修改所有对这些符号的引用。链接器按照汇编器产生的重定位条目(relocation entry)的详细指令/信息,进行上述的重定位。
0x02 目标文件
目标文件有三种形式:
①可重定位目标文件:包含一个二进制代码和数据,可以在编译时(广义的编译,实际指链接时)与其他可重定位目标文件合并起来,创建一个可执行目标文件。
②可执行目标文件:包含二进制代码和数据,可以被直接复制到内存并执行。
③共享目标文件:一种特殊的可重定位目标文件,可以在加载时或者运行时被动地加载进内存并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。
一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)是以文件形式存放在磁盘的目标模块。
目标文件按照特定的目标文件格式来组织,各个系统的目标文件格式都不相同。Windows上使用PE(portable executable)可移植可执行格式,而现代x86-64 linux系统使用ELF(executable and linkable format) 可执行可链接格式,各种格式的基本概念相似。
0x04 可重定位目标文件
一个典型的ELF 可重定位目标文件的格式如上图,各部分的说明如下:
ELF头:以一个16字节的序列开始,该序列描述了生成该文件的系统的字的大小和字节序列。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,如ELF头的大小,目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表在文件内的偏移,以及节头部表中条目的大小和数量。
节头部表:用来描述不同节的位置(文件内的偏移和内存中的位置,如果是可重定位目标文件,内存中的位置总是0,等待链接时重定位)和大小。目标文件中的每个节(如.bss .text)都在节头部表有一个固定大小的条目(entry)。
夹在ELF头和节头部表之间的都是节,一个经典的ELF可重定位目标文件包含以下几个节(可执行目标文件则又略有不同):
.text 已经编译好的机器代码。
.rodata read only data,只读数据,比如printf语句中的格式字符串和switch语句中的跳转表。
.data c语言中已经初始化的全局和静态变量,而局部变量在运行时被保存在栈里,既不出现在.data节也不出现在.bss节。
.bss 未初始化或者初始化为0的全局和静态变量。在目标文件中这个节不占据实际空间,仅仅是一个占位符。(因为.data和.bss里都是保存全局变量和静态变量的值,而.bss保存的值未初始化,或者为0,因此不用填)。在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。(而局部变量未初始化,到底怎么处理,不同编译器有不同实现)。
.symtab 一个符号表,存放在程序中定义和引用的函数和全局变量的信息。和前面的.data和.bss不同,前面两个存放的是全局变量的值,而这个存放的是名字和其他信息(比如所在节,值在所在节里的偏移,值所占的字节,但是不包括值本身)
.rel.text 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改的.text节里的位置。一般来说,任何调用外部函数或者引用全局变量的指令都需要修改。而调用本地函数的指令责不需要修改(因为是相对地址,而且偏移位置在编译和汇编时就知道)。可执行目标文件已经完成重定位了,所以没有这一节。(全局变量在.text只可能被引用,不可能被定义,定义的在.data或.bss)
.rel.data 被模块引用或者定义的所有全局变量(包括外部和内部)的值重定位信息。一般而言,全局变量引用是在代码里引用,所以对其引用的重定位在.rel.data里,而全局变量的值是不变的,但是如果全局变量被初始化为另一个全局变量的地址,或者外部定义函数的地址,那么其值(也就是对应的地址),需要被修改。
.debug 调试符号表,不多介绍
.line 调试用的c源代码和机器码的行映射关系,不多介绍
.strtab 一个字符串表,内容包括 .symtab和.debug中的符号,以及节头部的节名字。字符串表的每一项都是以’\0’结束。