动静态库
动静态库的基本原理
我们知道,生成一个可执行程序会经历以下四个步骤:
- 预处理:头文件展开,宏替换,去注释,条件编译,最终会生成
.i
文件; - 编译:用于词法分析,语法分析,语义分析,符号汇总,检查无误后将代码翻译成汇编代码,会生成
.s
文件; - 汇编:将汇编指令转化为二进制指令,会生成
.s
文件; - 链接:生成各个
.o
文件,链接起来,生成可执行程序;
比如下列程序:Add.c
,Sub.c
和main.c
需要形成可执行程序,需要先生成Add.o
,Sub.o
和main.o
文件,然后链接起来,生成可执行程序;
同样,我们在另外一个程序中也要用到这Add.c
,Sub.c
这两个文件,经过同样的操作就可以。
实际上,对于可能频繁用到的源文件,例如上述的Add.c
,Sub.c
,我们可以将它们的目标文件Add.o
,Sub.o
进行打包,之后需要用到这些目标文件时就可以之间链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。
我们可以理解为库本质都是一堆目标文件(xxx.o)的集合。
动静态库的认识
在Linux下创建test.c文件,编写如下代码:
cpp
1 #include <stdio.h>
2
3 int main()
4 {
5 printf("hello world\n");
6 return 0;
7 }
在这份代码当中我们可以通过调用printf输出hello world,主要原因是gcc编译器在生成可执行程序时,将C标准库也链接进来了。
我们使用 ldd 命令可以查看一个可执行程序依赖的库文件;
这其中的libc.so.6
就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6
实际上只是一个软链接;
实际上该软链接的源文件libc-2.17.so
和libc.so.6
在同一个目录下,为了进一步了解,我们可以通过file 文件名命令来查看libc-2.17.so
的文件类型。
我们需要了解,在Linux下:以.so
为后缀的是动态库,以.a
为后缀的是静态库。在windos下以.dll
为后缀的是动态库,以.lib
为后缀的是静态库。
上面的libc-2.17.so
实际上就是一个动态库,这里可执行程序所依赖的libc.so.6
实际上就是C动态库,当我们去掉一个动静态库的前缀lib
,再去掉后缀.so
或者.a
及其后面的版本号,剩下的就是这个库的名字。
gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static
选项。
此时生成的可执行程序就是静态链接的了,可以明显发现静态链接生成的可执行程序的文件大小,比动态链接生成的可执行程序的文件大小要大得多。
静态链接生成的可执行程序并不依赖其他库文件,此时当我们使用 ldd 文件名命令查看该可执行程序所依赖的库文件时就会看到以下信息。
动静态库特征
静态库
静态库是程序在编译链接的时候把库的代码复制到可执行文件当中的,生成的可执行程序在运行的时候将不再需要静态库,因此使用静态库生成的可执行程序的大小一般比较大。
优点:
- 使用静态库生成可执行程序后,该可执行程序就可以独自运行,不再需要库了。
缺点:
- 使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码。
动态库
动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
优点:
- 节省磁盘空间,且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存当中不会存在重复代码。
缺点: - 必须依赖动态库,否则无法运行。
静态库的打包与使用
我们以两个源文件add.c
和sub.c
,两个头文件add.h
和sub.h
来实现:
add.h文件内容:
cpp
1 #pragma once
2
3 extern int Add(int a, int b);
add.c文件内容:
cpp
1 #include "add.h"
2
3 int Add(int a, int b)
4 {
5 return a + b;
6 }
sub.h文件内容:
cpp
1 #pragma once
2
3 int Sub(int a, int b);
sub.c文件内容:
cpp
1 #include "sub.h"
2
3 int Sub(int a, int b)
4 {
5 return a - b;
6 }
第一步:让所有
.c
文件生成.o
目标文件;
第二步:使用ar命令将所有目标文件打包为静态库;
ar
命令是gnu的归档工具,常用于将目标文件打包为静态库,下面我们使用ar
命令的-r
选项和-c
选项进行打包。
-r
(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。-c
(create):建立静态库文件。
此外,我们可以用ar命令的-t选项和-v选项查看静态库当中的文件。
-t
:列出静态库中的文件。-v
(verbose):显示详细的信息。
第三步:将头文件和生成的静态库组织起来;
当我们把自己的库给别人用的时候,实际上需要给别人两个文件夹,一个文件夹下面放的是一堆头文件的集合,另一个文件夹下面放的是所有的库文件。
因此,在这里我们可以将add.h
和sub.h
这两个头文件放到一个名为include的目录下,将生成的静态库文件libcal.a
放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mathlib给别人使用了。
创建makefile
创建makefile以后使我们的操作更加便捷:
接下来我们创建一个main.c文件:
cpp
1 #include <stdio.h>
2 #include <add.h>
3
4 int main()
5 {
6 int a = 10;
7 int b = 20;
8 int ret = Add(a, b);
9 printf("%d + %d = %d\n", a, b, ret);
10 return 0;
11 }
现在该目录下就只有main.c和我们刚才打包好的静态库。
方法一:使用选项
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
说明一下:- 因为编译器不知道你所包含的头文件
add.h
在哪里,所以需要指定头文件的搜索路径。 - 因为头文件
add.h
当中只有Add
函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。 - 实际中,在库文件的
lib
目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。 -I
,-L
,-l
这三个选项后面可以加空格,也可以不加空格。
方法二:把头文件和库文件拷贝到系统路径下
既然编译器找不到我们的头文件和库文件,那么我们直接将头文件和库文件拷贝到系统路径下不就行了。
虽然已经将头文件和库文件拷贝到系统路径下,但当我们使用gcc编译main.c生成可执行程序时,还是需要指明需要链接库文件路径下的哪一个库。
我们使用gcc编译的是C语言,而gcc就是用来编译C程序的,所以gcc编译的时候默认就找的是C库,但此时我们要链接的是哪一个库编译器是不知道的,因此我们还是需要使用-l
选项,指明需要链接库文件路径下的哪一个库。
扩展:
实际上我们拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染。
动态库的打包与使用
动态库的打包相对于静态库来说有一点点差别,但大致相同:
第一步:让所有源文件生成对应的目标文件
此时用源文件生成目标文件时需要携带-fPIC
选项:
-fPIC
(position independent code):产生位置无关码。
-fPIC
作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。- 如果不加
-fPIC
选项,则加载.so
文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so
文件代码段的进程在内核里都会生成这个.so
文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so
文件代码段和数据段内存映射的位置。 - 不加
-fPIC
编译出来的.so
是要在加载时根据加载到的位置再次重定位的,因为它里面的代码BBS位置无关代码。如果该.so
文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so
的代码副本(因为.so
被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。 - 我们总是用
-fPIC
来生成.so
,但从来不用-fPIC
来生成.a
。但是.so
一样可以不用-fPIC
选项进行编译,只是这样的.so
必须要在加载到用户程序的地址空间时重定向所有表目。
第二步:使用-shared选项将所有目标文件打包为动态库
与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。
第三步:将头文件和生成的动态库组织起来
与生成静态库时一样,为了方便别人使用,在这里我们可以将add.h和sub.h这两个头文件放到一个名为include的目录下,将生成的动态库文件libcal.so放到一个名为lib的目录下,然后将这两个目录都放到mlib下,此时就可以将mlib给别人使用了。
创建makefile
我们依然使用main.c来进行动态库的演示:
使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I,-L,-l这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l选项指明需要链接的库名字来生成可执行程序;
此时使用gcc编译main.c生成可执行程序时,需要用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明需要链接库文件路径下的哪一个库。
此时我们会发现,生成的可执行程序a.out
无法执行,我们要注意的是:我们使用-I
,-L
,-l
这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd命令进行查看。
我们如何去解决这个问题呢?
方法一:拷贝.so文件到系统共享库路径下
方法二:更改
LD_LIBRARY_PATH
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
方法三:配置
/etc/ld.so.conf.d/
我们可以通过配置/etc/ld.so.conf.d/的方式解决该问题,/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。
此时我们发现系统还是没有找到该可执行程序所依赖的动态库,我们需要使用ldconfig命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了。