CSAPP-07链接
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
- 链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;
- 也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;
- 甚至执行于运行时(run time),也就是由应用程序来执行。
在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。
链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分即为更小、更好管理的模块,可以独立地修改和编译这些模块。当改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
重点:
- 传统静态链接;
- 加载时的共享库的动态链接;
- 运行时的共享库的动态链接。
静态链接
- 符号解析(sumbol resolution):目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
- 重定位(relocation):编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
基本事实:
目标文件纯粹是字节块的集合。有些包含程序代码,有些包含程序数据,而其他的则包含引导链接器和加载器的数据结构。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。链接器对目标机器了解甚少。
目标文件
有三种形式:
- 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件:包含二进制代码和数据,其形式可以被直接复制到内存并运行。
- 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。
各个系统的目标文件格式都不相同:
- Unix:a.out;
- Windows:可移植可执行(Protable Executable,PE)格式;
- MaxOS-X:Mach-O格式;
- x86-64 Linux和Unix系统:可执行可链接格式(Executable and Linkable Format,ELF)。
可重定位目标文件
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
- .data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
- .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,仅仅是一个占位符。
- .symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息。
- .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
- .rel.data:被模块引用或定义的所有全局变量的重定位信息。
- .debug:一个调试符号表。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。
- .strtab:一个字符串表。其内容包括.symmtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串的序列。
1
2
3
gcc -Og -S -g static_val_test.c
gcc -Og -c -g static_val_test.c
readelf -all static_val_test.o
符号和符号表
每个可重定位目标模块m都有一个符号表,包含m定义和引用的符号的信息,有三种符号:
- 由模块m定义并能被其他模块引用的全局符号。对应于非静态的C函数和全局变量。
- 由其他模块定义并被模块m引用的全局符号,称为外部符号。对应于在其他模块中定义的非静态C函数和全局变量。
- 只被模块m定义和引用的局部符号。对应于带static属性的C函数和全局变量,不能被其他模块引用。
认识到本地链接器符号和本地程序变量不同是很重要的。.symtab中的符号表不包含对应于本地非静态程序变量的任何符号。这些符号在运行时在栈中被管理。链接器对此类符号不感兴趣。
有趣的是,定义为带有static属性的本地过程变量是不在栈中管理的。相反,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。
符号解析
相同模块的局部符号的引用,符号的解析是非常简单明了的。
对全局符号的引用解析就棘手得多。
链接器如何解析多重定义的全局符号
在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
- 规则1:不允许有多个同名的强符号;
- 规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
- 规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
与静态库链接
链接器如何使用静态库来解析引用
重定位
合并输入模块,为每个符号分配运行时地址。由两步组成:
- 重定位节和符号定义:将所有相同类型的节合并为同一类型的新的聚合节。
- 重定位节中的符号引用:修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
重定位条目
无论何时,汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。
两种最基本的重定位类型:
- R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。
- R_X86_64_32:重定位一个使用32位绝对地址的引用。
重定位符号引用
可执行目标文件
加载可执行目标文件
通过某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。
任何linux程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行程序。这个程序复制到内存并运行的过程叫做加载。
每个linux程序都有一个运行时内存映像。
动态链接共享库
共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。
共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。
共享库也称为共享目标(shared object),在Linux系统中通常用.so后缀来表示,微软的操作系统大量的使用了共享库,称为DLL(动态链接库)。
从应用程序中加载和链接共享库
1
2
3
4
5
6
7
8
9
10
11
#include <dlfcn.h>
// 加载和链接共享库filename
// 若成功返回指向句柄的指针,若出错则为NULL
void *dlopen(const char *filename, int flag);
// 输入是一个指向前面已经打开了的共享库的句柄和一个sumbol名字。
// 若成功返回指向符号的指针,若出错则为NULL
void *dlsym(void *handle, char *symbol);
// 如果没有其他共享库还在使用这个共享库,dlclose卸载该共享库
int dlclose (void *handle);
共享库和Java本地接口
Java本地接口(Java Native Interface,JNI),基本思想是将本地C函数编译到一个共享库中,当一个正在运行的Java程序试图调用函数时,Java解释器利用dlopen接口(或者与其类似的接口)动态链接和加载共享库,然后再调用函数。
位置无关代码
共享库的一个主要目的就是允许多个正在运行的进程共享内存中相同的库代码,从而节约宝贵的内存资源。
多个进程如何共享程序的一个副本的?
可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC)。对GCC使用 -fpic 选项指示编译系统生成PIC代码。共享库的编译必须总是使用该选项。
PIC数据引用
编译器通过运用一下这个有趣的事实来生成对全局变量的PIC引用:无论我们在内存中的何处加载一个目标模块(包括共享目标模块),数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。
PIC函数调用
延迟绑定(lazy binding):将过程地址的绑定推迟到第一次调用该过程时 。
避免动态链接器在加载时进行成百上千个其实并不需要的重定位。
第一次调用过程的运行时开销很大,但是其后的每次调用都只回花费一条指令和一个间接的内存引用。
延迟绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的:
- 全局偏移量表(Global Offset Table,GOT);
- 过程链接表(Procedure Linkage Table,PLT)。
库打桩机制
库打桩(library interpositioning):允许截获对共享库函数的调用,取而代之执行自己的代码。
- 追踪对某个特殊库函数的调用次数;
- 验证和追踪它的输入和输出值;
- 甚至把它替换成一个完全不同的实现。
编译时打桩
int.c
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <malloc.h>
int main()
{
int *p = malloc(32);
free(p);
return(0);
}
malloc.h
1
2
3
4
5
6
/* Local malloc header file */
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
mymalloc.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* 1.编译时打桩
* gcc -DCOMPILETIME -c mymalloc.c
* gcc -I. -o intc int.c mymalloc.o
* ./intc
*
* 输出:
* 编译时打桩:malloc(32)=0x7ffff4936260
* 编译时打桩:free(0x7ffff4936260)
*/
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h>
/* malloc wrapper function */
void *mymalloc(size_t size)
{
void *ptr = malloc(size);
printf("编译时打桩:malloc(%d)=%p\n",
(int)size, ptr);
return ptr;
}
/* free wrapper function */
void myfree(void *ptr)
{
free(ptr);
printf("编译时打桩:free(%p)\n", ptr);
}
#endif
链接时打桩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
* 链接时打桩
* gcc -DLINKTIME -c mymalloc.c
* gcc -c int.c
* gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
* ./intl
*
* 输出:
* 链接时打桩:malloc(32) = 0x7fffe72a7260
* 链接时打桩:free(0x7fffe72a7260)
*/
#ifdef LINKTIME
#include <stdio.h>
void *__real_malloc(size_t size);
void __real_free(void *ptr);
/* malloc wrapper function */
void *__wrap_malloc(size_t size)
{
void *ptr = __real_malloc(size); /* Call libc malloc */
printf("链接时打桩:malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
/* free wrapper function */
void __wrap_free(void *ptr)
{
__real_free(ptr); /* Call libc free */
printf("链接时打桩:free(%p)\n", ptr);
}
#endif
运行时打桩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
* 运行时打桩
* gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
* gcc -o intr int.c
* LD_PRELOAD="./mymalloc.so" ./intr
*/
#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
/* malloc wrapper function */
void *malloc(size_t size)
{
void *(*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc"); /* Get address of libc malloc */
if ((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size); /* Call libc malloc */
printf("运行时打桩:malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
/* free wrapper function */
void free(void *ptr)
{
void (*freep)(void *) = NULL;
char *error;
if (!ptr)
return;
freep = dlsym(RTLD_NEXT, "free"); /* Get address of libc free */
if ((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}
freep(ptr); /* Call libc free */
printf("运行时打桩:free(%p)\n", ptr);
}
#endif