C/C++ 静态库与动态库
什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。
库文件常常按照特定格式来组织,在linux下,它是ELF格式(Executable Linkable Format,可执行可链接格式),而在windows下是PE(Portable Executable,可移植可执行)。
而通常目标文件有三种形式:
- 可执行目标文件:即我们通常所认识的,可直接运行的二进制文件。
- 可重定位目标文件:包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件。
- 共享目标文件:它是一种在加载或者运行时进行链接的特殊可重定位目标文件。
程序编译成可执行程序的步骤:
简单实例
编写.c文件
//main.c
#include<stdio.h>
#include<math.h>
int main(int argc,char *argv[])
{
printf("hello 编程珠玑\n");
int b = 2;
double a = exp(b);
printf("%lf\n",a);
return 0;
}
代码计算e的2次方并打印结果。由于代码中用到了exp函数,它位于数学库libm.so或者libm.a中,因此编译时需要加上-lm。
生成可重定位目标文件main.o:
$ gcc -c main.c #生成可重定位目标文件
$ readelf -h main.o #查看elf文件头部信息
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
(省略其他内容)
通过上面的命令将main.c生成为可重定位目标文件。通过readelf命令也可以看出来:REL (Relocatable file)。
观察共享目标文件libm.so:
$ readelf -h /lib/x86_64-linux-gnu/libm.so.6
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: DYN (Shared object file)
(省略其他内容)
查看可执行目标文件main:
$ gcc -o main main.o -lm #编译成最终的可执行文件
$ readelf -h main #查看ELF文件头
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
(省略其他内容)
Note: 如果使用到的函数没有在libc库中,那么你就需要指定要链接的库,本文中需要链接libm.so或libm.a。可以看到,最终生成的main类型是Executable file,即可执行目标文件。
静态库
之所以称为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态库特点总结:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
静态链接构建我们的可执行文件:
$ gcc -c main.c
$ gcc -static -o main main.o -lm
在这个过程中,就会用到系统中的静态库libm.a。这个过程做了什么呢?首先第一条命令会将main.c编译成可重定位目标文件main.o,第二条命令的static参数,告诉链接器应该使用静态链接,-lm参数表明链接libm.a这个库(类似的,如果要链接libxxx.a,使用-lxxx即可)。由于main.c中使用了libm.a中的exp函数,因此链接时,会将libm.a中需要的代码“拷贝”到最终的可执行文件main中。
Note: 必须把-lm放在后面。
放在最后时它是这样的一个解析过程:
- 链接器从左往右扫描可重定位目标文件和静态库
- 扫描main.o时,发现一个未解析的符号exp,记住这个未解析的符号
- 扫描libm.a,找到了前面未解析的符号,因此提取相关代码
- 最终没有任何未解析的符号,编译链接完成
如果将-lm放在前面
- 链接器从左往右扫描可重定位目标文件和静态库
- 扫描libm.a,由于前面没有任何未解析的符号,因此不会提取任何代码
- 扫描main.o,发现未解析的符号exp
- 扫描结束,还有一个未解析的符号,因此编译链接报错
如果把-lm放在前面,编译结果如下:
$ gcc -static -lm -o main main.o
main.o: In function `main':
main.c:(.text+0x2f): undefined reference to `exp'
collect2: error: ld returned 1 exit status
动态库
为什么需要动态库,其实也是静态库的特点导致。
- 空间浪费是静态库的一个问题。
- 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
-
动态库把对一些库函数的链接载入推迟到程序运行的时期。
-
可以实现进程之间的资源共享。(因此动态库也称为共享库)
-
将一些程序升级变得简单。
-
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
通常我们编译的程序默认就是实用动态链接:
$ gcc -o main main.c -lm #默认使用的是动态链接
通过ldd命令来观察可执行文件链接了哪些动态库:
$ ldd main
linux-vdso.so.1 => (0x00007ffc7b5a2000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fe9642bf000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe963ef5000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe9645c8000)
Reference
https://blog.nowcoder.net/n/8e07e78a703c413c916d0f830b8ceda7
https://zhuanlan.zhihu.com/p/71372182