Linux下静态库、共享库的创建与应用
目录
1 什么是静态库和共享库
1.1 为什么要有静态库和共享库
还是先来说一下我自己对静态库和共享库的理解。
为什么会有静态库和共享库这两个东西呢?我们都知道,为了避免一些重复性的工作并且便于编程,开发人员定义了一系列的标准函数以供调用,这些函数都放在相应的函数库中,而我们在进行开发时也会经常用到这些标准函数,那么就需要找到函数的定义,那么编译器该怎么去找到函数的定义呢?
一种方法是让编译器自动识别函数。比如说我在程序中调用了printf函数,那么当编译器识别到对printf函数的调用时,就直接生成printf函数定义的相应代码,这种方法听起来非常简单,但是实际上缺点也很多,因为标准库函数是会更新的,一旦增加、删除或者修改了其中某一个标准函数,那么相应的编译器也必须进行改变,不然无法识别新的函数,这也是相当复杂的,需要从编译器进行改变,那有没有办法可以将标准函数的改变与编译器分开呢?这就是第二种方法了。
第二种方法是直接将所有的标准函数都放在一个源文件中,然后编译生成一个可重定位目标文件(.o),当编译器识别到某一个被调用的函数时,就直接去这个.o文件中寻找该函数的定义,显然在这种方法下,即使修改了标准函数,编译器也不需要进行改变了,这就解决了第一种方法带来的问题。但是这种方法也有缺点,因为标准函数是非常多的,比如说a.cpp中只用了一个printf函数,b.cpp中只用了一个scanf函数,当它们各自编译结束后,二者生成的可执行文件中都包含着所有标准函数集合的一个副本,这是对磁盘空间的一种浪费,不仅如此,当可执行文件运行,程序加载到内存中时,是会将所有标准函数一起加载到内存中的,这也是对内存资源的一种极大浪费,不容忽视。除此之外,当某个标准函数发生改变的时候,都需要对所有函数进行重新编译,这个过程也非常耗时而不易维护。
显而易见,第二种方法的主要缺点就是将所有函数都放在一起太浪费资源,因此就考虑将这些函数都各自分开成一个个的小模块,到时候按需调用。但是也不能每个函数自成一个模块,这样显然也会很麻烦,因此就可以考虑将相关的函数放在一个模块中,然后一个或多个模块封装到一个库中,这个库显然就是多个模块文件(可重定位目标文件)的集合,也叫静态库(.a)。
值得注意的是,静态库也不知道用户到底会调用哪个函数(模块),所以其中也很可能存在用户不需要的模块,但在链接时,链接器将只会复制被程序引用的目标模块,这样一来,相较于第二种方法,程序所生成的可执行文件在内存和磁盘中的空间大小将显著降低。
这样看起来,静态库似乎已经将前两种方法的缺点都改善了,但它并非是最好的,举个例子,现在有a.cpp和b.cpp都依赖于同一个静态库lib.a中的相同模块m,各自生成可执行文件a和b,那么不管程序是在编译时还是运行时,磁盘中和内存中m都会存在两份,各自存在于可执行文件a和b中。这显然也是一种资源的浪费,而共享库(.so)则很好的解决了这个问题,共享库在程序运行时,会被加载到内存中任意地址处,此时,内存中就存在着该共享库的副本,所有引用该共享库的可执行文件都共享这个库,这样一来,原本需要在磁盘和内存中分别存在2份的模块m如今只需在磁盘和内存中各存1份即可。
1.2 静态库和共享库的区别
- 静态库被程序静态链接于编译时,而共享库被动态链接于运行时;
- 静态链接时,需要把所有对静态库的引用内容都嵌入到最终的可执行文件中,且相同模块并不共用,都有相应的副本,因此相应的可执行文件在磁盘和内存中所占空间较大;而动态链接是发生在运行时的,并且是共享的,因此相应的可执行文件在磁盘中所占空间较小;
- 由于静态链接是在编译时完成的,因此可执行文件中以及包含有所需的静态库,可执行文件可以单独运行;由于动态链接是在程序运行时完成的,可执行文件在加载入内存运行时才会链接共享库,因此可执行文件必需共享库的支持,不能单独运行;
- 当静态库中某个模块更新后,需要重新编译链接生成相应的可执行文件;另一方面若共享库中某个模块更新了,只要接口没有改变,就不需要重新编译链接生成可执行文件;
- 静态链接比动态链接速度稍快
- 静态库中不能包含共享库,而共享库中可以包含静态库
综合以上区别,可以发现动态链接的性能是明显优于静态链接的,但是这不一定就说明静态链接一定比不上动态链接了。如果库本身就比较小且不经常改变,基于速度的角度,应当选择静态链接,否则还是应当选择动态链接,由于是大型软件需要经常维护、扩展之类的情况。除此之外,静态链接的一个显著优点是其相应的可执行文件已经是完整的,包含所有所需的模块,因此可以很方便地移动到其他地方执行。
2 用例子说明两种库的创建与应用
建立以下文件树,其中bin文件夹用于存放可执行程序,lib文件夹用于存放库文件,src文件夹用于存放源代码,如图所示:
在src文件夹中,包含了四则运算的四个源文件.cpp以及一个主函数源文件main.cpp,其中main.cpp的代码如下:
四则运算分别定义于相应的.cpp文件中。现在以该例来说明静态库与共享库如何创建并应用的。
2.1 静态库的创建与应用
2.1.1 静态库的创建
静态库实际上就是一个或多个.o文件的集合。因此第一步是将各模块编译为.o文件。
编译命令为g++ -c xxx.cpp 如下:
此时就可以将add.o、div.o......四个模块打包到静态库中,这里需要用到AR工具,ar的指令详解可参考ar指令说明
(在使用ar命令时,创建库时常用ar rcs .....,若往库中添加模块时用r即可ar r ....,若要删除库中某一模块用d即可ar d ......)
在这里使用ar rcs libxxx.a xx.o xx.o..... rcs中的r表示向库中添加模块,若模块已存在则替换,c表示创建库文件,s表示生成一个目标文件索引。
此时可用命令参数t来查看静态库中的模块清单,如下所示:
此时静态库则创建完成。
2.1.2 静态库的应用
静态库的应用即是将main.cpp文件与建立的libcalcu.a静态库进行链接并生成可执行文件运行的过程。
静态库链接直接使用g++命令即可,不过由于g++在默认情况下是动态链接的,因此如果要进行静态链接那么就需要命令参数-static,如下所示:
此时就在bin文件夹中生成了可执行文件output,加载并运行output,如下:
为了与动态链接的可执行文件作对比,我们再来看看可执行文件output的大小:
可见静态链接后的可执行文件大小为1.6M。
2.1.3 静态库搜索路径
1.先去找链接命令行中的参数-L。-L参数可直接指定静态库的搜索路径,-L. 表示在当前目录下搜索(注意L后面有个点),-L后面直接加上路径表示在该路径下搜索(L后面没有点),比如-L../lib libcalcu.a 表示在当前目录的同级目录lib中搜索libcalcu.a静态库,这里在-L指明了搜索路径的情况下,静态库的命名格式为libxxx.a,那么就可以用-l命令将其简化为-lxxx,即可将-L../lib libcalcu.a 简化为-L../lib -lcalcu;
2.再找静态链接的环境变量LIBRARY_PATH下的路径;(个人感觉这里设置环境变量意义不大)
3.再找系统默认路径/lib、/usr/lib、/usr/local/lib。
补充说明
在进行静态链接时一定要注意命令行中库文件放在后面,被依赖的文件也要放在后面,这里的a依赖于b是指b中定义了a中所引用的一个符号。如果文件之间依赖关系复杂,可以将 多个依赖关系复杂的文件放在一个静态库中,或者在命令行中根据依赖关系多次指明被依赖文件也是可以的,比如说x.o依赖于y.a,同时y.a依赖于z.a,并且z.a由依赖于y.a,那么此时这三个文件在命令行中的链接顺序就可以是x.o y.a z.a y.a。
之所以这样,是因为在符号解析过程中,链接器维护一个可重定位目标文件的集合E,这个集合中的文件最终合并为可执行文件;一个未解析符号集合U,该集合中存放着被引用但是未被定义的符号;以及一个一个已经被定义的符号D,在初始状态下,三个集合均为空。所谓符号,就像变量名、函数名都是符号。
然后链接器从左到右按照各个可重定位目标文件(.o)和静态库文件(.a)在命令行上出现的顺序来对每个文件进行扫描,如果输入文件为可重定位目标文件,那么链接器就会将该文件放到集合E中,并且将该输入文件中的符号定义和引用情况反应在集合U和集合D中;
如果输入文件为静态库文件,那么链接器就会扫描该静态库文件中的各成员文件(.o),将成员文件中的符号与U中已被引用但是未定义的符号进行匹配,如果该静态库中某个成员文件定义了U中某个已被引用但未被定义的符号,那么链接器就将该成员文件放到集合E中,然后在U中删除该符号,D中添加该符号,再继续扫描下一个成员文件,这样一直反复进行扫描下去,直到U和D中集合都不再变化,就将多余的成员文件抛弃,然后就继续扫描下一个输入文件了。
若文件a依赖于b,那么就说明a中存在某一符号的引用,b中存在该符号的定义。如果先扫描到符号的定义,那么就会将该符号放到D中,而由于每次扫描都是与U中的符号进行匹配,因此即使后面再扫描到该符号的引用,也会直接将该符号又放入U中,由于符号的定义已经在D中了,因此到最后该符号依然存在于U中,U中非空,说明链接中存在被引用但是未定义的符号,从而该符号被认为是未定义的符号而报错。
2.2 共享库的创建与应用
2.2.1 共享库的创建
创建共享库的输入文件可为.c/.cpp文件,用命令参数-shared表示创建共享库,-fpic参数也是必要的,指示编译器生成与位置无关的代码,这样才能实现应用程序之间的资源共享,如下所示:
可见此时lib文件夹下存在的libcalcusr.so即是创建的共享库。
值得一提的是,虽然-static和-shared这一对命令参数恰好一个代表静态一个代表共享,但是二者使用的时机是完全不同的:-static使用在生成可执行文件时,而-share则使用在创建共享库时,而静态库的创建是使用的ar命令。
2.2.2 共享库的应用
共享库创建完成后,就要将其链接到main.cpp文件中来生成可执行文件了,如下:
可以看到,可执行文件output_s已经生成,并且其大小明显小于静态链接所生成的可执行文件。
这里尤其需要注意的是./libcalcusr.so,这里直接指定了可执行文件所要链接的共享库的搜索路径在当前目录下(如果将./libcalcusr.so改为libcalcusr.so,虽然生成可执行文件时不会出错,但是最终可执行文件运行时是无法找到共享库的,即链接时正常,运行时出错),同样,如果将共享库文件移至src下,那么就应当将其改为../src/libcalcusr.so,总之,在最后需要指定共享库的搜索路径,否则在加载执行可执行文件时会出现错误。
加载运行如下:
2.2.2 共享库搜索路径
1.先找编译目标代码时指定的动态库搜索路径。这里所指的编译时实际就是指的在最后用共享库和main.cpp文件生成可执行文件时直接指定共享库的搜索路径,需要注意的是,这里指定共享库的搜索路径必须同时指出链接时路径和运行时路径,链接器根据给出的链接时路径找到共享库这样才能生成可执行文件,程序运行时需根据运行时路径找到共享库才能运行可执行文件。有两种方法,一种是如上所述直接给出共享库的路径,这里就相当于路径既代表了链接时路径也代表了运行时路径;另一种方法是使用命令参数-L来指定链接时路径,这点和静态库类似就不多说了,然后使用命令参数-Wl(小写L),-rpath=xxxx来指定运行时路径,比如说这里的libcalcusr.so在lib文件夹下,main.cpp在src文件夹下,我要将可执行文件最终输出在bin文件夹下,当前目录为src,就使用以下命令即可:g++ -o ../bin/output_s main.cpp -L../lib -lcalcusr -Wl,-rpath=../lib,其中的-lcalcusr也是在-l参数下的库名简写。
2.再找环境变量LD_LIBRARY_PATH指定的动态库搜索路径。如果此时已经生成了可执行文件,但是可执行文件找不到共享库从而无法运行,那么就可以设置环境变量LD_LIBRARY_PATH来指出共享库的搜索路径。举个例子,生成的可执行文件在bin文件夹下,不管之前链接时是如何指定搜索路径的,总之现在无法找到共享库,而此时共享库位于bin文件夹下,那么就可以使用指令export LD_LIBRARY_PATH=../bin ,这里的右侧路径是相对于当前路径而言的。不过这种方式是治标不治本的, 只是当前链接可行,后面就不行了。
3.再找配置文件/etc/ld.so.conf中指定的动态库搜索路径。打开/etc/ld.so.conf,在文件末尾处加上共享库路径,保存后再在命令行中输入ldconfig命令执行即可。
4.最后寻找默认的动态库搜索路径/usr/lib、/lib(分先后顺序)。
此外,如果我们并不知道可执行文件需要链接哪些共享库,就可以使用ldd指令,来查看一个可执行文件所依赖的共享库,这样也方便解决无法找到共享库的问题。
如图所示,可以看到可执行文件output_s所依赖的共享库。
补充说明
以上链接共享库的方法为隐式链接共享库,当然还有显式链接共享库了。显示是指在程序中直接写出相关的链接代码,在Linux下,可用dlopen函数通过指定共享库路径来加载和链接共享库,加载成功后可以得到相应共享库的句柄,然后就可以根据该句柄并调用dlsym函数来获取相应共享库中指定符号的地址(往往就是共享库中某函数地址),通过这个地址就可以调用相应的函数了,从而实现共享库的应用。