菜鸟笔记
提升您的技术认知

【Linux】动态库与静态库

动态库与静态库

  • 文件系统补完
    • 文件的三个时间acm
  • 动态库与静态库
    • 动态链接与静态链接
    • 静态库与动态库
      • 动静态库的对比
  • 生成静态库
    • 打包静态库
    • 使用静态库
  • 生成动态库
    • 打包动态库
    • 使用动态库

文件系统补完

文件的三个时间acm

我们通过stat指令查看文件信息:

[lyl@VM-4-3-centos 2022-3-14]$ stat log.txt 
  File: ‘log.txt’
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d	Inode: 790871      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/     lyl)   Gid: ( 1001/     lyl)
Access: 2022-03-13 22:18:13.571644848 +0800
Modify: 2022-03-13 22:18:13.571644848 +0800
Change: 2022-03-13 22:18:13.571644848 +0800
 Birth: -

可以看到在文件信息下有3个时间,这三个时间分别代表着:

  • Access:最近访问文件的时间
  • Modify:文件内容最近修改的时间
  • Change:文件属性最近修改的时间

    那么这三个时间有什么作用呢?
    我们在使用自动化构建工具Makefile时,如果连续make会发现:

    这便是由Modify与Change两个时间决定的,若Modify时间不早于Change,则gcc指令可以执行,否则会显示此时mytest is up 同date.

动态库与静态库

我们在实际开发中,经常要使用别人已经实现好的功能,这是为了开发效率和鲁棒性(健壮性);因为那些功能都是顶尖的工程师已经写好的,并且已经践行多年的代码。
那么如何使用他人开发的功能呢?可以使用:1.库,包括静态库与动态库。2.开源代码。3.基本的网络功能调用,比如各种网络接口、语音识别等等。
这其中,我们将详细介绍静态库和动态库。

动态链接与静态链接

一般情况下,为了更好的支持开发,第三方库或者是语言库,都必须提供静态库和动态库,这是方便程序员根据需要进行bin(二进制文件)的生成。
动态库是动态链接生成的,而静态库是静态链接生成的。
一般来说,我们直接gcc编译默认是动态链接的而如果加上-static选项,那么生成的可执行文件将为静态生成的
在使用-static选项时可能出现yum -y install glibc-static的报错,这其实是静态链接时没有找到libc.a。使用yum -y install glibc-static指令安装即可解决问题。


可以很明显的看到动态链接的文件大小明显要比静态链接的文件大小要小多了,这是为什么呢?
其实,动态链接是当执行到要调用的接口时,编译器会自动去搜寻所链接的库,而静态链接则是暴力的将所要用的库中可执行程序使用的二进制代码全部拷贝到我们生成的可执行文件中,这也就是为什么静态链接生成的文件这么大的原因了。

静态库与动态库

一般的命名方式为lib+库的名字+.a比如C语言提供的标准静态库名字就是libc.a
静态库是指程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
而动态库则是指程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表(头文件),而不是外部函数所在目标文件(.o)的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking),也就是说,动态链接是在需要调用接口时才会去将所用接口的二进制代码拷贝到内存中。
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
  • 这里需要提一下的是,我们之前所提过的进程地址空间中有一个共享区,而一般动态库的代码就映射在共享区,所有进程都共享着动态库的代码。

动静态库的对比

动态库被加载在内存中,可以供多个使用库的程序共享映射到自己的虚拟地址空间使用,因此可以减少页面交换以及降低内存中代码冗余,并且因为与源程序模块分离,因此开发模式比较好。
而加载动态库的程序运行速度相对较慢,因为动态库运行时加载,映射到虚拟地址空间后需要重新根据映射起始地址计算函数/变量地址。
静态库则与之相反,其运行速度相对较快,但消耗资源较多。

生成静态库

我们为什么会制作库呢?一般是想让别人能够使用我们实现的功能,但又不暴露自己的源代码才会打包库。那么接下来我们来学习如何打包静态库。

打包静态库

由于生成静态库需要先生成目标文件(.o)再进行打包,故先编写相应的源文件再将其编译成目标文件:

[lyl@VM-4-3-centos 2022-3-14]$ gcc -c add.c -o add.o #生成目标文件
[lyl@VM-4-3-centos 2022-3-14]$ gcc -c sub.c -o sub.o #生成目标文件

此时的add.o和sub.o文件是已经编译好但还没有链接的两个文件,此时再用 ar命令将其打包成静态库:

[lyl@VM-4-3-centos 2022-3-14]$ ar -rc libmycal.a add.o sub.o #打包成静态库

这里的ar是gnu归档工具,rc表示(replace and create)。

