共享库之动态连接
静态链接浪费内存和磁盘空间、模块更新困难等问题,因此寻找一种更好的办法来组织程序模块。
静态链接对程序的更新、部署和发布也会带来很多麻烦。
动态链接:
就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。
动态链接的方式使得开发过程中各个模块更加独立、耦合度更小,便于不同的开发者和开发组织之间进行独立的开发和测试。
动态链接还有一个特点就是程序在运行时可以动态的选择加载各种程序模块,使得插件成为可能。
Linux系统中,ELF动态链接文件被称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象,它们一般都是以“.so”为扩展名;动态链接文件被称为动态链接库。在windows下为.dll。
- /*Program1.c */
- #include "Lib.h"
- int main()
- {
- foobar(1);
- return 0;
- }
- /*Program2.c*/
- #include "Lib.h"
- int main()
- {
- foobar(2);
- return 0;
- }
- /*Lib.c*/
- #include <stdio.h>
- void foobar(int i)
- {
- printf("Printing from Lib.so %d\n",i);
- }
- /*Lib.h*/
- #ifndef LIB_H
- #define LIB_H
- void foobar(int i);
- #endif
/*Program1.c */ #include "Lib.h" int main() { foobar(1); return 0; } /*Program2.c*/ #include "Lib.h" int main() { foobar(2); return 0; } /*Lib.c*/ #include <stdio.h> void foobar(int i) { printf("Printing from Lib.so %d\n",i); } /*Lib.h*/ #ifndef LIB_H #define LIB_H void foobar(int i); #endif
horst@debian:~/horstdemo/simpleDynamic$ gcc -fPIC -shared -o mylib.so lib.c
将Lib.c 编译成为一个共享对象文件,‘-shared’表示产生共享对象
-fPIC(Position Independent Code)表示使用地址无关代码技术来产生输出文件
horst@debian:~/horstdemo/simpleDynamic$ gcc -o program1 program1.c ./mylib.so
利用 ./mylib.so分别进行编译链接。
运行结果:
- horst@debian:~/horstdemo/simpleDynamic$ ./program1 &
- [1] 14969
- horst@debian:~/horstdemo/simpleDynamic$ Printing from lib.so 1
horst@debian:~/horstdemo/simpleDynamic$ ./program1 & [1] 14969 horst@debian:~/horstdemo/simpleDynamic$ Printing from lib.so 1
查看进程的虚拟地址空间:
可以看到有Lib.so映射文件。
horst@debian:~/horstdemo/simpleDynamic$ cat /proc/14969/maps
08048000-08049000 r-xp 00000000 08:05 395370 /home/horst/horstdemo/simpleDynamic/program1
08049000-0804a000 rw-p 00000000 08:05 395370 /home/horst/horstdemo/simpleDynamic/program1
b7e35000-b7e36000 rw-p b7e35000 00:00 0
b7e36000-b7f8b000 r-xp 00000000 08:01 1971754 /lib/i686/cmov/libc-2.7.so
b7f8b000-b7f8c000 r--p 00155000 08:01 1971754 /lib/i686/cmov/libc-2.7.so
b7f8c000-b7f8e000 rw-p 00156000 08:01 1971754 /lib/i686/cmov/libc-2.7.so
b7f8e000-b7f91000 rw-p b7f8e000 00:00 0
b7f9f000-b7fa0000 rw-p b7f9f000 00:00 0
b7fa0000-b7fa1000 r-xp 00000000 08:05 395365 /home/horst/horstdemo/simpleDynamic/mylib.so
b7fa1000-b7fa2000 rw-p 00000000 08:05 395365 /home/horst/horstdemo/simpleDynamic/mylib.so
b7fa2000-b7fa4000 rw-p b7fa2000 00:00 0
b7fa4000-b7fa5000 r-xp b7fa4000 00:00 0 [vdso]
b7fa5000-b7fbf000 r-xp 00000000 08:01 1962242 /lib/ld-2.7.so
b7fbf000-b7fc1000 rw-p 0001a000 08:01 1962242 /lib/ld-2.7.so
bfdab000-bfdc0000 rw-p bffeb000 00:00 0 [stack]
链接时重定位(静态链接);装载时重定位(动态链接)
地址无关代码:PIC,Position-independent Code,把指令中那些需要修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以再每个进程中拥有个副本。
指令跳转、调用 数据访问
模块内部 (1)相对跳转和调用 (2)相对地址访问
模块外部 (3)间接跳转和调用(GOT) (4)间接访问(GOT)
如果一个共享对象lib.so中定义了一个全局变量G,而进程A和B都使用了lib.so,那么当进程A改变这个全局变量G的值的时,进程B中G会受到影响么?
不会,因为当lib.so被两个进程加载时,它的数据段部分在每个进程中都有独立的副本,从这个角度看,共享对象中的全局变量实际上和定义在程序内部的全局变量没什么区别。如果是线程,则有影响。
延迟绑定:
动态链接比i链接慢的主要原因是动态链接下对于全局静态的数据访问要进行复杂的GOT定位,然后间接寻址.
由于很多函数在程序执行过程中不一定被用到(错误处理函数,特殊功能模块),ELF采用了一种叫延迟绑定(Lazy Binding)的做法,基本思想就是当函数第一次被用到时才进行绑定。
操作系统如何确定动态连接器?
在动态链接的ELF可执行文件中,有一个专门的段叫做“.interp”,如果我们使用objdump来查看:
horst@debian:~/horstdemo/simpleDynamic$ objdump -s ./program1
./program1: file format elf32-i386
Contents of section .interp:
8048114 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
8048124 2e3200
动态链接ELF中最重要的结构应该是“.dynamic”段,这个段里面保存了动态连接器所需要的基本信息,比如依赖哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
使用readelf工具查看“.dynamic”段的内容:
horst@debian:~/horstdemo/simpleDynamic$ readelf -d ./program1
Dynamic section at offset 0x588 contains 22 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [./mylib.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x804835c
0x0000000d (FINI) 0x804854c
0x00000004 (HASH) 0x8048148
0x6ffffef5 (GNU_HASH) 0x8048188
0x00000005 (STRTAB) 0x8048274
0x00000006 (SYMTAB) 0x80481c4
0x0000000a (STRSZ) 143 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049664
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048344
mylib.so中的foobar函数称为导出函数
为了表示动态链接这些模块之间的符号导出与导入关系,ELF专门有一个叫做动态符号表的段用来保存这些信息。这个段叫做“.dynsym”。
可以使用如下命令来查看动态符号表:
horst@debian:~/horstdemo/simpleDynamic$ readelf -sD ./mylib.so
Symbol table for image:
Num Buc: Value Size Type Bind Vis Ndx Name
9 0: 00000308 0 FUNC GLOBAL DEFAULT 9 _init
7 0: 0000160c 0 NOTYPE GLOBAL DEFAULT ABS _edata
4 0: 00000000 613 FUNC GLOBAL DEFAULT UND sleep
2 0: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
1 0: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
10 0: 0000044c 57 FUNC GLOBAL DEFAULT 11 foobar
6 1: 00001614 0 NOTYPE GLOBAL DEFAULT ABS _end
11 1: 000004c8 0 FUNC GLOBAL DEFAULT 12 _fini
5 2: 00000000 267 FUNC WEAK DEFAULT UND __cxa_finalize
8 2: 0000160c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
3 2: 00000000 54 FUNC GLOBAL DEFAULT UND printf
Symbol table of `.gnu.hash' for image:
Num Buc: Value Size Type Bind Vis Ndx Name
6 0: 00001614 0 NOTYPE GLOBAL DEFAULT ABS _end
7 0: 0000160c 0 NOTYPE GLOBAL DEFAULT ABS _edata
8 1: 0000160c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
9 1: 00000308 0 FUNC GLOBAL DEFAULT 9 _init
10 2: 0000044c 57 FUNC GLOBAL DEFAULT 11 foobar
11 2: 000004c8 0 FUNC GLOBAL DEFAULT 12 _fini
动态链接的文件中,也有类似静态链接的重定位表,分别叫做“.rel.dyn”和“.rel.plt”,他们分别相当于“.rel.text”和“.rel.data”。
查看动态链接文件的重定位表:
horst@debian:~/horstdemo/simpleDynamic$ readelf -r ./mylib.so
Relocation section '.rel.dyn' at offset 0x2c8 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
00001608 00000008 R_386_RELATIVE
000015e0 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
000015e4 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
000015e8 00000506 R_386_GLOB_DAT 00000000 __cxa_finalize
Relocation section '.rel.plt' at offset 0x2e8 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
000015f8 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
000015fc 00000307 R_386_JUMP_SLOT 00000000 printf
00001600 00000407 R_386_JUMP_SLOT 00000000 sleep
00001604 00000507 R_386_JUMP_SLOT 00000000 __cxa_finalize
动态链接的步骤和实现:
动态链接的步骤基本分为三步:先启动动态连接器本身,然后装载所有需要的共享对象,最后是重定位和初始化。
但是对于动态链接器本身来说,它的重定位工作是由谁来完成的?
动态连接器本身通过自举(Bootstrap)来完成。
完成基本自举之后,动态连接器将可执行文件和连接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table)。
动态连接器按照各个模块之间的依赖关系,当有两个不同的模块定义了同一个符号时怎么办?
当一个符号需要被加入全局符号表时,如果相同的符号已经存在,则后加入的符号被忽略。
显示运行时链接:
动态库的装载则是通过一些列由动态连接器提供的API,具体是4个函数:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)、以及关闭动态库(dlclose)。
dlopen()函数用来打开动态库,并将其加载到进程的地址空间,完成初始化过程。
void *dlopen(const char *filename, int flag);
dlsym(),我们通过该函数找到所需要的符号。
dlsym(void *handle, char *symbol);