『Linux从入门到精通』第 ㉒ 期 - 动静态库-LMLPHP

💐专栏导读

💐文章导读

本章我们将深入学习Linux中动静态库的使用及其原理。

『Linux从入门到精通』第 ㉒ 期 - 动静态库-LMLPHP

🐧什么是库?

在学习生涯中,我们总是能谈到库,例如C语言标准库、C++标准库。那么到底什么是库呢?

在计算机科学中,术语“库”通常指的是库文件(Library),它是一组预编译的、可重用的代码和资源的集合,用于支持软件开发。库的目的是为开发人员提供一组常用的功能,以便在应用程序中进行调用,从而避免重复编写相同的代码。

库可以分为两大类:

  • 静态库(Static Library): 静态库在编译时被链接到应用程序中,形成一个独立的可执行文件。在程序运行时,静态库的代码被完全复制到应用程序中,因此应用程序不再依赖于原始的库文件。静态库的文件扩展名通常是.a(在Unix/Linux系统中)或.lib(在Windows系统中)。

  • 动态库(Dynamic Library): 动态库在运行时加载到内存中,多个应用程序可以共享同一个动态库的实例。这可以减小应用程序的大小,因为动态库的代码只需要存在一份,而且可以在运行时更新。动态库的文件扩展名通常是.so(在Unix/Linux系统中)或.dll(在Windows系统中)。

在编程中,开发人员通过包含库的头文件、链接库文件,以及在代码中调用库提供的函数或方法,可以轻松地利用库的功能。

在我们的Linxu机器上,系统已经为我们预装了C/C++的头文件和库文件。头文件提供方法说明,库文件提供方法的实现。头文件与库文件是有对应关系的,需要组合在一起使用。

一个可执行程序生成需要经历四个阶段:预处理、编译、汇编、链接。头文件在预处理阶段被引入,库文件在链接阶段被引入。

C/C++库文件

$ ls /usr/lib64/libc*
/usr/lib64/libc-2.17.so         /usr/lib64/libc_nonshared.a        /usr/lib64/libcroco-0.6.so.3.0.1    /usr/lib64/libc.so
/usr/lib64/libcap-ng.so.0       /usr/lib64/libcollection.so.2      /usr/lib64/libcrypt-2.17.so         /usr/lib64/libc.so.6
/usr/lib64/libcap-ng.so.0.0.0   /usr/lib64/libcollection.so.2.1.1  /usr/lib64/libcrypto.so.10          /usr/lib64/libcupscgi.so.1
/usr/lib64/libcap.so.2          /usr/lib64/libcom_err.so.2         /usr/lib64/libcrypto.so.1.0.2k      /usr/lib64/libcupsimage.so.2
/usr/lib64/libcap.so.2.22       /usr/lib64/libcom_err.so.2.1       /usr/lib64/libcryptsetup.so.12      /usr/lib64/libcupsmime.so.1
/usr/lib64/libcgroup.so.1       /usr/lib64/libcpupower.so.0        /usr/lib64/libcryptsetup.so.12.3.0  /usr/lib64/libcupsppdc.so.1
/usr/lib64/libcgroup.so.1.0.41  /usr/lib64/libcpupower.so.0.0.0    /usr/lib64/libcryptsetup.so.4       /usr/lib64/libcups.so.2
/usr/lib64/libcidn-2.17.so      /usr/lib64/libcrack.so.2           /usr/lib64/libcryptsetup.so.4.7.0   /usr/lib64/libcurl.so.4
/usr/lib64/libcidn.so           /usr/lib64/libcrack.so.2.9.0       /usr/lib64/libcrypt.so              /usr/lib64/libcurl.so.4.3.0
/usr/lib64/libcidn.so.1         /usr/lib64/libcroco-0.6.so.3       /usr/lib64/libcrypt.so.1

C/C++头文件

