由于对静态链接和动态链接的概念已经动作有所不了解,因此特意写了这篇文章进行初步的梳理,主要参考《深入理解计算机系统》这本书。
1 静态链接
1.1 静态链接概念
静态链接以一组可重定位的目标文件为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节组成。 为了创建可执行文件,链接器必须完成两个主要任务:符号解析和重定位。
1.2 符号解析
程序的链接——符号解析
链接器解析符号表.symtab,将每个符号引用和符号定义联系起来。如下图所示,符号解析就是去干箭头干的活。
符号解析的整体过程如下:
- 程序中有定义和引用的符号(包括函数和变量)
- 编译器将定义的符号放在符号表中
- 符号表是一个结构数组
- 每个表项包含符号名、长度位置等信息
- 链接器将每个符号的引用都与一个确定的符号定义建立关联
1.3 重定位
合并输入模块,为每个符号分配运行时地址。
1.3.1 重定位表目
汇编器遇到对最终位置未知的目标引用的时候,就会生成一个可重定位的表目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。.relo.text
,.relo.data
这两个节存放了这些表目。
1.3.2 重定位流程
在.o
文件中,可以看出符号引用位置的值都是未知的,因此需要在重定位的时候,将其填充为正确的地址。如main
函数的地址d
处的4个全为0的字节,这部分需要在链接阶段通过链接器将array的地址填充进去。
d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 14 <main+0x14>
test@iZuf6bogmr00a004vw0sbeZ:~/ctest/csapp_test$ objdump -S main.o
main.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
************************************************************************/
int sum(int *a, int n);
int array[2] = {1, 2};
int main() {
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
int val = sum(array, 2);
8: be 02 00 00 00 mov $0x2,%esi
d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 14 <main+0x14>
14: e8 00 00 00 00 callq 19 <main+0x19>
19: 89 45 fc mov %eax,-0x4(%rbp)
return val;
1c: 8b 45 fc mov -0x4(%rbp),%eax
}
1f: c9 leaveq
20: c3 retq
1.4 静态库
静态库是一系列可重定位目标文件的集合,在Linux系统下,通过ar
工具将其打包为一个.a
文件。
静态库出现的原因:使用静态库而不是直接使用可重定位的目标文件的原因在于,如果直接使用一个大的可重定位目标文件,那么链接器会将整个可重定位目标文件链接到可执行文件中,如果由多个可执行文件,就会导致每个可执行文件都有这样的拷贝,可执行文件的大小大大增加。而静态库打包了一组可重定位目标文件,在链接的时候,只会拷贝那些被程序引用的目标模块。
静态库解析顺序:在符号解析阶段,链接器从左到右来扫描可重定位目标文件和存档文件。
动态链接
一文看懂动态链接 动态链接和静态链接不同,动态链接在链接阶段并不会将共享库拷贝到可执行文件中,而是在程序加载或者运行时进行通过动态链接器进行加载。
大概思路是这样,程序a和b都依赖模块c,那么等程序a运行的时候,才链接c,c此时被加载进内存。程序b运行时,直接跟内存中的c进行链接,不再单独加载。 这样,无论是在磁盘还是内存中,都只有一份c模块,完美。
这里需要强调一下,这里所谓的共享,并不是共享整个Lib.c的内容,而是特指共享它的代码部分。 对于Lib.c中的数据部分,每个进程都需要一份自己的拷贝,因为它们可能需要独立地修改Lib.c中的数据。
动态链接的GOT和PLT表
动态库有两个特性,一是所有进程共享同一个动态库,即整个内存空间中只有一个动态库被加载;二是,动态库共享的是代码,而其数据区是不共享的,每个进程有自己单独的数据区。
GOT
因此动态库代码区如果引用到了数据区的数据,需要使用GOT来确定这个数据,保证每个进程读取到的是自己进程的数据。
PLT
可执行程序在调用到动态库的函数时,是不知道其地址的,在链接阶段也不知道。只有在程序加载进内存调用到动态链接器的时候,才知道函数的地址,但是此时我们是在加载阶段,并不能直接修改代码区的数据,因此可以定义一个标记指向数据区的地址,这个地址保存着被调函数的地址,可以在动态加载的时候被修改,这个区域就叫PLT
。(其实上述PLT
的功能和GOT
的功能是一样的),编译器加入PLT
的主要作用是实现了延迟绑定。
延迟绑定
在一开始的时候,GOT
表中的函数地址并不是指向真的函数。可执行程序第一次执行到共享库的函数的时候,会先调用动态链接器
,将GOT
表中的函数地址修改为真正的函数地址,等到第二次执行到共享库函数的时候,会直接调用到GOT
的地址,而不会再调用动态链接器
了。