链接
- 链接将代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载(复制)到内存并执行
- 链接可以执行与编译时、加载时、运行时
- 早期的计算机系统通常需要手动链接,现在计算机系统通产由链接器自动执行
- 链接器将大型程序分解为小的模块,可以分离编译
- 有助于构造大型程序
- 错误的定义多个全局变量的程序通过链接器不会产生警告
- 理解作用域规则
- 能够理解其他重要的系统概念
- 利用共享库
编译器驱动程序
- 要用GNU编译系统构造示例程序,在shell中输入一下命令,调用GCC驱动程序:
gcc -Og -o prog main.c sum.c
- 驱动程序生成main.o和sum.o,最后运行链接器程序ld,将文件和必要的系统文件组合起来,创建一个可执行的目标文件prog:
ld -o prog [system object files and args] /tmp/main.o/tmp/sum.o
- 执行可执行文件prog:
./prog
- shell调用操作系统中的一个叫做加载器的函数,将可执行文件prog的代码和数据复制到内存,将控制转移到这个程序的开头
静态链接
- Linux LD静态链接器,以一组可重定位的目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出
- 构造可执行文件链接器必须完成两个任务:1)符号解析;2)重定位
- 符号解析,符号解析的目的是为了使每个符号引用正好和一个符号定义相关联;
- 重定位,编译器和汇编器生成次若干个地址0开始的代码和数据节。链接器通过每个符号定义与一个内存位置关联来重定位这些节,然后修改所有对这些符号的引用,使他们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别的执行这样的重定位
- 目标文件纯粹是字节块的集合
目标文件
- 目标文件有三种形式:可重定位目标文件、可执行目标文件、共享目标文件
- 可重定位目标文件:包含二进制代码和数据,可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
- 可执行目标文件:包含二进制代码和数据,其形式可以直接被复制到内存并运行
- 共享目标文件:一种特殊类型的可重定位目标文件,在加载或运行时被动态的加载进内存并链接
- 编译器和汇编器生成可重定位目标文件,链接器生成可执行目标文件
- 目标模块是字节序列,目标文件是以文件形式存放在磁盘中的目标模块
符号解析
- 每个可重定义的目标模块m都有一个符号表,包含m定义和引用的符号的信息,在链接器的上下文中有三种不同的符号:
- 由模块定义并能被其他模块引用的全局符号,全局链接器符号对应于非静态的C函数和全局变量
- 由其他模块定义并被模块m引用的全局符号,这些符号被称为外部符号,对应其他模块中定义的非静态的C函数和全局变量
- 只被模块m定义和引用的局部符号,对应带static属性的C函数和全局变量
- 可以利用static变量隐藏变量和函数名字
- 符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号
- 每个符号被分配到目标文件的某个节,由section字段表示,这个字段也是到节头部表的一个索引
- 可重定位文件中有三个特殊的伪节:
- ABS表示不该被重定位的符号
- UNDEF表示未定义的符号(在本目标模块中引用,在其他模块中定义)
- COMMON表示还未被分配位置的未初始化的数据目标(value给出对其目标,size给出最小的大小)
- GNU READELF是一个查看目标文件内容的很方便的工具
- 重载中,将每个函数和参数列表组合编码成一个对链接器来说唯一的名字,这种编码过程称为重整;相反的过程称为恢复
- 多个模块定义多个同名全局变量,编译时编译器向汇编器输出每个全局符号:函数和已初始化的全局变量是强符号、未初始化的全局变量是弱符号
- 不允许有多个同名的强符号
- 如果有一个强符号多个弱符号,选择强符号
- 如果有多个弱符号,则从多个弱符号中随机选一个
- 与静态库链接 将所有相关目标打包成一个单独的文件称为静态库,可以用作链接器的输入,当链接器构造一个输出的可执行文件时,只复制静态库里被应用程序引用的目标模块
- 相关函数可以被编译成独立的目标模块,封装成一个独立的静态库文件。链接时只复制被程序引用的模块,减少了可执行文件在内存中的大小;应用程序员只需要包含较小的库函数的名字
Linux中,静态库以一种称为存档的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合,有一个头部来描述每个成员目标文件大小和位置。存档文件名以.a为标识
1
2
3
4
5
6gcc-c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.c #创建静态库
gcc -static -o prog2c main2.o ./libvector.a
#static参数高速编译器驱动程序,链接器创建一个完全链接的可执行文件,可以加载到内存并运行
#在当前目录下查找libvector.a关于库的一般准则是将他们放在命令行的结尾,如果库的成员是相互独立的,则库可以以任何顺序放置在命令的结尾处;否则需要对库排序;如果需要满足依赖需求,可以在命令行上重复库;另一种方法是将其中某些文件合并成一个单独的存档文件
重定位
- 合并输入模块,为每个符号分配运行时地址,重定位分为两步:
- 重定位节和符号定义:将所有相同类型的节合并为同一类型的新聚节,完成后程序的每条指令和全局变量都有唯一的运行时内存地址
- 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得他们只想正确的运行时地址
- 重定位条目
-汇编器遇到对最终位置未知的目标引用,就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用,就会生成一个重定位条目高速链接器将目标文件合并成可执行文件时如何修改这个引用1
2
3
4
5
6typedef struct{
long offset; //offset是被修改的引用的节偏移
long type:32, //type告诉链接器如何修改新的引用
symbol:32;
long addend; //addend是一个有符号常数
}Elf64_Rela;
- ELF定义了32种不同的重定位类型,其中两种基本的定位类型:R_X86_64_PC32和R_X86_64_32,两种重定位类型都支持x86_64小型代码模型
R_X86_64_PC32
:重定位使用一个32位PC相对地址的引用,PC值通常是下一条指令在内存中的地址R_X86_64_32
:重定位使用一个32位的绝对地址的引用
- 重定位符号引用
- 可执行目标文件是完全链接的,ELF可执行文件被设计的很容易加载到内存,可执行文件的连续的片被映射到连续的内存段。程序头部表描述了这种映射关系
- 加载可执行目标文件
./prog
通过调用驻留在存储器中称为加载器的操作系统代码来运行它,任何Linux程序都可以通过调用execve函数来调用加载器。将程序的代码和数据复制到内存并且从程序入口运行的过程称为加载- 每个Linux程序运行都有一个内存映像,在Linux x86-64系统中代码段总是从地址0x400000处开始,后面是数据段,运行时,堆在数据段之后通过调用malloc库往上增长
- 内存映像:
- 在程序头部表的指导下,加载器将可执行文件的片复制到代码段和数据段;加载器跳转到程序的入口点(_start函数的地址),这个函数在系统目标文件ctrl.o中定义;_start函数调用系统启动函数__libc_start_main,定义在libc.so中;初始化执行环境,调用用户层main函数
动态链接共享库
- 共享库是一个目标模块,在运行或加载时可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,是由动态链接器执行的。
- 共享库也称为共享目标,Linux系统中通常用.so后缀,微软的操作系统使用的动态链接库(DDL)
- 给定文件系统中,一个库只有一个.so文件,所有引用该库的可执行目标文件共享这个文件。在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享
gcc -shared -fpic -o libvector.so addvec.c multvec.c #-shared选项只是链接器创建一个共享目标文件
gcc -o prog21 main2.c ./libvector.so #链接到示例程序中
- 加载器加载执行prog21,prog21包含一个.interp节,这一节包含动态链接器的路径名,动态链接器本身就是一个共享目标,加载和运行这个动态链接器。
- 动态链接器通过执行下面的重定位来完成链接任务
- 重定位libc.so的文本和数据到某个内存段
- 重定libvector.so的文本和数据到另一个内存段
- 重定位prog21中所有对由libc.so和libvector.so定义的符号的引用
- 动态链接器将控制传递给应用程序,共享库的位置不会再改变了
- 应用程序可能在运行时要求动态链接器加载和链接某个共享库,无需在编译时将这些库链接到应用中
- 将每个生成的动态内容的函数打包在共享库中,当一个来自Web浏览器请求到达时,服务器动态地加载和链接适当的函数,然后调用它们。函数会一直缓存在服务器的地址空间中,只需要简单的函数调用的开销就可以处理随后的请求。在运行时无需停止服务器就更新已存在的函数
位置无关代码(Position-Independent Code,PIC)
- 多个进程如何共享一个程序的副本:
- 给每个共享库分配一个预备的专用地址空间片,但是有很多严重的问题
- 编译共享模块的代码块,可以把它们加载到内存的任何位置而无需修改,无数个进程可以共享一个共享模块的单一副本。
- PIC数据引用:在数据段开始的地方创建一个全局偏移变量表(Global Offset Table,GOT),GOT是数据段的一部分
- 每个被目标模块引用的全局数据目标都有一个8字节条目
- 编译器为GOT中的每个条目生成一个重定位记录
- 在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址
- 每个引用全局变量的目标模块都有自己的GOT
3.PIC函数调用:通过延迟绑定,将过程地址的绑定延迟到第一次调用该过程时。是通过GOT和过程链接表(Procedure Linkage Table,PLT)之间的交互来完成的。PLT是代码段的一部分
- 目标模块调用定义在共享库的任何函数都有自己的GOT和PLT