本文学习文章Linux库文件详解的笔记,并对其内容进行组织优化,新增Unix高级环境编程中对共享库的讨论与介绍。如对文章内容有任何问题欢迎留言讨论。由于微信订阅号的限制,发布后不能编辑内容,因此可以通过阅读原文来获取最新文章内容。如果文章内容存在错误,欢迎发送消息讨论。

基本概念

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a,.lib)和动态库(共享库)(.so,.dll)。
Linux系统中静态库.a动态库.so。本文讲解linux系统中的静态库与动态库。
所谓静态、动态是指链接的方式。将一个程序编译成可执行程序的步骤:

静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大。这点我们稍后通过例子验证。动态库(共享库)的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。
不同的应用程序如果调用相同的动态库(共享库),那么在内存中只需要有一份该动态库(共享库)的实例。共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小,但增加了运行时间开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。

库存在的意义

库是别人写好的现有的,成熟的,可以复用的代码,你可以使用但要记得遵守许可协议。
现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
共享库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。

静态库

静态库的后缀是.a,它的产生分两步
Step 1.由源文件编译生成一堆.o,每个.o里都包含这个编译单元的符号表
Step 2.ar命令将很多.o转换成.a,成为静态库
示例:
max.c

int max(int a,int b)
{
    return (a>b)?a:b;
}

min.c

int min(int a,int b)
{
    return (a>b)?b:a;
}

gcc -c max.c
gcc -c min.c
将生成max.o与min.o,然后使用ar命令将多个.o打包到一个静态库中。
ar cr libcmp.a max.o min.o

然后使用如下代码使用该静态库中的max函数与min函数。

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    printf("%d\n",max(a,b));
    printf("%d\n",min(a,b));
    return 0;
}

使用gcc -o main main.c -L. -lcmp来使用静态库。
-L 选项指定从.(当前目录寻找库文件libcmp.a),我们在上面gcc命令中去掉前缀lib和后缀.a,与-l+库名组合一起使用,-l和库名之间可以有空格,也可以没有,因此也可以写成:gcc -o main main.c -L. -l cmp
我们就生成了可执行文件main.然后程序就能正常运行,建议使用-lcmp形式,因为这种写法兼容性更好。

动态库

动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀lib,但其文件扩展名为.so。例如:我们将创建的动态库名为cmp,则动态库文件名就是libcmp.so。
首先需要介绍GCC命令的诸多选项:
-shared 该选项指定生成动态连接库,不用该标志外部程序无法链接。相当于一个可执行文件。
-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
-L.:表示要连接的库在当前目录中(注意L后的点)
-lcmp:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称,如果我们生成的库为libmirror.so,那么使用动态库时gcc参数为-lmirror.
LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
当然如果有root权限的话,可以修改/etc/ld.so.conf文件,然后调用 /sbin/ldconfig来达到同样的目的,不过如果没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了。
代码依旧是上面的max.c和min.c。操作步骤如下:
gcc -c -fPIC max.c
gcc -c -fPIC min.c
gcc -shared -o libcmp.so max.o min.o
这样就生成了动态库libcmp.so,上面可以直接使用一条语句:
gcc -fPIC -shared -o libcmp.so max.c min.c
然后使用动态库中的max和min函数,结果出错:

出错原因在于:错误提示,找不到动态库文件libmyhello.so。程序在运行时,会在/usr/lib和/lib等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。有多种方法可以解决。
(1)将生成的动态库拷贝到/usr/lib中
(2)更改环境变量。
LD_LIBRARY_PATH=. ./main
LD_LIBRARY_PATH=.是告诉可执行文件main,从当前目录寻找动态库。
还可以通过上文提到的方法,拥有root权限,通过执行ldconfig来解决这一问题。
操作实例:

上图中可以看到,在不做处理时提示找不到动态库,也证明了如果删除动态库,可执行文件将无法执行。首先我们将动态库拷贝一份到目录/usr/lib下,然后执行可执行文件执行成功。
第二种方法是设置环境变量,从当前目录寻找动态库。

依赖库

在使用静态库生成可执行文件后,如果我们删除libcmp.a,可执行程序照常运行,静态库中的公用函数已经连接到目标文件中了。
静态链接库的一个缺点是,如果我们同时运行了许多程序,并且它们使用了同一个库函数,这样,在内存中会大量拷贝同一库函数。这样,就会浪费很多珍贵的内存和存储空间。使用了共享链接库的Linux就可以避免这个问题。
共享函数库和静态函数在同一个地方,只是后缀有所不同。比如,在一个典型的Linux系统,标准的共享数学函数库是/usr/lib/libm.so。
当一个程序使用共享函数库时,在连接阶段并不把函数代码连接进来,而只是链接函数的一个引用。当最终的函数导入内存开始真正执行时,函数引用被解析,共享函数库的代码才真正导入到内存中。这样,共享链接库的函数就可以被许多程序同时共享,并且只需存储一次就可以了。共享函数库的另一个优点是,它可以独立更新,与调用它的函数毫不影响。

静态库动态库对比

首先我们来看下,使用静态库与动态库可执行文件的大小。
代码如下:

#include <stdio.h>
int main()
{
     printf("Hello,world\n");
     return 0;
}


使用static阻止使用共享库,可以通过size命令查看a.out的大小。
通过上面的图我们可以清晰的看到,使用静态库的话生成的可执行文件的大小远远大于使用动态库的方式。
我们发现我们在使用静态库libcmp.a和动态库libcmp.so时使用的gcc命令都一样,如果静态库libcmp.a和动态库libcmp.so都存在,会怎样呢?

在解释图中执行情况前,首先介绍ldd命令:
ldd命令用于打印程序或者库文件所依赖的共享库列表。
因此我们可以在执行gcc -o main main.c -L. -lcmp之后,通过ldd命令查看在静态库和动态库同时存在且在去掉前缀和后缀之后名字相同,在gcc中均为-lcmp,我们可以看到在第二行libcmp.so,因此可以确认:如果当静态库和动态库同名时, gcc命令将优先使用动态库。
图中提示找不到库的原因在前面我们已经有解决方法,最简单的方法是将库拷贝到/usr/lib目录下,该例子只为证明如果当静态库和动态库同名时, gcc命令将优先使用动态库。
静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大。这点我们前面的例子已验证。
动态库(共享库)的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。
不同的应用程序如果调用相同的库,那么在内存中只需要有一份该动态库(共享库)的实例。
静态库和动态库的最大区别,静态情况下,把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度,和降低程序的耦合度。
静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。因此删除静态库后对可执行文件无影响。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在,如果删除动态库将导致可执行文件执行失败。