序言
这是文件系统的最后一节内容,在本篇文章中我们将介绍动静态库。相信在大家编程都多多少少会用到库,使用库确实极大地便利了我们的编程工作,不仅提高了开发效率,还使得代码更加模块化和可重用。
通过这篇文章的学习,我们将了解到什么是动静态库?是怎么实现的?背后的原理是什么?
1. 静态库
1.1 静态库的概念
静态库是指将一些公共代码编译成 库
文件,在链接步骤中,连接器(Linker
)会从这些库文件中取得所需的代码,并将它们 直接复制到生成的可执行文件中
。这种方式被称为静态链接(Static Linking
)。在内存中的图像如下:
1.2 静态库的特点
代码复制
:使用静态库时,库中的代码会被直接复制到最终的程序中。这意味着最终的可执行文件会包含所有必要的库代码,因此在运行时不需要再额外加载库文件。单一拷贝
:由于库代码被复制到可执行文件中,所以如果有多个程序使用了同一个静态库,那么在每个程序中都会存在该库代码的一份独立拷贝。这可能会导致最终的可执行文件体积较大
。更新困难
:如果静态库中的代码需要更新,那么所有使用了该库的程序都需要重新编译和链接
,以便包含新的库代码。这可能会增加维护成本。
静态库的操作相比于动态库是较简单的,直接将使用的库的代码拷贝到我们的程序中,但是这会导致我们的程序的体积较大,并且需要修改静态库时,利用了静态库的程序全都要重新写入一遍,消耗非常大!
1.3 静态库的实现
1. 前置工作
在这里我们尝试实现一下静态库,并且静态链接到我们的程序。首先我简单的实现了一个 Mymath.cc
文件:
1 #include "Mymath.h" 2 3 int Add(const int &left, const int &right){ 4 return left + right; 5 } 6 7 int Sub(const int &left, const int &right){ 8 return left - right; 9 }
还包含一个 Mymath.h
,来声明我实现了哪些函数以及头文件。
之后我实现了一个 Main.cc
函数,该函数主要是调用我们实现的函数:
Main.cc X 1 #include "Mymath.h" 2 3 int main(){ 4 int A = 1; 5 int B = 1; 6 7 std::cout << "A + B = " << Add(A, B) << std::endl; 8 9 return 0; 10 }
2. 编译打包
首先我们需要将我们实现的函数编译为 .o
的目标文件:g++ -c Mymath.cc
,之后我们将我们的所有 .o
文件打包。(在本篇文章中,只有一个 .o 文件,但是在实际的场景下,会包含很多个该文件
)
打包的指令是 ar rc libMymathc.a *.o
(在这里 rc
选项代表 replace and create
存在就覆盖,不存在就创建)。其实在这里我们就不难知道,所谓的库文件也就是很多的 .o
文件打包。注意:打包的静态文件一般都以 .a 结尾,以 lib 开头。
3. 静态库链接
现在我们有了 .h
文件并且将我们的所有 .o
文件打包成了一个 .a
文件,那该怎么使用呢?有两个方法:
安装到库
我们使用的所有 C/C++
的官方库函数头文件都存放在本地的仓库里,就比如所有头文件存放在 /usr/include
,而所有的打包好的库函数存放在 /usr/lib64
,当使用某个函数时会自动前往库函数中寻找。我们现在制造的库函数,想要使用也可以放入库函数中:放入头文件:sudo cp ./mylib/include/Mymath.h /usr/include/
放入包:sudo cp ./mylib/lib/libMylib.a /lib64
将自己的库函数放入到系统指定目录,其实这就是安装。
现在库也安装好了,是不是我们就直接可以 g++ Main.cc
编译程序形成可执行文件了呢?还不够,因为 g++ 默认是只认识 C/C++ 的官方库函数,不认识我们这种第三方库
,所以我们还需要告诉他我们使用了哪个第三方库:g++ Main.cc -l mylib
。(注意:我们库的名字需要去掉前缀 lib 和后缀 .a
),就可以啦!
指定搜索路径
我们的程序编译时会自动寻找使用的库函数,但是查找的路径只限于官方库函数的路径,所以我们可以指定搜索路径,我们需要使用到 g++
的新选项:
I
:指定头文件的路径L
:指定包的路径
然后我们的指令为:编译时指定搜索路径:g++ Main.cc -I ./mylib/include -L ./mylib/lib/ -l Mylib -static
是的,这个指令非常的长,我来为大家分段解释一下:
-I
:该选项后跟你头文件所在的目录,不需要指定具体使用了哪些头文件-L
:该选项后跟你包所在的路径,并且需要指定所用的那些包,使用-l
后跟着使用的包(命名和上一致)-static
:指名需要静态连接
所以说,大概就这两种方式来使用静态库。
2. 动态库
2.1 动态库的概念
动态库是一种 在程序运行时被加载和链接的库文件
。与静态库不同,动态库在编译时不会被直接复制到可执行文件中,而是在程序运行时 根据需要动态地加载到内存中
。在内存中的图像如下:
2.2 动态库的特点
运行时独立存在
:动态库在程序运行时被加载到内存中,而不是在编译时静态地链接到执行程序中。这意味着动态库可以作为独立的文件存在,并在多个程序之间共享。
代码复用与节省空间
:动态库允许多个程序共享同一份代码
,这种代码复用机制不仅减少了磁盘空间的使用,还减少了内存中的重复代码量,提高了系统的整体效率。
更新方便
:由于动态库是独立于程序存在的
,因此当动态库需要更新时,只需替换旧的动态库文件即可,而无需重新编译或链接依赖于该动态库的程序
。这使得软件的更新和维护变得更加方便和快捷。
2.3 动态库的实现
1. 前置工作
我们在这里和静态库复原一份代码,将该份代码分别制作动静态库。
2. 编译打包
首先,我们需要将该函数编译为 .o
的目标文件,和静态链接的不同在这里我们需要加上 -fpic
选项,所以指令是:g++ Mymath.cc -fPIC -c
。
之后,我们需要将所有相关的 .o
目标文件进行打包,打包的指令是:gcc -shared -o libMylib.so Mymath.o
。因为我们绝大部分制作的库都是动态库,所以 g++
直接提供构造方式。注意:打包的静态文件一般都以 .so 结尾,以 lib 开头。
3. 动态库链接
现在也打包好了,那该怎么使用我们制作的动态库呢?和静态库一样,要生成可执行文件,我们要么安装到官方库,要么指定搜索路径,我们选择后者:g++ Main.cc -I ./mylib/include -L ./mylib/lib -lMylib
,不错,没有任何报错,也生成了可执行文件,现在运行 ./a.out
:
ubuntu@VM-24-13-ubuntu:~/8_8$ ./a.out
./a.out: error while loading shared libraries: libMylib.so: cannot open shared object file: No such file or directory
咦,怎么出错啦,找不到该文件?
首先,我们理解静态库,当链接通过时,我们 静态库中的函数直接拷贝到了我们的程序中,但是动态库在链接时,只是让我们的程序和动态库建立某种关联,所以在运行时需要找到动态库,加载运行!!!
现在我们需要程序在运行时链接到动态库文件,该怎么办呢?当使用动态库时,编译器会自动到 /usr/lib64
文件下寻找该文件,所以我们可以采取:
将动态库文件安装到库中
这个方法在上面介绍过,在此不多介绍。
创建动态库文件的快捷方式到库中
不直接将我们的动态库文件放入系统的库中,将我们的动态库文件的软链接放入,系统依然可以找到:sudo ln -s /home/ubuntu/8_8/mylib/lib/libMylib.so /lib64/libMylib.so
更改搜索路径环境变量
系统在查找动态库时,是根据环境变量找到动态库的位置,所以我们可以将我们动态库所处的位置加载到环境变量中,
系统查找路径对应的环境变量是:LD_LIBRARY_PATH
添加方式为:LD_LIBRARY_PATH=$LD_LIBRARY_PATH:YOUR_PATH
但该添加是内存级别的,重启后会消失,若想要永久的,可更改配置文件,在此不多赘述。
完成以上三种任意一种后,就可以正常执行我们的动态库啦!
3. 动静态库的原理
3.1 静态库的原理
静态库的原理比较简单,主要就是链接器会从静态库中提取出被目标文件引用的函数和数据,并将它们复制到可执行文件中。因此,当程序运行时,它不再需要静态库文件,因为所有必要的代码和数据都已经被包含在了可执行文件中。
3.2 动态库的原理
静态库在编译时链接,而动态库是在 运行时链接
,这也反映了动态库的原理肯定是更难的。下面我将逐步中依次讲解如何链接,会涉及到底层,但是过底层的知识不会涉及。
1. 查找动态库
通过 进程地址空间 的学习,我们了解到 操作系统执行程序时通常是按照虚拟地址执行的,这些虚拟地址在执行过程中会通过页表转换到物理内存上
,所以当我们尝试调用动态库中的函数时,发现该函数并不存在于物理内存上,所以动态链接器 会查找相应的动态库文件,并将其加载到内存中。
内存上包含一块区域是专门管理存储动态库的,当程序需要某个动态库时,会在这块区域查找,如果未查找到,会将该动态库加载。
2. 将动态库加载到进程地址空间并建立映射
大家是否还记得我们的进程地址空间图像:
在堆栈之间存在一个区域叫做 — 共享区,所以 动态库中的函数和数据映射到进程的地址空间的共享区中
。现在动态库存在于进程地址空间中了,他有了虚拟地址;同时他也存在于物理内存中,他也有了物理内存地址。
最后就是将动态库的虚拟地址和物理内存地址通过页表建立映射关系。
3. 调用的动态库中的函数
现在准备工作都做好了,就等着别人来调用了。一个动态库里面可能实现了很多的函数,那怎么锁定程序需要函数的位置呢?
首先,我现在手里有了库函数在共享区的虚拟地址,其实我还有该函数在库中位置的偏移量(调用的函数名),采用 虚拟地址 + 偏移量
的方法就可以定位到我们需要函数的位置。
所以动态库可以映射到共享区的任何位置,因为尽管虚拟地址发生了变化,但是偏移量是一定的,采用 虚拟地址 + 偏移量
的方法总能定位到函数的位置。
4. 总结
在这篇文章中介绍了动静态库的实现,原理,以及制作,希望大家有所收获。😊