Dynamic Shared Objects

https://www.xuenixiang.com/thread-3187-1-1.html

ELF 文件类型

什么是动态链接  

随着系统中可执行文件的增加,静态链接带来的磁盘和内存空间浪费问题愈发严重。例如大部分可执行文件都需要 glibc,那么在静态链接时就要把 libc.a 和编写的代码链接进去,单个 libc.a 文件的大小为 5M 左右,那么 1000 个就是 5G。两个静态链接的可执行文件都包含 testLib.o,那么在装载入内存时,两个相同的库也会被装载进去,造成内存空间的浪费。静态链接另一个明显的缺点是,如果对标准函数做了哪怕一点很微小的改动,都需要重新编译整个源文件,使得开发和维护很艰难。如果不把系统库和自己编写的代码链接到一个可执行文件,而是分割成两个独立的模块,等到程序真正运行时,再把这两个模块进行链接,就可以节省硬盘空间,并且内存中的一个系统库可以被多个程序共同使用,还节省了物理内存空间。这种在运行或加载时,在内存中完成链接的过程叫作动态链接,这些用于动态链接的系统库称为共享库,或者共享对象,整个过程由动态链接器完成。  

如图右半部所示,func1.ELF 和 func2.ELF 中不再包含单独的 testLib.o,当运行 func1.ELF 时,系统将 func1.o 和依赖的 testLib.o 装载入内存,然后进行动态链接。完成后系统将控制权交给程序入口点,程序开始执行。接下来,当 func2.ELF 想要执行时,由于内存中已经有 testLib.o,因此不再重复加载,直接进行链接即可。  

GCC 默认使用动态链接编译,通过下面的命令我们将 func.c 编译为共享库,然后使用这个库编译 main.c。参数 -shared 表示生成共享库,-fpic 表示生成与位置无关的代码。这样可执行文件 func.ELF2 就会在加载时与 func.so 进行动态链接。需要注意的是,动态加载器 ld-linux.so 本身就是一个共享库,因此加载器会加载并运行动态加载器,并由动态加载器来完成其他共享库以及符号的重定位。  

位置无关代码  

可以加载而无须重定位的代码称为位置无关代码(Position-Independent Code,PIC),它是共享库必须具有的属性,通过给 GCC 传递 -fpic 参数可以生成 PIC。通过 PIC,一个共享库的代码可以被无限多个进程所共享,从而节约内存资源。由于一个程序(或者共享库)的数据段和代码段的相对距离总是保持不变的,因此,指令和变量之间的距离是一个运行时常量,与绝对内存地址无关。于是就有了全局偏移量表(Global Offset Table, GOT),它位于数据段的开头,用于保存全局变量和库函数的引用,每个条目占 8 个字节,在加载时会进行重定位并填入符号的绝对地址。实际上,为了引入 RELRO 保护机制,GOT 被拆分为.got 节和.got.plt 节两个部分,不需要延迟绑定的前者用于保存全局变量引用,加载到内存后被标记为只读;需要延迟绑定的后者则用于保存函数引用,具有读写权限。 ​

延迟绑定  

由于动态链接是由动态链接器在程序加载时进行的,当需要重定位的符号(库函数)多了之后,势必会影响性能。延迟绑定(lazy binding)就是为了解决这一问题,其基本思想是当函数第一次被调用时,动态链接器才进行符号查找、重定位等操作,如果未被调用则不进行绑定。  

ELF 文件通过过程链接表(Procedure Linkage Table, PLT)和 GOT 的配合来实现延迟绑定,每个被调用的库函数都有一组对应的 PLT 和 GOT。  

位于代码段.plt 节的 PLT 是一个数组,每个条目占 16 个字节。其中 PLT[0] 用于跳转到动态链接器,PLT[1] 用于调用系统启动函数__libc_start_main(),我们熟悉的 main() 函数就是在这里面调用的,从 PLT[2] 开始就是被调用的各个函数条目。  

位于数据段.got.plt 节的 GOT 也是一个数组,每个条目占 8 个字节。其中 GOT[0] 和 GOT[1] 包含动态链接器在解析函数地址时所需要的两个地址(.dynamic 和 relor 条目),GOT[2] 是动态链接器 ld-linux.so 的入口点,从 GOT[3] 开始就是被调用的各个函数条目,这些条目默认指向对应 PLT 条目的第二条指令,完成绑定后才会被修改为函数的实际地址。