$ ls /usr/include/stdio.h
/usr/include/stdio.h
$ ls /usr/include/c++/4.8.5/iostream 
/usr/include/c++/4.8.5/iostream

🐧为什么要有库?

引入库的概念有很多重要的原因,它们有助于提高软件开发的效率、可维护性和可扩展性。以下是一些主要的原因:

  1. 代码重用: 库提供了一组通用的功能或工具,可以在多个项目中被重复使用。这样可以避免开发人员重复编写相同的代码,提高了开发效率。

  2. 模块化开发: 库使软件能够以模块化的方式构建。通过将不同的功能分解成独立的库,开发人员可以更容易地理解和维护代码。模块化开发还使得团队能够并行工作,每个成员专注于特定的任务或功能。

  3. 抽象和封装: 库提供了对底层实现的抽象,使开发人员可以专注于高层次的问题而不必关心底层的细节。这种抽象和封装的概念有助于隐藏复杂性,提高代码的可读性和可维护性。

  4. 提高可靠性: 库经过充分测试和验证,可以提供高质量的代码。开发人员使用库时,可以信任这些已经验证过的功能,减少了潜在的错误和漏洞。

  5. 快速开发: 使用库可以加速开发过程。通过利用现成的库,开发人员可以更快地构建应用程序,而不必从头开始编写每一个功能。

  6. 标准化: 某些库成为行业或社区标准,提供了一致的接口和实现。这种标准化有助于确保代码的一致性,同时使得不同项目之间更容易进行集成和交互。

  7. 可扩展性: 库使得软件的架构更具扩展性。通过将不同的模块组织为库,可以更容易地添加新功能、升级现有功能或替换特定的实现,而无需修改整个应用程序。

总体而言,引入库的概念有助于构建更可靠、可维护和可扩展的软件系统,提高了软件开发的效率和质量。

🐧写一个自己的库

为了更深入的理解库运作的原理,我们尝试自己写一个库,并交给其他小伙伴使用。

接下来,我们将实现一个加减运算的程序,并将给程序的源代码与头文件进行打包,并交给小伙伴——小黑使用。

$ touch add.c
$ touch add.h
$ touch sub.c
$ touch sub.h
/* add.h */
#pragma once 
int add(int a, int b);
/* add.c */
#include "add.h"
int add(int a, int b){
  return a + b;
}
/* sub.h */
#pragma once 
int sub(int a, int b);
/* sub.c */
#include "sub.h"
int sub(int a, int b){
  return a - b;
}

现在我们已经将计算器的源代码写好,现在我们想让小黑使用我们的成果,倘若我们直接把源文件以及头文件发给小黑,这种做法肯定是没问题的。

但是我们又想让小黑使用我们的成果,又不想让小黑看到我们的源代码,现在该怎么办呢?

🐦方法一

第一种方法是我们可以将源代码经过预处理、编译、汇编后形成二进制文件。小黑拿到该二进制文件后,再将它自己写的程序同样经过预处理、编译、汇编形成二进制文件,然后将两个二进制文件进行链接即可。

$ gcc -c *.c
$ ll
total 28
-rw-rw-r-- 1 hxy hxy   60 Feb 28 16:25 add.c
-rw-rw-r-- 1 hxy hxy   38 Feb 28 16:25 add.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 add.o
-rw-rw-r-- 1 hxy hxy   60 Feb 28 16:20 sub.c
-rw-rw-r-- 1 hxy hxy   39 Feb 28 16:19 sub.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 sub.o
drwxrwxr-x 2 hxy hxy 4096 Feb 28 16:40 xiaohei
$ cp *h xiaohei/
$ cp *o xiaohei/

/*小黑视角*/
ll
total 20
-rw-rw-r-- 1 hxy hxy   38 Feb 28 16:43 add.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:45 add.o
-rw-rw-r-- 1 hxy hxy  200 Feb 28 16:26 main.c
-rw-rw-r-- 1 hxy hxy   39 Feb 28 16:43 sub.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:45 sub.o
/*小黑视角*/
$ gcc -c main.c 
$ gcc -o test add.o sub.o main.o
$ ls
add.h  add.o  main.c  main.o  sub.h  sub.o  test
$ ./test 
10 + 3 = 13
10 - 3 = 7

