程序员的自我修养——第七章——动态链接

        静态链接浪费内存和磁盘空间、模块更新困难等问题,因此寻找一种更好的办法来组织程序模块。

   静态链接对程序的更新、部署和发布也会带来很多麻烦。

 

动态链接:

      就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。

 

动态链接的方式使得开发过程中各个模块更加独立、耦合度更小,便于不同的开发者和开发组织之间进行独立的开发和测试。

动态链接还有一个特点就是程序在运行时可以动态的选择加载各种程序模块,使得插件成为可能。

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

 

posted @ 2012-05-06 22:51  KingsLanding  阅读(2305)  评论(2编辑  收藏  举报