一、动静态库的概念和原理
1. 认识动静态库
在Linux下创建文件编写以下代码,并生成可执行程序。
#include <stdio.h> int main() { printf("Hello Linux!\n"); //库函数 return 0; }
这是一段最简单的程序,结果就是打印" Hello Linux! " 到屏幕上。
我们可以通过以下操作来简单认识一下动静态库。
在这份代码当中我们可以通过调用printf输出hello world,主要原因是gcc编译器在生成可执行程序时,将C标准库也链接进来了。在Linux下,我们可以通过ldd 文件名来查看一个可执行程序所依赖的库文件。
这里的libc.so.6
其实就是该程序执行所依赖的库文件,我们可以通过ls命令发现libc.so.6
实际上是一个软链接。
该软链接指向的是文件libc-2.17.so
,其实它们在同一个目录lib64
下面。我们可以通过file指令查看该文件。
我们发现libc-2.17.so
其实是一个共享目标文件库,也就是我们所说的动态库。
注意:
- 在Linux中,动态库以
.so
为文件后缀,静态库以.a
为文件后缀。 - 在Windows中,动态库以
.dll
为文件后缀,静态库以.lib
为文件后缀。
这里可执行程序所依赖的libc.so.6实际上就是C语言动态库,当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。而gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static选项。
gcc -o hello-s hello.c -static
此时生成的可执行程序就是静态链接的了,可以明显发现静态链接生成的可执行程序的文件大小,比动态链接生成的可执行程序的文件大小要大得多。
静态链接生成的可执行程序并不依赖其他库文件,此时当我们使用ldd 文件名
命令查看该可执行程序所依赖的库文件时就会看到以下信息。
此外,当我们分别查看静态链接生成的可执行程序的文件类型时,也可以看到它是静态链接的。
2. 动静态库的概念
- 静态库
之所以称为静态库,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
静态库特点:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
- 动态库
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布带来麻烦。
动态库特点:
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
3. 动静态库的原理
动静态库的本质其实就是一堆.o文件,只需要将其链接起来就能生成可执行程序。
我们知道,源文件和头文件最终要变成可执行程序都要经过以下四个步骤:
- 预处理:完成头文件的展开、去掉注释、宏替换、条件编译等,最终形成
xxx.i
文件; - 编译:完成词法、语法、语义分析、符号汇总等。检查无误后将代码翻译成汇编指令;
- 汇编:将汇编指令转换成二进制指令,最终形成
xxx.o
文件; - 链接:将生成的各个
xxx.o
文件进行链接,最终形成可执行文件。
例如,一个项目中的test1.c、test2.c、test3.c、main.c经过一系列编译形成可.o文件test1.o、test2.o、test3.o以及main.o,最终在把它们链接起来形成可执行文件a1.out。
如果有另外一个或多个项目当中也需要使用到test1.c、test2.c、test3.c和main函数main2、main3,生成可执行程序的过程也和上面的一样。
而实际上,对于可能频繁用到的源文件,比如这里的test1.c、test2.c、test3.c,我们可以将它们的目标文件test1.o、test2.o、test3.o、进行打包,之后需要用到这几个目标文件时就可以之间链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。
实际上,所有库本质都是一堆目标文件xxx.o
的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的 “半成品” 。
二、动静态库的生成与打包
样例代码:
head1.h
#pragma once #include <stdio.h> #include <time.h> extern void print(const char* msg);
test1.c
#include "head1.h" void print(const char* msg) { printf("%s : %lld\n", msg, (long long)time(NULL)); }
head2.h
#pragma once #include <stdio.h> #include <assert.h> extern int addToVal(int from, int to);
test2.c
#include "head2.h" int addToVal(int from, int to) { assert(from <= to); int res = 0; int i = 0; for(i = from; i <= to; ++i) { res += i; } return res; }
1. 静态库的生成与打包
第一步使用gcc将文件进行编译生成
.o
文件。
gcc -c test1.c -o test1_s.o -std=c99 gcc -c test2.c -o test2_s.o -std=c99
第二步使用ar命令将所有目标文件打包生成静态库
ar
命令是gnu的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包。
-r
(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。-c
(create):建立静态库文件。
此外,我们可以用ar
命令的-t
选项和-v
选项查看静态库当中的文件。
-t
:列出静态库中的文件。-v
(verbose):显示详细的信息。
第三步将头文件和生成的静态库组织起来
当我们把自己的库给别人用的时候,实际上需要给别人两个文件夹,一个文件夹下面放的是一堆头文件的集合,另一个文件夹下面放的是所有的库文件。因此,在这里我们可以将head1.h和head2.h这两个头文件放到一个名为include的目录下,将生成的静态库文件libmylib.a放到一个名为lib的目录下,然后将这两个目录都放到lib-static下,此时就可以将lib-static给别人使用了。
[lhf@hecs-197241 test_lib]$ mkdir -p lib-static/lib [lhf@hecs-197241 test_lib]$ mkdir -p lib-static/include [lhf@hecs-197241 test_lib]$ cp *.a lib-static/lib [lhf@hecs-197241 test_lib]$ cp *.h lib-static/include
使用Makefile
实际上我们可以将上述所要执行的命令全部写到Makefile当中,当我们以后要生成静态库以及组织头文件和库文件时就可以一步到位,不用每次重新生成的时候都要敲这么多命令,同时也体现了Makefile的实用性。
Makefile:
#生成静态库 libmylib.a:test1_s.o test2_s.o ar -rc libmylib.a test1_s.o test2_s.o test1_s.o:test1.c gcc -c test1.c -o test1_s.o -std=c99 test2_s.o:test2.c gcc -c test2.c -o test2_s.o -std=c99 .PHONY:lib lib: mkdir -p lib-static/lib mkdir -p lib-static/include cp *.a lib-static/lib cp *.h lib-static/include .PHONY:clean clean: rm -rf *.o *.a *.so lib-static
编写Makefile后,只需一个make就能生成所有源文件对应的目标文件进而生成静态库;然后一个make lib就能将头文件和静态库组织起来。
2. 动态库的生成与打包
第一步:让所有源文件生成对应的目标文件
注意要生成静态库,此时编译生成目标文件时要带 -fPIC选项
-fPIC
(position independent code):产生位置无关码。
gcc -fPIC -c test1.c -o test1.o -std=c99 gcc -fPIC -c test2.c -o test2.o -std=c99
另外:
- -fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
- 如果不加-fPIC选项,则加载.so文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so文件代码段和数据段内存映射的位置。
- 不加-fPIC编译出来的.so是要在加载时根据加载到的位置再次重定位的,因为它里面的代码BBS位置无关代码。如果该.so文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码副本(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。
- 我们总是用-fPIC来生成.so,但从来不用-fPIC来生成.a。但是.so一样可以不用-fPIC选项进行编译,只是这样的.so必须要在加载到用户程序的地址空间时重定向所有表目。
第二步:使用gcc 的 -shared
选项将所有目标文件打包为动态库
gcc -shared -o libmylib.so test1.o test2.o
第三步:将头文件和生成的动态库组织起来
和上面组织的静态库的方法一样,这里就直接使用Makefile。
#生成动态库 libmylib.so:test1.o test2.o gcc -shared -o libmylib.so test1.o test2.o test1.o:test1.c gcc -fPIC -c test1.c -o test1.o -std=c99 test2.o:test2.c gcc -fPIC -c test2.c -o test2.o -std=c99 .PHONY:lib lib: mkdir -p lib-dyl/lib mkdir -p lib-dyl/include cp *.so lib-dyl/lib cp *.h lib-dyl/include .PHONY:clean clean: rm -rf *.o *.so lib-dyl
三、动静态库的使用
在另外一个test目录下准备样例代码test.c
:
#include "head1.h" #include "head2.h" int main() { int start = 0; int end = 100; int result = addToVal(start, end); printf("result: %d\n", result); print("hello world"); return 0; }
拷贝动态库和静态库到test目录下:
cp lib-dyl/ -rf test cp lib-static/ -rf test
1. 静态库的使用
方法一:使用gcc选项
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
: 指定头文件的路径;-L
: 指定库文件的路径;-l
: 需要指明链接库文件路径下的哪一个库。
编译test.c
的指令:
gcc test.c -I./lib-static/include -L./lib-static/lib -lmylib
【注意】
- 编译器不知道test.c所包含的头文件在哪里,所以需要指定头文件的搜索路径。
- 头文件当中只有函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。
- 实际上,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
方法二:把头文件和库文件拷贝到系统库的路径下
我们用C语言编写程序所用到库和头文件其实存在于系统库的路径下,当程序需要时,它会自动到系统库路径下去寻找,我们也可以将自己写的库保存到系统库的路径之下。
sudo cp lib-static/include/* /usr/include/ sudo cp lib-static/lib/* /lib64
需要注意的是,虽然已经将头文件和库文件拷贝到系统路径下,但当我们使用gcc编译test.c生成可执行程序时,还是需要指明需要链接库文件路径下的哪一个库。
gcc test.c -lmylib
为什么之前使用gcc编译C语言代码的时候没有指明过库名字?
因为gcc就是用来编译C程序的,所以gcc编译的时候默认就找的是C库,但此时我们要链接的是哪一个库编译器是不知道的,因此我们还是需要使用-l选项,指明需要链接库文件路径下的哪一个库。
通过上面的过程我们可以发现,实际上我们拷贝头文件和库文件到系统库路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染。
2. 动态库的使用
当我们同样使用-I
,-L
,-l
这三个选项使用动态库进行编译,生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd
命令进行查看。
运行程序:
我们发现可执行程序所依赖的动态库没有找到,所有动态库的使用和静态库的使用是有差别的。
方法一:拷贝库文件到系统共享库路径下
sudo cp lib-dyl/lib/* /lib64
此时我们发现程序就能运行起来了。
方法二:更改
LD_LIBRARY_PATH
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
此时用ldd
命令查看可执行程序发现其找到了所依赖的动态库。程序也就能正常运行了。
注意:当我们关闭shell或者重启Linux系统后,配置的环境变量也就消失了。
方法三:配置
/etc/ld.so.conf.d/
/etc/ld.so.conf.d/路径下存放的都是以.conf为后缀的配置文件,这些配置文件当中存放的都是各种路径,系统会自动查找/etc/ld.so.conf.d/路径下的配置文件,从而找到库的路径。我们可以把自己写的库文件也放的在/etc/ld.so.conf.d/的配置文件当中。
echo /home/lhf/linux_code/test_lib/lib-dyl/lib > my-lib.conf sudo cp my-lib.conf /etc/ld.so.conf.d/
可以看到my-lib.conf
在etc/ld.so.conf.d/
目录下:
此时我们使用ldd
指令查看可执行程序a.out
,会发现可执行程序并没有所依赖的动态库,那是因为配置文件没有更新。
使用ldconfig
指令更新配置文件,此时我们可以看到可执行程序所依赖的动态库:
程序便能正常运行了。
方法四:软连接
关于软连接可见博主的上一篇博客 【Linux学习】基础IO
在/lib64系统路径下创建libmylib.so的软连接:
sudo ln -s /home/lhf/linux_code/test_lib/lib-dyl/lib/libmylib.so /lib64/libmylib.so
此时我们用ldd
指令查看a.out
发现其依赖的文件正是我们在/lib64
路径下创建的软连接。
四、动静态库的优缺点
静态库
静态库是程序在编译链接的时候把库的代码复制到可执行文件当中的,生成的可执行程序在运行的时候将不再需要静态库,因此使用静态库生成的可执行程序的大小一般比较大。
优点:
- 使用静态库生成可执行程序后,该可执行程序就可以独自运行,不再需要库了。
缺点:
- 使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码。
动态库
动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
优点:
- 节省磁盘空间,且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存当中不会存在重复代码。
缺点: - 必须依赖动态库,否则无法运行。