深入理解静态链接库和动态链接库

为什么要使用链接库?大概有如下几个原因:1.利用前人为我们写好的库,比如数学库libm.so,免去再造轮子的困扰。2.充分使得程序的模块化,方便开发和后期升级。 3. 减小可执行文件的体积。链接库分为静态链接库、动态链接库。动态链接库还有不同的使用形式。那么他们的区别是什么?在什么情况下使用?编程时需要考虑那些方面呢?

原文:小宇的博客

静态库 static linking library

静态库一般命名为libxxx.a,其中xxx是库的名字。利用静态库方式编译生成的可执行程序的体积一般比较大一些。因为整个静态库的内容都会被链接到代码中。由此,我们可以发现他的优点,即编译后的科执行程序不依赖任何外部的库文件。这点可以很好的保证程序的可部署性,因为不用再考虑库的兼容性和依赖性。其缺点也是显而易见,即对库做的修改,必须重新编译整个可执行程序。升级也需要替换整个可执行程序。对于一些不间断运行程序,比如数据库软件,升级后需要重启。
无论静态库还是动态库,都是利用gcc生成的。gcc经过预处理编译链接三个步骤生成可执行文件,静态库的生成只需要前两步,gcc的-c选项就是让gcc只做预处理编译两步。通过这两步由libmy.c生成了libmy.o

$ gcc -c libmy.c

接下来就是用ar命令把一个或几个.o文件放到一个.a文件中。
-c:创建一个a文件
-r:加入新的o文件时候,如果重名,就采取替换的方法
-s:加入一个o文件的索引
-v:输出详细的过程

$ ar -crsv libmy.a libmy.o

下一步就是先编译main.c文件,然后把生成的main.o文件和libmy.a库文件链接到一起。

$ gcc -c main.c
$ gcc main.o -o main -L. -lmy

把上述过程写成Makefile文件

$ cat Makefile 
all:main

main: main.o libmy.a
    gcc main.o -o main -L. -lmy
libmy.a: libmy.o
    ar -crsv libmy.a libmy.o
libmy.o: libmy.c
    gcc -c libmy.c
clean:
    -rm *.o *.a

下面我们看看nm生成的可执行程序的内容。

$ nm main | grep add
0000000000400551 T add

add函数别标记为T:该符号在text section文本段中,说明该函数的内容已经在可执行程序中了。
接下来用objdump来看一下链接库中的add函数内容:

$ objdump -S main
0000000000400551 :
  400551:       55                      push   %rbp
  400552:       48 89 e5                mov    %rsp,%rbp
  400555:       89 7d fc                mov    %edi,-0x4(%rbp)
  400558:       89 75 f8                mov    %esi,-0x8(%rbp)
  40055b:       8b 55 fc                mov    -0x4(%rbp),%edx
  40055e:       8b 45 f8                mov    -0x8(%rbp),%eax
  400561:       01 d0                   add    %edx,%eax
  400563:       5d                      pop    %rbp
  400564:       c3                      retq   
  400565:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40056c:       00 00 00 
  40056f:       90                      nop

链接库libmy.a已经完全到可执行程序main中,运行时不需要再依赖libmy.a了。add的地址是400551函数的实际地址,运行时add的地址就是这个。下面我们用gdb调试一把:
深入理解静态链接库和动态链接库_第1张图片

动态链接库 dynamic linking library

和静态库不同,正如其名字所说,动态库在编译的时候并没有编译到目标代码中,而是在程序执行的时候才打开库文件并载入库文件中的相应函数。因此,该种方法生成的可执行程序比较小。同时,在程序有bug时,定位到某个库文件,只需要替换相应的so文件就可以了。不需要替换整个科执行程序。但是缺点也是显而易见,在程序部署的时候必须确保环境的库文件版本正确,否则程序无法运行。
在编译动态库时,需要加上如下参数:

-fPIC:生成位置无关代码(position-independent code)。在PIC代码中,所有的地址是通过一个global offset table(GOT)的偏移量访问的。这种特性正好服务于动态库,因为其载入的地址不是确定的。
-shared:生成共享目标文件,支持动态连接。

$ gcc -fPIC -shared -o libmy.so libmy.c

动态链接库的加载方式有2中,即在编译时指定和运行时指定。

编译时指定

这种方式,是在gcc编译时指定-lxxx库文件。程序在运行时,查找系统中的相关库文件,并由操作系统加载想干的库文件。这种方式不用在编码过程中做出特殊处理,在编程方面和静态库类似。

$ gcc main.o -o main -L. -lmy

把上述内容写成Makefile文件

$ cat Makefile
all:main