[lyl@VM-4-3-centos 2022-3-14]$ ar -tv libmycal.a #查看静态库的目录列表
rw-rw-r-- 1001/1001   1240 Mar 14 11:11 2022 add.o
rw-rw-r-- 1001/1001   1240 Mar 14 11:11 2022 sub.o
  • t:列出静态库中的文件
  • v:verbose 详细信息

使用静态库

[lyl@VM-4-3-centos 2022-3-14]$ cp ./*.h ./lib #将头文件复制到lib目录下(提供函数入口地址,使用接口的前提)
[lyl@VM-4-3-centos 2022-3-14]$ cp ./libmycal.a ./lib # 将静态库复制到lib目录下

既然已经打包好了静态库,让我们包一下头文件来调用我们实现的接口:

#include <stdio.h>
#include "add.h"
#include "sub.h"

int main()
{
  
  int a = 10;
  int b = 20;
  printf("a+b:%d\n", Add(a, b));
  printf("a-b:%d\n", Sub(a, b));
  return 0;
}

发现代码编译不过去,报错说函数Add及Sub未定义,我们明明已经实现了接口并打包成静态库了,这是为什么呢?
其实gcc编译时去链接库和头文件是去默认路径以及当前路径寻找,而我们将静态库打包到lib目录下,gcc编译时就找不到我们的库了,所以我们需要加一些选项来告知gcc去寻找指定路径的库及头文件。
gcc寻找的默认路径:

/usr/include


因此,正确链接的指令为:

gcc -o main main.c -I ./lib -L ./lib  -lmycal -static

其中:

  • -I(i的大写) + 指定路径:是指告知gcc除了默认路径,还要去寻找一下指定的路径的头文件
  • -L + 指定路径:指定库所在的路径。
  • -l(L的小写)+库名字:表示要具体链接的是哪一个库,因为指定目录下可能不止一个库,所以要指明库的名字。
    由此,我们就静态链接生成了一个可执行文件main,运行main程序结果如下:

    此时我们删除静态库libmycal.a,再运行main程序,发现程序照样可以运行。

生成动态库

学习完生成和使用静态库后,下面我们来生成一下动态库。

打包动态库

在这里,我们将生成动态库的依赖关系及方法写进自动化构建工具中:

需要注意的是:

  • 由于库在内存中是可加载的,它可能在内存中的任意位置,也可能被映射到进程地址空间的每个区域,所以为了保证库当中的代码执行不会出错,也就是要保证库中的代码是与位置无关的,因此生成.o文件时需要带上-fPIC选项表示生成与位置无关码。
  • 这里由于在依赖关系中已经点明了要生成的目标文件,故不带上$@也可以
  • 打包动态库不需要像静态库一样使用ar指令,直接用gcc即可,但是需要带上-shared选项表示生成共享库格式,这也体现了动态库代码映射在共享区的特点

此时make就制作好了动态库:

使用动态库

和静态库使用一样带上三个选项打包动态库:

这里我们在运行程序时可能会报错:

error while loading shared libraries
cannot open shared object file: No such file or directory

这是因为,此时的可执行程序已经与编译器无关了,这属于运行问题,运行时系统也需要找到我们所使用的库,但在默认路径下没有我们的库。
这里解决方法有多种,但我倾向于推荐下面这一种:
修改环境变量LD_LIBRARY_PATH,将动态库所在路径添加到该环境变量中,这样程序在运行时系统就能够找到动态库,从而运行成功。

[lyl@VM-4-3-centos dynamic]$ echo $LD_LIBRARY_PATH
:/home/lyl/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
[lyl@VM-4-3-centos dynamic]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/lyl/2022-3-14/dynamic/lib
[lyl@VM-4-3-centos dynamic]$ echo $LD_LIBRARY_PATH
:/home/lyl/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/lyl/2022-3-14/dynamic/lib

当然,让系统找到我们的动态库还可以拷贝.so文件到系统共享库路径下, 一般指/usr/lib。但是这可能会污染系统原生的库,一般不推荐这样做。
其次,还有一种方法,在我们的系统下有/etc/ld.so.conf.d/这个路径:

[lyl@VM-4-3-centos dynamic]$ ls /etc/ld.so.conf.d/
bind-export-x86_64.conf  dyninst-x86_64.conf  hcoll.conf  kernel-3.10.0-1160.11.1.el7.x86_64.conf  mariadb-x86_64.conf  mxm.conf  sharp.conf

我们可以把库所在的路径写进这个路径当中,可以在这个路径下制造自己的.conf,然后再将库路径写进这个conf中:

echo "/home/lyl/2022-3-14/dynamic" > /etc/ld.so.conf.d/lyl.conf

当然这个方法也不推荐,毕竟可能污染库的头文件和库。

好了,动态库和静态库的全部内容至此介绍完毕。