🐦方法二 静态库

方法一中我们需要将许多的 .o文件以及.h文件打包给对方,这种做法明显感觉不是特别优雅。接下来我们就是用静态库的方式。

  1. 先将我们的.o文件打包生成一个静态库,并发送给小黑;
$ ar -rc libcalculate.a *.o
$ ll
total 32
-rw-rw-r-- 1 hxy hxy   60 Feb 28 16:25 add.c
-rw-rw-r-- 1 hxy hxy   38 Feb 28 16:25 add.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 add.o
-rw-rw-r-- 1 hxy hxy 2688 Feb 28 18:31 libcalculate.a
-rw-rw-r-- 1 hxy hxy   60 Feb 28 16:20 sub.c
-rw-rw-r-- 1 hxy hxy   39 Feb 28 16:19 sub.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 sub.o
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:23 xiaohei
$ cp libcalculate.a xiaohei

/* 小黑视角 */
$ ll
total 16
-rw-rw-r-- 1 hxy hxy   38 Feb 28 18:35 add.h
-rw-rw-r-- 1 hxy hxy 2688 Feb 28 18:33 libcalculate.a
-rw-rw-r-- 1 hxy hxy  200 Feb 28 16:26 main.c
-rw-rw-r-- 1 hxy hxy   39 Feb 28 18:35 sub.h

注意

这里我们需要注意库的命名规则。库的命名是以lib为开头,以.a或.so为结尾。例如 libcalculate.a 的真实名称为 calculate

在小黑拿到我们的库文件后,他就可以编译生成自己的程序了。但是这里有几个细节需要注意:

  • 因为我们的库是第三方的,编译器并不知道这个库的存在,所以我们需要指明库所在的路径;
  • 同样,我们需要告诉编译器该链接哪一个库;
  • 同理,我们还需指明头文件所在的路径。但是目前头文件就在当前路径下,所以可省略;

2.小黑进行编译链接;

/* 小黑视角 */
$ gcc -o test main.c -L . -l calculate -I .
$ ./test 
10 + 3 = 13
10 - 3 = 7
  • -L 选项:指明库所在的路径;
  • -l 选项: 告诉编译器链接哪一个库;
  • -I 选项:告诉编译器头文件的位置;

🐦标准化

上面方法二中我们演示了一个库文件的使用原理。在实际的项目开发中,我们并不会这么随意潦草。
再以小黑为例:

  1. 将头文件全部移至一个目录下;
  2. 将库文件全部移至一个目录下;
  3. 将头文件与库文件进行打包;
  4. 将打包好的文件上传至云端;
$ mkdir lib
$ mkdir include
$ cp *.h include/
$ cp *.a lib
$ tar -czf calcuate.tgz include lib

远在海外的小黑想用我们写好的库,于是在云端将压缩包下载到了本地;

/* 小黑视角 */
$ ll
total 8
-rw-rw-r-- 1 hxy hxy 788 Feb 28 19:01 calcuate.tgz
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c

小黑将它进行解压看到了头文件与库文件;

$ tar xzf calcuate.tgz 
$ ll
total 16
-rw-rw-r-- 1 hxy hxy  788 Feb 28 19:01 calcuate.tgz
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 include
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 lib
-rw-rw-r-- 1 hxy hxy  200 Feb 28 16:26 main.c

最后小黑进行了编译链接等一系列操作,成功运行了自己的程序;

$ gcc -o test main.c -I ./include -L ./lib -l calculate
$ ./test 
10 + 3 = 13
10 - 3 = 7

以后的小黑会经常用到这个库,但是他觉得每次都要写这么长的指令有些麻烦。于是他将这个库的头文件全部移至系统的/usr/include目录下;将库文件移至/usr/lib目录下;

