Linux基础IO----动态库与静态库
什么是库?
库是由一些.o文件打包在一起而形成的可执行程序的半成品。
如何理解这句话呢?
首先,一个程序在运行前需要进行预处理、编译、汇编、链接这几步。
预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。
链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序。
在链接之前的这些.o文件就是上文提到的被打包进库里的文件。
那么为什么要对这些文件进行打包呢?
拿我们平时常用的库举例,我们写程序所用到的输入输出函数,比如prinf、scanf,这些函数都不是我们自己去实现的,为什么却能为我们所用呢?原因就是这些函数是在库里的程序实现的,不需要我们手动重新再去“造轮子”,拿来就直接能用,库里还有很多方便使用的函数,在程序里包含不同的库,能使用的库函数就越多越方便我们实现程序。所以库存在的意义就是帮我们把基础的常用的函数等打包到一起直接提供我们使用。所以我们又说库是可执行程序的“半成品”。
认识动静态库
编写一个简单的程序:
能够正常打印处hello lib是因为我们的库中已经有了printf函数的实现,在gcc生成可执行程序时,将C标准库链接到我们的程序里了。接下来我们通过ldd+文件名命令查看一个可执行程序链接了哪些库吧:
注意:ldd命令只能查看可执行程序依赖的库和动态库的依赖库。
其中lib.c.so.6就是该可执行程序依赖的库文件,我们通过ls命令可以发现lib.c.so.6是一个软链接:
在Linux中,动态库的后缀是.so 静态库的后缀是.a
去掉前缀lib和后缀.so .a和后面的版本号就是库的名字,在这里就是c
在默认情况下gcc 或者g++编译器都是默认动态连接库的,若想进行静态链接需要加上-static选项:
可以发现静态连接库的可执行程序的大小非常大,这是因为静态链接就是把静态库直接整个加入到你的可执行程序里的,所以这个程序执行就不需要依赖仍和库。
动静态库的特征
静态库的特征
静态库是程序在编译链接的时候把库的代码直接复制到可执行程序里,导致文件变大,并且程序在执行的时候不在依赖静态库。
优点:静态链接库的程序运行不再依赖静态库。
缺点:程序占用大量空间,如果有多个程序都是静态链接的同一个库就会有很多重复代码。
动态库的特征
动态库是在程序运行的时候才去链接相应代码的,多个程序共享库内的代码,所以动态库也叫共享库,能够大大节省内存。链接时将库映射到进程的进程地址空间的共享区中,当进程调用库内的函数时就可以直接去共享区内找。
优点:节省空间,内存中不会存在很多重复的代码。
缺点:程序必须以来动态库运行,如果动态库找不到了程序就运行不了。
静态库的打包和使用
为了更好的理解静态库,下面我们将自己写的方法打包进一个我们自己的静态库中。
add.h add.c:
#pragma onceextern int my_add(int x, int y);
#include "add.h"int my_add(int x, int y)
{return x + y;
}
sub.h sub.c:
#pragma onceextern int my_sub(int x, int y);
#include "add.h"int my_sub(int x, int y)
{return x - y;
}
打包:
第一步:让源文件生成.o文件
第二步:用ar命令将 .o文件打包
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给别人使用了。
使用:
创建下面程序,包含头文件add.h:
#include <stdio.h>
#include <add.h>int main()
{int a = 10, b = 20;int sum = my_add(a, b);printf("%d + %d = %d\n",a,b,sum);return 0;
}
方法一:使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
gcc main.c -I./mathlib/include -L./mathlib/lib -lcal
根据命名规则我们的库名字就是cal,所以 最后一个选项是cal
说明:
- 编译器不知道你自己写的头文件add.h的位置,所以编译的时候需要你指出来,同时还需要库文件的路径来定位库。
- 实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀
lib
,再去掉后缀.so
或者.a
及其后面的版本号,剩下的就是这个库的名字。 -I
,-L
,-l
这三个选项后面可以加空格,也可以不加空格。
方法二:把头文件和库文件拷贝到系统路径下
为什么用C语言自带的库不是需要这样大费周章的写路径,其实是因为编译器找库和头文件首先是去系统路径下找的,所以不需要直接指出,同时gcc是用来编译C程序的所以默认找的是c库所以也不用指明库的名字
可以尝试:
sudo cp mathlib/lib/libcal.a /lib64/
sudo cp mathlib/include/*
将你的库加入系统路径,需要注意的是就算把文件拷贝到系统路径下,使用gcc时仍需指定库名。
扩展:
实际上我们拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染。
动态库的打包和使用
打包:
还是以刚才的四个文件举例:
第一步:让源文件生成对应的.o文件
此时用源文件生成目标文件时需要携带-fPIC
选项:
-fPIC
(position independent code):产生位置无关码。
fPIC的作用:
- 实现内存地址无关性
- 使用-fPIC编译的代码,不依赖与在内存中的具体位置,可在不同内存地址上正确运行。动态链接库被不同程序加载时,可能加载到不同内存地址,位置无关代码可以适应这种情况。
- 支持动态库共享
- 允许多个进程共享动态库的单个副本 。若不使用
-fPIC
,加载动态库代码段时,因代码含绝对地址,需重定位,这会使每个使用该库的进程在内核里生成各自的代码段副本,造成内存浪费
- 允许多个进程共享动态库的单个副本 。若不使用
- 提高程序灵活性与可移植性
- 使动态链接库能在不同环境、不同内存布局下加载和使用,无需因内存地址差异重新编译
第二步:使用-shared选项将所有目标文件打包为动态库
gcc -shared -o libcal.so add.o sub.o
第三步:将头文件和生成的动态库组织起来
和静态库一样,将头文件和库文件整合到一起一个放include一个放lib目录
使用:
还是用main.c来演示:
#include <stdio.h>
#include <add.h>int main()
{int x = 20;int y = 10;int z = my_add(x, y);printf("%d + %d = %d\n", x, y, z);return 0;
}
说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I
,-L
,-l
这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l
选项指明需要链接的库名字来生成可执行程序,下面我们仅以第一种方法为例进行演示。
gcc main.c -I./mlib/include -L./mlib/lib -lcal
编译成功但是执行失败,这是为什么呢?
在编译时通过
-L./mlib/lib
指定了动态库的搜索路径,但这只对编译链接阶段有效,而运行阶段的动态库搜索需要单独配置。
方法一:拷贝.so文件到系统共享库路径下
我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。
sudo cp mlib/lib/libcal.so /lib64
方法二:更改
LD_LIBRARY_PATH
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/YOURPATH/libtest/mlib/lib
但是这个环境变量的修改时暂时的,下次重启就失效了。