静态库、动态库和共享库 -- 程序函数库 -- C 语言




 ​程序函数库​​

​ ​程序函数库,本质是一个包含已经编译好代码和数据的文件,这些编译好的代码和数据通常是经过高度抽象的通用逻辑,可以供其他程序使用,避免重复造轮子。程序函数库可以使得程序的开发工作更加模块化,更容易重新编译,而且更方便升级。


程序函数库可分为 3 种类型:

  • 静态库(Static Libraries)
  • 共享库(Shared Libraries)
  • 动态库(Dynamically Loaded Libraries)

在 Linux 中,静态库命名为 lib*.a;而动态库和共享库本质是一个类似的东西,只是在 Linux 中叫作共享对象 lib*.so(Share Object),而在 Window 中叫作动态加载链接, 文件后缀为 .dll。


在 C 语言中,不管是使用哪一种库,程序员必须在程序中通过 include 来包含相应的头文件,并在预编译阶段替换 include 的内容,然后在链接阶段将调用到的库函数从各自所在的档案库中链接到合适的地方。


从上文我们知道,链接(Link)是程序被装载到内存运行之前需要完成的一个步骤。链接又分为动态链接(Dynamic Link)和静态链接(Static Link)两种方式。

  • ​静态链接​:是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。
  • ​动态链接​:则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。


库的基本概念

库是已经写好的、成熟的、可复用的代码, 每个程序都需要依赖很多底层库, 不可能每个人的代码从零开始编写, 因此库的存在具有非常重要的意义.

在我们开发的应用中经常有一些公共代码是需要反复使用的, 就可以把这些代码编译成库文件.

库可以简单看成一组目标文件的集合, 将这些目标文件经过压缩打包之后形成的一个文件.





静态链接​​

​ 

静态库的优缺点


直观的看,一个全静态方式生成的简单 print 程序大小为 857K,而动态链接生成的一样的可执行文件只有 8.4K,因为静态链接的可执行文件包含了整理 stdio 库文件。


如上图,对于静态编译的程序 1、2,因为都使用了 staticMath 库。所以在内存中就有两份相同的 staticMath.o 目标文件,一旦程序数量过多就很可能会内存不足,很浪费空间。


优点

静态链接方式的好处是:

  • 方便程序移植,因为可执行程序包含了所有库函数的内容,放在任何环境当中都可以执行。
  • 如果你想把自己提供的函数给别人使用,但是又想对函数的源代码进行保密,此时就可以给别人提供一个静态函数库文件。


缺点

而缺点就是:

  • 可执行文件通常会比较大。
  • 而且每次库文件升级的话,都要重新编译源文件,很不方便。




创建静态库文件​​

​ ​静态函数库文件使用 ar(Archiver)程序创建,下面看一个例子。

add.c

#include "add.h"

int add(int a, int b) {
    return a + b;
}

add.h

#ifndef _ADD_H
#define _ADD_H

int add(int a, int b);

#endif

生成目标文件

gcc -c add.c --std c99

生成静态库文件

ar -crv libadd.a add.o

使用静态库:

#include <stdio.h>

#include "./add.h"


int main() {
     int number1 = 10;
     int number2 = 90;
     printf("SUM: %d\n", add(number1, number2));
     return 0;
}

编译

  • -L:指定加载库文件的路径。
  • -l:指定加载的库文件。

gcc -std=c99 test.c -o test -L./ -ladd


动态链接​​

​ ​动态链接,即:在程序运行过程中动态的调用库文件。

  • 好处是:占空间小、程序文件小。
  • 缺点是:可移植性太差,如果两台电脑运行环境不同,例如:动态库存放的位置不一样、没有动态库文件,就很可能导致程序运行失败。


在基于 GNU glibc 的 Linux 系统中,​gcc 编译链接时的动态库搜索路径的顺序通常为​

  • 首先从 gcc 命令的参数 -L 指定的路径寻找;
  • 再从环境变量 LIBRARY_PATH 指定的路径寻址;
  • 再从默认路径 /usr/lib64、/usr/lib、/usr/local/lib 寻找。

在基于 GNU glibc 的 Linux 系统中,运行一个 ELF 格式的可执行文件时,系统会启动 Program Loader(/lib/ld-linux.so.X,X 是版本号),这个 Loader 会加载可执行文件所需要使用的所有的共享函数库。所以,​二进制文件执行时的动态库搜索路径的顺序通常为​:

  • 首先搜索编译目标代码时指定的动态库搜索路径;
  • 再从环境变量 LD_LIBRARY_PATH 指定的路径寻址;
  • 再从配置文件 /etc/ld.so.conf 中指定的动态库搜索路径;
  • 再从默认路径 /usr/lib64、/usr/lib 寻找。