$ sudo cp include/*.h /usr/include/
$ sudo cp lib/* /lib64/

以后他每次使用这个库时,编译器会自动在这两个目录下寻找所程序所依赖的头文件与库文件;

$ gcc -o test main.c -lcalculate
$ ./test 
10 + 3 = 13
10 - 3 = 7

🐦方法三 动态库

以上我们尝试将自己的源文件制作为一个静态库供小黑使用,接下来我们在尝试制作一个动态库。

$ gcc -fPIC -c add.c sub.c
  • fPIC:产生位置无关码(position independent code);
gcc -shared -o libcalculate.so *.o
  • shared: 表示生成共享库格式;

接着我们把生成的.so文件放在lib目录下,将.h文件放到include目录下,并打包发给小黑(重复之前的操作)。

$ rm lib/libcalculate.a
$ rm calcuate.tgz 
$ cp *.so lib
$ tar -czf calculate.tgz lib include
$ cp calculate.tgz xiaohei/

小黑拿到压缩文件,解压后得到libinclude于是用我们的库来链接自己的程序。

/*小黑视角*/
$ tar xzf calculate.tgz 
$ ll
total 16
-rw-rw-r-- 1 hxy hxy 2343 Feb 29 16:39 calculate.tgz
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 include
drwxrwxr-x 2 hxy hxy 4096 Feb 29 16:38 lib
-rw-rw-r-- 1 hxy hxy  200 Feb 28 16:26 main.c
$
$ gcc -o test main.c -I include -L lib -lcalculate
$ ll
total 28
-rw-rw-r-- 1 hxy hxy 2343 Feb 29 16:39 calculate.tgz
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 include
drwxrwxr-x 2 hxy hxy 4096 Feb 29 16:38 lib
-rw-rw-r-- 1 hxy hxy  200 Feb 28 16:26 main.c
-rwxrwxr-x 1 hxy hxy 8432 Feb 29 16:49 test
$
$ ./test 
./test: error while loading shared libraries: libcalculate.so: cannot open shared object file: No such file or directory

小黑发现了事情的不妙,心想刚才不是还好好的吗?怎么运行时提示找不到库文件呢?

原来是因为在程序运行时,calculate.so并没有在系统的默认路径下,所以OS找不到!那么如何才能让OS找到我们的库呢?答案是需要我们自己来配置。

🐦配置动态库

配置动态库有三种方法:

  1. 环境变量:LD_LIBRARY_PATH (临时方案);
  2. 软链接方案;
  3. 配置文件方案

🐱环境变量

导入环境变量LD_LIBRARY_PATH:

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/..../lib/ # 你的.so文件存放路径

查看环境变量LD_LIBRARY_PATH:

$ echo $LD_LIBRARY_PATH
:/usr/local/protobuf/lib/:/home/hxy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/..../lib/ # 你的.so

现在再来运行程序就可以成功运行了:

$ ./test 
10 + 3 = 13
10 - 3 = 7

🐱软链接

上面我们说在程序运行时,calculate.so并没有在系统的默认路径下,所以OS找不到我们的库,那么这个默认路径在哪里呢?

# 一般在这两个路径下
$ /lib
$ /lib64/

所以我们直接将库文件移动到这两个路径下也可以,但是还有比较优雅一点的方案,那就是为我们的库文件建立软链接。

$ sudo ln -s lib/libcalculate.so /lib64/libcalculate.so
$ ./test 
10 + 3 = 13
10 - 3 = 7

🐱配置文件

  1. 在系统的 /etc/ld.so.conf.d/ 目录下创建一个配置文件;
$ sudo touch /etc/ld.so.conf.d/calculate.conf
  1. 将动态库所在路径写入配置文件;
$ sudo vim /etc/ld.so.conf.d/calculate.conf
$ sudo cat /etc/ld.so.conf.d/calculate.conf
/home/.../lib # 你的.so文件路径
  1. 让配置文件生效;
