Linux下静态库、共享库的建立与应用

目录html

1 什么是静态库和共享库编程

1.1 为何要有静态库和共享库函数

1.2  静态库和共享库的区别工具

2 用例子说明两种库的建立与应用性能

2.1 静态库的建立与应用spa

2.1.1 静态库的建立命令行

2.1.2 静态库的应用htm

2.1.3 静态库搜索路径blog

补充说明索引

2.2 共享库的建立与应用

2.2.1 共享库的建立

2.2.2 共享库的应用

2.2.2 共享库搜索路径

补充说明


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函数来获取相应共享库中指定符号的地址(每每就是共享库中某函数地址),经过这个地址就能够调用相应的函数了,从而实现共享库的应用。