库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以操作系统载入内存执行。库有两种:静态库(.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放在后面。
放在最后时它是这样的一个解析过程:
如果将-lm放在前面
如果把-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
为什么需要动态库,其实也是静态库的特点导致。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
通常我们编译的程序默认就是实用动态链接:
$ 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)
https://blog.nowcoder.net/n/8e07e78a703c413c916d0f830b8ceda7
https://zhuanlan.zhihu.com/p/71372182