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

时间:2022-12-18 09:42:05

为什么要使用链接库?大概有如下几个原因: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 <add>:
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调试一把:
深入理解静态链接库和动态链接库

动态链接库 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 <main>:
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 <add@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 <add@plt>

0000000000400590 <add@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<stdio.h>
#include<stdlib.h>
#include<dlfcn.h> /* 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)