可见,区别于静态链接,动态链接的方式使得多个程序可以使用同一个库文件,这就是所谓 ​共享对象​ 的含义。




创建共享库文件​​

​ ​在动态链接的过程中,我们希望链接的不是存储在磁盘上的目标文件代码,而是链接到了内存中的共享库(Shard Libraries)。这个加载到内存中的共享库会被很多程序的指令调用。在 Windows 中,这个共享库文件就是 .dll(Dynamic-Link Libary,动态链接库)文件。而在 Linux 下,这些共享文件就是 .so(Shared Object)文件。


​注意​:由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如 libtest.a 和 libtest.so,gcc 链接时默认优先选择动态库,链接 libtest.so,如果要让 gcc 选择链接 libtest.a 则可以指定 gcc 选项 -static,该选项会强制使用静态库进行链接。e.g.

gcc -static hello.c -o hello


有了动态链接方式之后,我们得以把内存利用得更加的极致,共享库文件是有如共享单车一般的存在。因为共享库文件中的函数是在程序启动时被加载的,所有的程序在重新运行的时候都可以自动加载最新的函数库中的内容,所以,非常易于更新。Linux 系统中的共享库文件还有可以实现更多的功能:

  • 升级了函数库但是仍然允许程序使用老版本的函数库。
  • 当执行某个特定程序的时候可以覆盖某个特定的库或者库中指定的函数。
  • 可以在库函数被使用的过程中修改这些函数库。


不过,要想在程序中运行时加载共享库代码,就要求这些共享库代码是 “地址无关” 的。也就是说,我们编译出来的共享库文件的指令代码,是地址无关。换句话说,共享库无论加载到那个内存地址,都能够正常的运行。否则,就是地址相关代码。幸运的是,大部分函数库代码都是可以做到地址无关的,因为它们都被实现为接收特定的输入,进行确定的操作,然后再返回结果。这些函数的代码逻辑和输入数据存放在内存什么位置并无所谓。


共享库文件的名字​​

​ 

每个共享函数库都有个特殊的名字(soname)。soname 必须以 lib 作为前缀,然后是函数库的名字,然后是 .so 后缀。此外,每个共享函数库都有一个真正的名字(real name),它是包含真正库函数代码的文件。真名有一个主版本号,和一个发行版本号(可选)。主版本号和发行版本号使你可以知道库函数的真实版本。


生成目标文件

gcc -c add.c --std c99

生成共享文件

  • -shared :指定生成共享库。
  • -fPIC :表示编译为位置独立的代码,用于编译共享库。目标文件需要创建成位置无关码,就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方。

gcc -fPIC -shared -o libadd.so add.c

动态链接编译

gcc -std=c99 test.c -o test -L./ -ladd

执行程序

$ ./test
./test: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

ERR:​libadd.so: cannot open shared object file: No such file or directory

因为执行程序找不到 libadd.so,查看 test 程序的动态链接库信息:

$ ldd test
  linux-vdso.so.1 =>  (0x00007fff39fd5000)
  libadd.so => not found
  libc.so.6 => /lib64/libc.so.6 (0x00007f5410c0b000)
  /lib64/ld-linux-x86-64.so.2 (0x00007f5410fd9000)

可以看到 test 执行程序用到的 libadd.so 确实是 not found,这是因为在 /etc/ld.so.conf 文件中设置了动态链接库了寻找路径:

