动/静态库是什么?
简单来说,二者是一个装了许多.o文件的仓库
.o文件是什么?
一个源文件要最终形成一个可执行程序,要经过以下4步:
- 1.预处理--完成头文件展开、去注释、宏替换、条件编译,形成.i文件
- 2.编译--完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,形成.s文件
- 3.汇编--将汇编指令转换成二进制指令,形成.o文件
- 4.链接--将各个.o文件链接起来,形成一个可执行程序
以gcc编译器为例,查看xxx.i xxx.s xxx.o文件
源文件:(test.c)
生成test.i文件(预处理)
gcc -E test.c -o test.i
- -E:只执行预处理动作
- -o:生成.i文件(-o用于生成-o后指定的文件 必须得加)
test.i文件内容
- 头文件被展开了
- 注释被删掉了
- 宏被替换了
生成test.s文件(编译)
gcc -S test.i -o test.s
- -S:执行到编译动作就停止
- -o:生成.s文件
test.s文件内容:
可以看到,c代码已经被转换成汇编代码了
生成test.o文件(汇编)
gcc -c test.s -o test.o
- -c(注意是小写):编译器执行到汇编就停止
- -o:生成.o文件
文件内容:
此时.o文件内存储的是二进制代码,但无法被识别
生成可执行程序(链接):
gcc test.o -o test
什么选项都不加代表着直接生成可执行程序
链接就是将所有的.o文件链接到一起,形成可执行程序,换言之,可执行程序是(>=1)的.o文件的集合
小结:
- gcc编译选项后的ESc分别对应着预处理(生成.i文件)、编译(生成.s文件)、汇编(生成.o文件)
- 不加选项意味着生成可执行程序,记得生成对应的文件时要加-o。
- 注意:其实xxx.i xxx.s xxx.o这些文件后缀无关紧要,重要的是它里面的文件内容,如果你乐意,你也可以将预处理后生成的文件写为xxx.p
- 经过上述分析,我们可以得出结论,动/静态库上存放了许多源文件的.o文件,也就是说动静态库上的.o文件是"链接即用"的,我们只要将他们与自己的.o文件链接(编译时链接/运行时链接),就可以形成想要的可执行程序了,库的出现提高了我们的编程效率。
如何使用动/静态库
使用动态库:
不用做特殊处理,因为编译器默认就是用动态链接的方式链接.o文件使之成为可执行程序的
使用静态库:
gcc -static test.c -o test-s
-static:使用静态链接
动态库与静态库有什么区别?
动态库:
- 动态库的内容会被加载到共享区当中,当可执行程序在运行时,会在虚拟地址中的共享区寻找对应的库内容并链接到程序当中,因此该可执行程序所占空间较小;
- 动态库是可以供多份程序共用的;如何实现的(虚拟地址空间--页表--物理内存)
- 但是,由于程序是在运行时才去库中寻找内容,所以它与库具有关联性,不能独自运行;
静态库:
- 编译器会直接将库的内容拷贝到程序当中,因此程序所占空间较大;
- 但这保证了程序与库之间的独立性,即程序编译好后不再依赖库;
验证:
1.动态库占空间较小,静态库栈空间较大
2.以动态链接形成的程序依赖动态库;以静态链接形成的程序不依赖库
使用ldd+可执行程序名可以查看程序与库的依赖关系
动态库依赖关系:
可以 看到,test依赖的是libc.so.6这个库
但libc.so.6实际上是一个软链接
所以test依赖的实际上是c-2.17这个动态库
注:在Linux中,去掉lib与.so及其后面的版本号就是该库的名称,.so后缀表示这是动态库,.a后缀表示这是静态库
在Windows中,.dll后缀表示动态库,.lib表示静态库
静态库依赖关系:
非动态执行程序,换言之,静态可执行程序不依赖库
//库文件(实现)与头文件(声明)是不同的
如何自己创建动/静态库?
创建静态库:
由于库是.o文件的集合,所以我们要先自己创造一批.o文件,(.o文件是由源文件生成的),除此之外,我们还需要头文件,为将调用的库函数做声明。
注:头文件是用于在编译阶段,让编译器知道,我有这个函数的实现方法,使编译通过
库文件是在链接时要使用到的具体实现方法。
1.创建源文件以及头文件
头文件add.h
#pragma once extern int my_add(int x, int y);
源文件add.c
int my_add(int x, int y) { return x + y; }
头文件sub.h
#pragma once extern int my_sub(int x, int y);
源文件sub.c
int my_sub(int x, int y) { return x - y; }
2.创建.o文件并将.o文件导入静态库
创建.o文件
导入静态库
ar -rc libcal.a add.o sub.o
- ar:归档工具,用于生成静态库
- -rc:指令选项
- r:replace 若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
- c:create 创建库
- libcal.a 生成的库名称
- add.o/sub.o 导入库中的目标文件
3.打包头文件与库文件
为了方便用户使用,我们可以创建一个目录,下设两个文件夹,分别存储头文件和库文件
至此就算完成了创建静态库,但想要使用,还要继续做如下工作。
使用静态库:
1.创建main.c
#include"add.h" #include"sub.h" #include<stdio.h> int main() { int addnum=my_add(1,2); int subnum=my_sub(1,2); printf("addnum:%d,subnum:%d\n",addnum,subnum); return 0; }
2.编译链接,形成可执行程序
gcc main.c -I./mymathlib/include -L./mymathlib/lib -lcal
- -l:指定头文件路径
- -L:指定库文件路径
- -l:指定库名称
大家可以这样记忆:
- I 即Include --头文件都是用#include<xxx.h>表示,所以-I表示头文件路径
- L即Library--意思是库,所以-L表示库文件路径
- l即library--是Library的小写形式,所以-l表示具体的库名称
运行结果:
问题:为什么编译时要加 -I -L -l?
平常我们自己生成可执行程序,可没有加什么 -I -L -l选项,为什么现在却要加呢?显然,是由于我们使用了自己创建的静态库。gcc编译器默认会到指定的路径下去寻找程序所需要的头文件、库文件,所以我们才要指定路径,让编译器找得到对应文件。
所以只要我们将我们的头文件路径与库文件路径添加到编译器默认的搜索路径当中,在编译时也可以不再添加选项
系统中存放头文件搜索的路径是/usr/include
sudo cp ./mymathlib/include /usr/include
系统中存放库文件搜索的路径是/lib64
sudo cp ./mymathlib/lib/libcal.a /lib64/
扩展:头文件需要被放进默认搜索路径吗?
笔者为什么会想到这个问题呢?因为从前我们在写程序时,也包含了自己写的头文件,但是我们也没有将它放进默认搜索路径当中去,程序一样正常运行。以我们上面写的代码为例(main.c),我们不用将头文件放进默认搜索路径当中 。
原因:
头文件分为两种:#include<xxx>和#include"xxx.h",也就是<>引用头文件和""引用头文件。前者一般是系统头文件,后者一般是我们自己写的头文件。编译器针对这二者,在搜索时路径会有所不同:
系统头文件:(按先后顺序进行搜索)
-I
参数指定的路径;- 环境变量
C_INCLUDE_PATH
或CPLUS_INCLUDE_PATH
包含的路径;- 内定路径。
用户头文件:(按先后顺序进行搜索)
头文件所在目录路径;
-I 参数指定的路径;
环境变量 C_INCLUDE_PATH 或 CPLUS_INCLUDE_PATH 包含的路径;
内定路径。
这也就解释了之前我们不将自己写的头文件放进默认搜索路径,编译一样没问题。
验证:
不指定-I 搜索路径,程序编译链接无误,且结果正确
创建动态库:
1.创建源文件以及头文件(与创建静态库的文件内容一致,这里就不费篇幅了)
2.创建.o文件并将.o文件导入动态库:
创建.o文件:
gcc -fPIC -c add.c ; gcc -fPIC -c sub.c
-fPIC:产生与位置无关码,这个编译选项很重要,是实现动态链接的关键
以下解释转载自:Linux动态库和静态库_libc-2.17.so-CSDN博客
-fPIC
作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
创建动态库并导入.o文件:
gcc -shared -o libcal.so add.o sub.o
- -shared:gcc编译选项,用于创建动态库
- -o:生成动态库名称
- add.o sub.o:导入动态库的目标文件
3.打包头文件和库文件
使用静态库:
1.创建main.c(与前文一致)
#include"add.h" #include"sub.h" #include<stdio.h> int main() { int addnum=my_add(1,2); int subnum=my_sub(1,2); printf("addnum:%d,subnum:%d\n",addnum,subnum); return 0; }
2.编译链接,形成可执行程序
运行结果:
加载共享库时发生错误,找不到libcal.so这个共享库
这是由于:
执行程序的加载器也是要在默认路径下搜索库文件的,加载器并不知道我们的库文件存在哪个路径。(静态编译的文件就不一样了,因为它所需要的库文件都已经写进程序了)
解决方法:(让加载器找得到我们的库文件)
以下方法转载自:Linux动态库和静态库_libc-2.17.so-CSDN博客
1.将我们的库存放到默认搜索路径
sudo cp mlib/lib/libcal.so /lib64
2.更改LD_LIBRARY_PATH
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
运行结果:
3.配置/etc/ld.so.conf.d/
/etc/ld.so.conf.d/
路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/
路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。
一些问题、一些现象
问题:
Q:为什么基于静态库和使用动态库生成的两个可执行程序大小差不多?
依赖静态库生成的可执行文件
依赖动态库生成的可执行文件
A:因为编译器默认使用的动态链接
现象:
1.对依赖静态库的可执行文件使用动态链接,该文件可以正常运行,此时它并没有依赖静态库
- 静态库--动态链接:在链接时,库的属性信息会被嵌入到可执行文件当中,因此可执行文件在运行时不再依赖外部库文件。
- 动态库--静态链接:编译时错误
我们最好不要将它们混用,可能会出现意想不到的问题。
2.将库留在程序所在目录与不将库留在当前目录的区别
- 对依赖静态库的程序不产生影响--都正常运行
- 对依赖动态库的程序产生影响
不留库:
程序找不到动态库,无法运行
留库:
程序能找到动态库,且能正常运行
尾言:如若文章有任何问题,欢迎各位在评价区留言指出,不胜感激~