$ sudo ldconfig

现在小黑的程序也能成功运行了;

$ ./test 
10 + 3 = 13
10 - 3 = 7

🐧静态库与动态库的区别

以上我们通过制作一个自己的静态库,对库文件有了基础的了解。那么库为何又要分为静态库与动态库呢?二者有何区别?

  1. 链接时机和方式:

    • 静态库: 在编译时被链接到目标程序中,链接器将库的代码和数据拷贝到最终的可执行文件中。因此,可执行文件在运行时独立于库文件。
    • 动态库: 在编译时并不直接链接到目标程序,而是在运行时由操作系统动态加载到内存中。动态库的链接发生在程序启动时(静态加载)或在运行时(动态加载)。
  2. 文件大小和内存占用:

    • 静态库: 链接时会将库的代码和数据完全复制到目标程序中,可能导致可执行文件较大。每个使用该库的可执行文件都包含一份库的拷贝。
    • 动态库: 多个程序可以共享同一个动态库的实例,因此相同的库只需要在内存中存在一份,可以减小程序的大小。
  3. 更新和维护:

    • 静态库: 如果库的代码或数据发生变化,需要重新编译并重新链接所有使用该库的程序。每个程序都需要更新以包含最新版本的库。
    • 动态库: 如果库的代码或数据发生变化,只需要替换库文件而无需重新编译和链接使用该库的程序。这使得动态库更容易更新和维护。
  4. 跨平台兼容性:

    • 静态库: 可执行文件与库的链接是在编译时完成的,因此在不同平台上可能需要不同版本的库。
    • 动态库: 由于动态库的加载是在运行时由操作系统完成的,因此相同的动态库文件可以在多个平台上使用。
  5. 运行时灵活性:

    • 静态库: 执行文件在编译时固定了对静态库的依赖,无法在运行时更改。
    • 动态库: 可以在运行时加载或替换动态库,这使得系统更加灵活。

🐧动态库的运作原理

🐔为什么进程可以在运行时加载动态库?

我们知道每个程序在运行时就变成一个进程,一个进程拥有自己的虚拟地址空间。

在程序运行时,我们只需要将库加载到内存当中,经过页表映射到进程的地址空间中,我们的代码执行库中的方法就依旧还是在自己的地址空间中进行函数跳转。

🐔为什么多个进程可以共享一个动态库

当多个进程同时运行时,按照同样的方式,将库中的地址映射到每个进程的地址空间中,那么如果每个程序使用的地址都是相同的,不会产生冲突吗?

还记得我们在用 gcc 生成动态库时用到的参数 - fPIC 吗?

-fPIC 是 GCC 编译器选项,用于生成位置无关码(Position Independent Code,PIC)。位置无关码是一种在内存中加载时不依赖于特定内存地址的机器码,通常用于共享库(动态链接库)的编译。

具体来说,使用 -fPIC 选项的目的是允许将生成的目标文件用于共享库,而这些库可以被多个进程加载到内存的不同地址上,而不会发生地址冲突。

它主要特点包括:

  1. 位置独立性: 生成的代码不依赖于特定的内存地址,可以在不同的内存地址空间中运行。这对于动态链接库是必要的,因为它们可能在不同的进程中加载并映射到不同的地址。

  2. 全局偏移表(Global Offset Table,GOT): 在运行时,PIC 代码使用全局偏移表,其中包含指向全局和共享库中的符号的指针。这些指针在加载库时进行重定位,以便正确地找到符号的位置。

  3. 避免绝对地址: 使用相对寻址或基于 GOT 的寻址,而不是绝对地址。这使得代码更容易在不同的地址空间中重定位。

本章的内容到这里就结束了!如果觉得对你有所帮助的话,欢迎三连~

『Linux从入门到精通』第 ㉒ 期 - 动静态库-LMLPHP

03-04 08:47