main: main.c libmy.so
    gcc main.c -o main -L. -lmy
libmy.so: libmy.c
    gcc -fPIC -shared -o libmy.so libmy.c
clean:
    -rm *.o *.so main

如果操作系统在加载动态库的过程中,没有找到相关文件,那么会报错。

$ ./main 
./main: error while loading shared libraries: libmy.so: cannot open shared object file: No such file or directory

这时候需要你把库文件放到正确的位置,比如/usr/lib下面。或者指定LD_LIBRARY_PATH环境变量。之后,用ldd命令查看是否找到了库文件,以及库文件的路径是否正确。

$ ldd ./main
    linux-vdso.so.1 =>  (0x00007ffe2db85000)
    libmy.so => ./libmy.so (0x00007f3d5a6a8000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3d5a2b8000)
    /lib64/ld-linux-x86-64.so.2 (0x0000555f008f0000)

下面我们用nm来看一下main的内容。

$ nm main | grep add
                 U add

add函数被标记为U,即Undefined未定义。说明main可执行文件中没有add的二进制代码。那么在运行时如何找到add的地址呢?我们分析一下汇编语言:

00000000004006c6 
: 4006c6: 55 push %rbp 4006c7: 48 89 e5 mov %rsp,%rbp 4006ca: be 01 00 00 00 mov $0x1,%esi 4006cf: bf 01 00 00 00 mov $0x1,%edi 4006d4: e8 b7 fe ff ff callq 400590 @plt> 4006d9: 89 c6 mov %eax,%esi 4006db: bf 84 07 40 00 mov $0x400784,%edi 4006e0: b8 00 00 00 00 mov $0x0,%eax 4006e5: e8 b6 fe ff ff callq 4005a0 <printf@plt> 4006ea: b8 00 00 00 00 mov $0x0,%eax 4006ef: 5d pop %rbp 4006f0: c3 retq 4006f1: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 4006f8: 00 00 00 4006fb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

看到程序在准备好参数之后执行了callq 400590

0000000000400590 @plt>:
  400590:   ff 25 82 0a 20 00       jmpq   *0x200a82(%rip)      
  400596:   68 00 00 00 00          pushq  $0x0
  40059b:   e9 e0 ff ff ff          jmpq   400580 <_init+0x20>

接下来实际上是调用libc_dl_runtime_resolve_sse把函数add的地址解析出来并保存到plt中。下次运行到这里直接可以jmpq到add函数的地址,不用再次解析地址了。关于这个过程之后再开贴讨论。
可以看出来,动态链接库的地址一般都很大。

运行时指定

这种方法是最复杂的也是最灵活的。因为动态链接库是在程序运行之后才加载的。这样能够在程序不中断的情况下替换动态链接库。这个特性非常适合数据库等需要长时间运行的软件。升级的时候,不需要停机,只需替换动态链接库文件,然后让程序再次载入即可。同时因为是在运行时指定动态链接库的位置,需要在程序编码中处理动态链接库的加载。
在编码过程中需要加上dlfcn.h头文件,该文件提供了一系列操作动态链接库的函数,比如:

dlopen:打开.so库文件,并返回一个handle用于以下函数
dlsym:在库文件中根据函数名找到函数地址并返回
dlclose:关闭库文件
我们就用这几个函数来完成动态链接库的加载。在编译的时候需要在gcc后面加上-ldl已加载libdl.so库。

#include
#include
#include /* dlopen dlsym dlclose dlerror */

/* import from libmy.so */
#define LIBMY "./libmy.so"
typedef int (*add_fp)(int a, int b);


int main()
{
    void *handle = NULL;
    add_fp add = NULL;
    char *error = NULL;

    handle = dlopen(LIBMY, RTLD_LAZY);

    if (handle == NULL)
    {
        printf("erro opening %s\n", LIBMY);
        exit(1);
    }

    dlerror(); /* clear any existing error */

    add = (add_fp)dlsym(handle,"add");

    error = dlerror();
    if (error != NULL)
    {
        printf("error dlsym: %s\n", error);
        exit(1);
    }
    printf("%d\n",(*add)(1,1));
    dlclose(handle);

    exit(0);
}

把上述内容写成Makefile文件

$ cat Makefile 
all:main

main: main.c
    gcc main.c -ldl -o main
clean:
    -rm main

该方法,ldd是看不到需要那些库文件的。如果需要的库文件不存在,程序在运行时才会报错。

$ ldd ./main
    linux-vdso.so.1 =>  (0x00007fffaebf1000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa182e3f000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa182a76000)
    /lib64/ld-linux-x86-64.so.2 (0x0000559bfc525000)

你可能感兴趣的:(linux,c语言)