$ cat /etc/ld.so.conf
include ld.so.conf.d/*.conf

$ ll /etc/ld.so.conf.d
总用量 40
-rw-r--r--  1 root root 26 4月   7 22:41 bind-export-x86_64.conf
-rw-r--r--  1 root root 24 4月   2 02:32 hcoll.conf
-rw-r--r--  1 root root 19 4月   2 02:23 ibutils.conf
-r--r--r--  1 root root 63 4月   1 07:40 kernel-3.10.0-1127.el7.x86_64.conf
-r--r--r--. 1 root root 63 10月 21 2017 kernel-3.10.0-693.5.2.el7.x86_64.conf
-r--r--r--  1 root root 63 11月 29 2018 kernel-3.10.0-957.1.3.el7.x86_64.conf
-r--r--r--  1 root root 63 5月   1 22:57 kernel-rt-3.10.0-1127.rt56.1093.el7.x86_64.conf
-rw-r--r--  1 root root 17 4月   3 01:52 mariadb-x86_64.conf
-rw-r--r--  1 root root 22 4月   2 02:26 mxm.conf
-rw-r--r--  1 root root 24 4月   2 02:28 sharp.conf

显然这里是没有 libadd.so 的存储路径的,所以我们需要添加一下 libadd.so 的路径:

$ cat /etc/ld.so.conf.d/test.conf
/root/workspace/test

然后执行 ldconfig 命令生效,再次执行 test 程序:

$ ./test
SUM: 100



共享库文件的存储路径​​

​ 通常的,共享函数库文件会被存放在一些特定的目录里,这样程序才能找到并使用共享库函数。常见的是 /usr 目录(UNIX System Resources),该目录作为操作系统的核心,包含了操作系统发行时自带的各种程序,以及支持这些程序的各种共享库文件、头文件、可执行文件等等。下属的 /usr/local 则包含了本地系统管理员自行添加的程序的库文件、头文件和可执行程序等。

  • /usr/lib
  • /usr/lib64
  • /usr/local/lib
  • /usr/local/lib64

64 位 Linux 的默认共享库路径为 /usr/lib64,其次才是 /usr/lib,而 /usr/local/lib、/usr/local/lib64 则不作为默认查询路径。GNU 标准建议所有的函数库文件都放在 /usr/local/lib 目录下,而且建议命令可执行程序都放在 /usr/local/bin 目录下。但这也只是一个建议,并不强制要求。


​LD_LIBRARY_PATH 环境变量​​

环境变量 LD_LIBRARY_PATH 用于指明共享库的检索路径,当我们在调试一个新的函数库、或者在特殊的场合使用一个非标准的函数库的时候就可以考虑使用该变量。当我们想在这默认检索目录以外存放共享库,但是又不想在 /etc/ld.so.conf 中增加记录,那么就可以 export LD_LIBRARY_PATH 执行一个自定义的路径。环境变量 LD_PRELOAD 则列出了需要被优先加载的共享库文件,功能与 /etc/ld.so.preload 类似。这些都是由 Program Loader 实现的。



 ​ldconfig 指令​​

​ 当程序启动时搜索所有的路径效率显然会很低,于是 Linux 实现了一个高速缓冲机制。ldconfig 是一个共享库管理工具,用于在遍历共享库检索路径并刷新缓存文件 /etc/ld.so.cache。ldconfig 通常在系统启动时,或引入了新的共享库时执行。


ldconfig 缺省情况下读出 /etc/ld.so.conf 相关信息,然后适当地设置符号链接,e.g.

$ ll
-rw-r--r-- 1 root root 459182 6月  17 15:52 libhiredis.a
lrwxrwxrwx 1 root root     18 6月  17 15:52 libhiredis.so -> libhiredis.so.0.14
-rwxr-xr-x 1 root root 269776 6月  17 15:52 libhiredis.so.0.14

如果共享库不是一个符号连接,而是一个实体文件的话,ldconfig 就会提示 is not asymbolic link 的错误。

ldconfig 
ldconfig: /lib/libdb-4.7.so is not a symbolic link

解决办法就是改成符号连接即可:

mv libdb-4.7.so libdb-4.so.7
ln -s libdb-4.so.7 libdb-4.7.so


最后,再写一个 Cache 到 /etc/ld.so.cache 这个文件中,这个 /etc/ld.so.cache 就可以被其他程序有效的使用了。


ldconfig 指令的可用选项说明如下:

  • -v:显示正在扫描的目录及搜索到的动态链接库,还有它所创建的链接的名字。
  • -n:仅扫描命令行指定的目录。
  • -N:不重建缓存文件。
  • -X:不更新文件的链接。
  • -f CONF:此选项指定共享库的配置文件,默认为 /etc/ld.so.conf。
  • -C CACHE:此选项指定生成的缓存文件,默认为 /etc/ld.so.cache。
  • -r ROOT:此选项改变应用程序的根目录为 ROOT(是调用 chroot 函数实现的)。
  • -p:打印出当前缓存文件所保存的所有共享库的名字。
  • -V:此选项打印出 ldconfig 的版本信息,而后退出。







​ldd 指令​​

​ ​ldd 命令的作用是打印可执行文件的共享库依赖关系。实际上 ldd 也是通过 ld-linux.so 实现的,例如:/lib/ld-linux.so.2 --list program 就相当于 ldd program。

$ ldd /bin/ls
  linux-vdso.so.1 =>  (0x00007ffdd83da000)
  libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f647ae43000)
  libcap.so.2 => /lib64/libcap.so.2 (0x00007f647ac3e000)
  libacl.so.1 => /lib64/libacl.so.1 (0x00007f647aa35000)
  libc.so.6 => /lib64/libc.so.6 (0x00007f647a667000)
  libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f647a405000)
  libdl.so.2 => /lib64/libdl.so.2 (0x00007f647a201000)
  /lib64/ld-linux-x86-64.so.2 (0x00007f647b06a000)
  libattr.so.1 => /lib64/libattr.so.1 (0x00007f6479ffc000)
  libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f6479de0000)

ldd 常用来解决程序因缺少某个库文件而不能运行的问题,例如上文中提到的例子。




reference


https://blog.51cto.com/u_15301988/5133605#_120