程序员的自我修养——第七章——动态链接
静态链接浪费内存和磁盘空间、模块更新困难等问题,因此寻找一种更好的办法来组织程序模块。
静态链接对程序的更新、部署和发布也会带来很多麻烦。
动态链接:
就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。
动态链接的方式使得开发过程中各个模块更加独立、耦合度更小,便于不同的开发者和开发组织之间进行独立的开发和测试。
动态链接还有一个特点就是程序在运行时可以动态的选择加载各种程序模块,使得插件成为可能。
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
root@ubuntu:~/Desktop/ezCode# gcc -fPIC -shared -o Lib.so Lib.c
将Lib.c 编译成为一个共享对象文件,‘-shared’表示产生共享对象
-fPIC(Position Independent Code)表示使用地址无关代码技术来产生输出文件
root@ubuntu:~/Desktop/ezCode# gcc -o Program1 Program1.c ./Lib.so
root@ubuntu:~/Desktop/ezCode# gcc -o Program2 Program2.c ./Lib.so
利用 ./Lib.so分别进行编译链接。
运行结果:
root@ubuntu:~/Desktop/ezCode# ./Program2
Printing from Lib.so 1
root@ubuntu:~/Desktop/ezCode# ./Program2
Printing from Lib.so 2
动态链接过程:
查看进程的虚拟地址空间:
可以看到有Lib.so映射文件。
root@ubuntu:~/Desktop/ezCode# ./Program1 &
[1] 4653
root@ubuntu:~/Desktop/ezCode# Printing from Lib.so 1
cat /proc/4653/maps
08048000-08049000 r-xp 00000000 08:01 209281 /root/Desktop/ezCode/Program1
08049000-0804a000 r--p 00000000 08:01 209281 /root/Desktop/ezCode/Program1
0804a000-0804b000 rw-p 00001000 08:01 209281 /root/Desktop/ezCode/Program1
b7678000-b767a000 rw-p 00000000 00:00 0
b767a000-b77d1000 r-xp 00000000 08:01 389519 /lib/libc-2.12.1.so
b77d1000-b77d2000 ---p 00157000 08:01 389519 /lib/libc-2.12.1.so
b77d2000-b77d4000 r--p 00157000 08:01 389519 /lib/libc-2.12.1.so
b77d4000-b77d5000 rw-p 00159000 08:01 389519 /lib/libc-2.12.1.so
b77d5000-b77d8000 rw-p 00000000 00:00 0
b77e4000-b77e5000 rw-p 00000000 00:00 0
b77e5000-b77e6000 r-xp 00000000 08:01 209280 /root/Desktop/ezCode/Lib.so
b77e6000-b77e7000 r--p 00000000 08:01 209280 /root/Desktop/ezCode/Lib.so
b77e7000-b77e8000 rw-p 00001000 08:01 209280 /root/Desktop/ezCode/Lib.so
b77e8000-b77ea000 rw-p 00000000 00:00 0
b77ea000-b77eb000 r-xp 00000000 00:00 0 [vdso]
b77eb000-b7807000 r-xp 00000000 08:01 389495 /lib/ld-2.12.1.so
b7807000-b7808000 r--p 0001b000 08:01 389495 /lib/ld-2.12.1.so
b7808000-b7809000 rw-p 0001c000 08:01 389495 /lib/ld-2.12.1.so
bfb39000-bfb4e000 rw-p 00000000 00:00 0 [stack]
root@ubuntu:~/Desktop/ezCode# readelf -l Lib.so
Elf file type is DYN (Shared object file)
Entry point 0x3b0
There are 6 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x00524 0x00524 R E 0x1000
LOAD 0x000f14 0x00001f14 0x00001f14 0x00100 0x00108 RW 0x1000
DYNAMIC 0x000f28 0x00001f28 0x00001f28 0x000c0 0x000c0 RW 0x4
NOTE 0x0000f4 0x000000f4 0x000000f4 0x00024 0x00024 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x000f14 0x00001f14 0x00001f14 0x000ec 0x000ec R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
01 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04
05 .ctors .dtors .jcr .dynamic .got
链接时重定位(静态链接);装载时重定位(动态链接)
地址无关代码: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来查看:
root@ubuntu:~/Desktop/ezCode# objdump -s Program1
Program1: file format elf32-i386
Contents of section .interp:
8048134 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
8048144 2e3200 .2.
该段保存了需要的动态链接器需路径,一般是/lib/ld-linux.so.2
也可以使用如下命令来查看一个可执行文件所需呀的动态连接器的路径:
root@ubuntu:~/Desktop/ezCode# readelf -l Program1 | grep interpreter
[Requesting program interpreter: /lib/ld-linux.so.2]
动态链接ELF中最重要的结构应该是“.dynamic”段,这个段里面保存了动态连接器所需要的基本信息,比如依赖哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
使用readelf工具查看“.dynamic”段的内容:
root@ubuntu:~/Desktop/ezCode# readelf -d Program1
Dynamic section at offset 0xf20 contains 21 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [./Lib.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x804835c
查看一个程序主模块或一个共享库依赖了哪些共享库:
root@ubuntu:~/Desktop/ezCode# ldd Program1
linux-gate.so.1 => (0x00c07000)
./Lib.so (0x008b2000)
libc.so.6 => /lib/libc.so.6 (0x0053a000)
/lib/ld-linux.so.2 (0x00f2d000)
Lib.so中的foobar函数称为导出函数
为了表示动态链接这些模块之间的符号导出与导入关系,ELF专门有一个叫做动态符号表的段用来保存这些信息。这个段叫做“.dynsym”。
可以使用如下命令来查看动态符号表:
root@ubuntu:~/Desktop/ezCode# readelf -sD Lib.so
Symbol table of `.gnu.hash' for image:
Num Buc: Value Size Type Bind Vis Ndx Name
6 0: 0000201c 0 NOTYPE GLOBAL DEFAULT ABS _end
7 0: 00002014 0 NOTYPE GLOBAL DEFAULT ABS _edata
8 1: 00002014 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
9 1: 00000328 0 FUNC GLOBAL DEFAULT 9 _init
10 2: 0000046c 57 FUNC GLOBAL DEFAULT 11 foobar
11 2: 000004e8 0 FUNC GLOBAL DEFAULT 12 _fini
动态链接的文件中,也有类似静态链接的重定位表,分别叫做“.rel.dyn”和“.rel.plt”,他们分别相当于“.rel.text”和“.rel.data”。
查看动态链接文件的重定位表:
root@ubuntu:~/Desktop/ezCode# readelf -r Lib.so
Relocation section '.rel.dyn' at offset 0x2e8 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
00002010 00000008 R_386_RELATIVE
00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
00001ff0 00000506 R_386_GLOB_DAT 00000000 __cxa_finalize
Relocation section '.rel.plt' at offset 0x308 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
00002004 00000307 R_386_JUMP_SLOT 00000000 printf
00002008 00000407 R_386_JUMP_SLOT 00000000 sleep
0000200c 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);
这段程序用运行时加载的方式将数学模块加载到进程中,然后获取sin()函数符号地址,调用sin()并且返回结果。
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char * argv[])
{
void *handle;
double (*func)(double);
char * error;
handle = dlopen(argv[1],RTLD_NOW);
if(handle == NULL)
{
printf("Open library %s error: %s\n",argv[1],dlerror());
return -1;
}
func = dlsym(handle,"sin");
if( (error = dlerror() ) != NULL)
{
printf("Symbol sin not found:error = %s\n",error);
goto exit_runso;
}
printf( "%f\n",func(3.1415926 / 2));
exit_runso:
dlclose(handle);
}
root@ubuntu:~/Desktop/ezCode# gcc -o RunSoSimple RunSoSimple.c -ldl
root@ubuntu:~/Desktop/ezCode# ./RunSoSimple /lib/libm-2.12.1.so
1.000000