读书-程序员的自我修养-链接、封装与库(20:第七章:动态链接(4)动态链接的步骤和实现)
动态链接的三个步骤:
- 启动动态连接器本身
- 装载所有需要的共享对象
- 重定位和初始化
1. 动态连接器自举
1.1 动态链接器本身也是一个共享对象
普通共享对象文件来说,它的重定位工作由动态链接器完成。
它也可以依赖其他共享对象,其中被依赖的共享对象由动态连接器负责链接和装载。
1.2 动态连接器特点:不依赖,自己玩,不调用
- 不依赖其他任何共享对象
- 本身所需要的全局变量和静态变量的重定位工作由自己完成
- 不可调用函数
1.3 自举:具有一定限制条件的启动代码往往被称作自举
2. 装载共享对象
2.1 全局符号表
完成自举后,动态连接器将可执行文件和连接器本身的符号表
都会合并到一个符号表当中,我们称为全局符号表(Global Symbol Table)。
2.2 装载过程步骤
- 链接器寻找可执行文件所依赖的共享对象
- 通过 .dynamic 段列出那些依赖的共享对象
- 将这些共享对象的名字防盗一个装载集合中
- 依次取出名字,然后打开共享对象,读取ELF头和.dynamic段
- 将它相应的代码段和数据段映射到进程空间中
- 如果有嵌套依赖,就循环装载
2.3 装载顺序:广度优先和深度优先
一般是广度优先的顺序装载
2.4 符号的优先级
2.4.1 问题:多个共享对象定义同一个符号怎么处理?
2.4.2 例子说明
有四个共享对象 a1.so a2.so b1.so b2.so,他们的源代码文件分别为
a1.c a2.c b1.c b2.c
/* a1.c */
#include<stdio.h>
void a()
{
printf("a1.c\n");
}
/* a2.c */
#include<stdio.h>
void a()
{
printf("a2.c\n");
}
/* b1.c */
#include<stdio.h>
void a();
void b1()
{
a();
}
/* b2.c */
#include<stdio.h>
void a();
void b2()
{
a();
}
/* main.c */
#include<stdio.h>
void b1();
void b2();
int main()
{
b1();
b2();
return 0;
}
我们假设b1.so 依赖于a1.so b2.so 依赖于a2.so.
将b1.so 与 a1.so进行链接, b2.so 与 a2.so进行链接。
gcc -fPIC -shared a1.c -o a1.so
gcc -fPIC -shared a2.c -o a2.so
gcc -fPIC -shared b1.c a1.so -o b1.so
gcc -fPIC -shared b2.c a2.so -o b2.so
root@ubuntu-admin-a1:/home/6Chapter# ldd b1.so
linux-vdso.so.1 => (0x00007ffdb9fd7000)
a1.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2f93d0b000)
/lib64/ld-linux-x86-64.so.2 (0x0000557f9b818000)
root@ubuntu-admin-a1:/home/6Chapter# ldd b2.so
linux-vdso.so.1 => (0x00007ffd7e4cc000)
a2.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb8a670b000)
/lib64/ld-linux-x86-64.so.2 (0x000055dbbaa2d000)
root@ubuntu-admin-a1:/home/6Chapter#
mian 依赖于b1.so和b2.so,b1.so依赖于a1.so,b2.so依赖于a2.so。
所以当动态链接器对main进行链接时,这四个动态库都会被装载到进程的地址空间中。
并且它们中的符号都会被并入到全局符号表。
但是,执行mian打印两次 a1.so 没有打印 b2.so 。
因为 a2.so中的函数被忽略了,a2被a1覆盖了,这就是全局符号介入。
2.4.3 全局符号介入:符号覆盖
一个共享对象里面的全局符号被另外一个共享对象的同名全局符号覆盖的现象,称作共享对象全局符号介入。
2.4.4 linux处理规则:后面的同名符号被忽略
当一个符号需要被加入全局符号表时,如果相同的符号名已存在,
则后加入符号被忽略。
3. 重定位和初始化
3.1 重定位和初始化的过程
第二步完成后,连接器开始重新遍历可执行文件和每个共享对象的重定位表,将他们的GOT/PLT中的每个需要重定位的位置进行修正。
3.2 重定位和初始化的结果:
所需要的共享对象也都已经装载并链接完成,此时链接器的任务完成了,将进程的控制权交给程序的入口地址。
4. linux动态链接器的实现
4.1 linux动态链接器本身是一个共享对象,路径是 /lib/ld-linux.so.2
它实际上是一个软连接,它指向 /lib/ld-x.y.z.so 这才是真正的动态链接器文件。
4.2 动态链接器不仅是共享对象,也是可执行程序
共享对象其实也是ELF文件,它也有跟可执行文件一样的ELF文件头。
可以执行: /lib/ld-linux.so.2
4.3 几个问题
4.3.1 动态链接器本身是静态链接的
因为它不能依赖其他共享对象
4.3.2 动态链接器本身不必须是PIC的,但往往是PIC
是否是 PIC 不是关键,但一般是的
因为:
- 如果不是 PIC,会使得代码段无法共享,浪费内存
- 也会使得ld.so 本身初始化更加复杂。