文章目录
链接
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存并执行。链接通常是由链接器来自动执行的。
链接器可以实现分离编译,将大型应用程序分解成更小的、更好管理的模块,可以独立地修改和编译这些模块,当改变这些模块中地一个时,只需要简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
执行时机:
- 编译时:源代码被翻译成机器代码时
- 加载时:程序被加载器加载到内存并执行时
- 运行时:由应用程序来执行
静态链接
静态链接器以一组可重定位目标文件(输入的可重定位目标文件由各种不同的代码和数据节(section)组成,每一节都是一个连续的字节序列)和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。
链接器必须完成的两个主要任务:
- 符号解析(symbol resolution):将每个符号引用和一个符号定义关联起来。目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。
- 重定位(relocation):编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
目标文件
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。一个目标模块就是一个字节序列,一个目标文件就是一个以文件形式存放在磁盘中的目标模块。
目标文件有三种形式:
- 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其它可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
可重定位目标文件
ELF(可执行可链接格式)头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节序列。
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息(ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移、节头部表中条目的大小和数量)。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。夹在ELF头和节头部表之间的都是节。
一个典型的ELF可重定位目标文件包含下面几个节:
- .text:已编译程序的机器代码
- .rodate:只读数据
- .data:已初始化的全局和静态C变量。局部变量在运行时保存在栈中。
- .bss:未初始化的全局变量和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
- .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
- .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其它文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,调用本地函数的指令则不需要修改。
- .rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
- .debug:一个调式符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。
- .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号信息。
在链接器的上下文中,有三种不同的符号:
- 由模块m定义并能被其它模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量。
- 由其它模块定义并被模块m引用的全局模块。这些符号称为外部符号,对应于在其它模块中定义的非静态C函数和全局变量。
- 只被模块m引用和定义的局部符号。它们对应于带static属性的C函数和全局变量。这些符号在模块m中任意位置都可见,但是不能被其它模块引用。
符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。
/* ELF符号表条目 */
typedef struct {
int name; // String table offset
char type:4, // function or data(4 bytes)
binding:4; // local or global(4 bytes)
char reserved; // unused
short section; // section header index
long value; // section offset or absolute address
long size; // object size in byte
} Elf64_Symbol;
符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目,所以这些目标的类型也有所不同。
每个符号都被分配到目标文件的某个节,由section字段表示,该字段也是一个到节头部表的索引。
可重定位目标模块有三个特殊的伪节,它们在节头部表中是没有条目的:
- ABS:不该被重定位的符号
- UNDEF:未定义的符号,即在本目标模块中引用,但是却在其它地方定义的符号
- COMMON:还未被分配位置的未初始化的数据目标
符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。符号解析完之后,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。
编译器只允许每个模块中每个局部符号有一个定义。当编译器遇到一个不是在当前模块中定义的符号时,会假设该符号是其它某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用的符号的定义,就输出一条错误信息并终止。
函数重载,编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字,这种编码过程叫做重整,而相反的过程叫做恢复。Foo::bar(int, long)
被编码成bar__3Fooil
链接器如何解析多重定义的全局符号
在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
根据强弱符号的定义,Linux链接器处理多重定义的符号名的规则如下:
- 不允许有多个同名的强符号
- 如果有一个强符号和多个弱符号同名,那么选择强符号
- 如果有多个弱符号同名,那么任选一个弱符号
与静态库链接
静态库(将所有相关的目标模块打包成一个单独的文件)可以用作链接器的输入,当链接器构造一个输出的可执行文件时,只复制静态库里被应用程序引用的目标模块。
Linux中静态库以一种称为存档的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。
链接器如何使用静态库来解析引用
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序(驱动程序自动将命令行中的所有.c文件翻译成.o文件)命令行上出现的顺序来扫描可重定位目标文件和存档文件。在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析(引用了但是尚未定义的符号)的符号集合U,一个在前面输入文件中已定义的符合的集合D。初始时,E、U、D均为空。
- 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输入文件。
- 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有成员目标文件都以此进行这个过程,直到U和D都不再发生变化。此时,任何不包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件。
- 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,构建输出的可执行文件。
关于库的一般准则是将它们放在命令行的结尾。如果需要满足依赖需求,可以在命令行上重复库。
- 如果库的成员是相互独立的,那么这些库就可以以任何顺序放置在命令行的结尾处
- 如果库不是相互独立的,那么必须对它们排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后的
重定位
合并输入模块,并为每个符号分配运行时地址。
重定位由两步组成:
- 重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。此时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
- 重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。这一步链接器依赖于可重定位目标模块中的重定位条目。
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。
无论何时汇编器遇到对最终位置未知的引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data中。
/* ELF重定位条目格式,每个条目表示一个必须被重定位的引用,并指明如何计算被修改的引用 */
typedef struct {
long offset; // offset of the reference to relocate
long type:32, // relocation type
symbol:32; // symbol table index
long addend; // constant part of relocation expression
};
ELF定义了32种不同的重定位类型:
- R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址,PC值通常是下一条指令在内存中的地址。
- R_X86_64_32:重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
重定位符号引用,不懂
可执行目标文件
- .init节定义了一个
_init
函数,程序的初始化代码会调用它。
加载可执行目标文件
加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行程序。
加载:将程序复制到内存并运行的过程
当加载器运行时,创建类似于图 7-15所示的内存映像(每个Lunux程序都有一个运行时内存映像)。
在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段–>加载器跳转到程序的入口点(_start函数的地址,定义在系统目标文件ctrl.o中,对所有的C程序都是一样的)–>_start函数调用系统启动函数__libc_start_main
(定义在libc.o中)–>__libc_start_main
初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。
加载器工作概述:Linux系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制。此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
动态链接共享库
共享库(共享目标)是一个目标模块,在运行和加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,由动态链接器的程序来执行。
当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
动态链接器将控制传递给应用程序时,共享库的位置就固定了,并且在程序执行的过程中都不会改变。
从应用程序中加载和链接共享库
Linux系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
成功:指向句柄的指针
出错:NULL
void *dlsym(void *handle, char *symbol);
int dlclose(void *handle);
const char *dlerror(void);
位置无关代码
位置无关代码(可以加载而无需重定位的代码)。
库打桩机制
库打桩允许你截获对共享库函数的调用,取而代之执行自己的代码。打桩可以发生在编译时、链接时或当程序被加载和执行时。
工具
- GNU READELF:查看目标文件内容
- AR:创建静态库,插入、删除、列出和提取成员
- OBJDUMP:显示一个目标文件中所有的信息
- LDD:列出一个可执行文件在运行时所需要的共享库
- READELF:显示要给目标文件的完整结构,包括ELF头中编码的所有信息
- SIZE:列出目标文件中节的大小和名字
- NM:列出目标文件的符号表中定义的符号
- STRIP:从目标文件中删除符号表信息
- STRINGS:列出一个目标文件中所有可打印的字符串