前言
在软件开发中,库(Library)是一种方式,可以将代码打包成可重用的格式,供其他程序调用。库可以分为静态库(StaticLibraries)和动态库(Dynamic Libraries 或 Shared Libraries)。这两种类型的库在链接和执行时有各自的特点和用途。本篇文章将围绕动静态库的原理及其使用展开讲解。
实验代码样例
/add.h/ #ifndef __ADD_H__ #define __ADD_H__ int add(int a, int b); #endif // __ADD_H__ /add.c/ #include "add.h" int add(int a, int b) { return a + b; } /sub.h/ #ifndef __SUB_H__ #define __SUB_H__ int sub(int a, int b); #endif // __SUB_H__ /sub.c/ #include "add.h" int sub(int a, int b) { return a - b; } ///main.c #include <stdio.h> #include "add.h" #include "sub.h" int main( void ) { int a = 10; int b = 20; printf("add(%d, %d)=%d\n", a, b, add(a, b)); a = 100; b = 20; printf("sub(%d,%d)=%d\n", a, b, sub(a, b)); }
解释以上代码:
该代码分为三个部分,一个是main函数,一个是add函数的声明与定义文件,一个是sub函数的声明与定义文件。
静态库
静态库是一种在程序编译时就被整合到可执行文件中的代码和数据集合。一般来说,无论是动态库还是静态库,其库中的内容都是一些被编译过但是还未被链接的目标文件(以.o或者.obj结尾的二进制文件)。
为什么要将这些目标文件打包成库呢?
- 首先就是便于代码复用。将常用的功能打包成库使得这些代码可以轻松的在多个项目中重用。这大大节省了开发的时间,也有助于提高代码的一致性。
- 使项目设计变得更加模块化。需要什么功能就添加什么库,将整个项目分解为更小更容易管理的模块。每个模块的库执行特定的功能,通过提供接口与其它模块交互。这有助于提高代码的维护性与可读性。
- 版本控制和兼容性。库可以独立于使用它们的应用程序进行版本控制。开发者可以对库进行更新和改进。对于我们使用者来说,只需要更新一下就能使用最新的库了。
- 团队协作。库允许不同的开发者或小组专注于特定的功能领域。例如,一个团队可以负责数据库交互的库,而另一个团队则可以专注于用户界面的组件。这样的分工促进了专业化,可以提高开发效率和产品质量。
生成一个静态库
根据库的特点,我们需要先将add.c文件和sub.c文件编译成目标文件,使用带-c
选项的gcc指令:
接下来使用归档工具 ar
命令将一组对象文件打包成一个库文件。具体使用方式如下:
ar -rc libmymath.a add.o sub.o
其中,-rc
是参数,表示替换、创建。libmymath.a
表示要生成的库文件(一般静态库是以.a为后缀)。add.o
、sub.o
则是输入的对象文件。
用指令ar -tv
查看库中的目录列表
选项t
表示列出静态库中的文件,v
表示详细信息。
归档工具ar
ar是一个用于创建和管理归档文件的工具,通常用于创建静态库。生成的归档文件是一个单独的文件,用来存储多个其它的文件和目录,常常在编译链接阶段用于组织静态库中的对象文件(.o文件)。ar来源于archive(归档),其主要功能就是把多个文件合并成一个文件,以便于管理。
虽然我们可以用-tv
选项查看归档文件中的目录,但它本身并不是一个目录,只是看起来像而已!此外,ar的功能其实与zip类似,但是ar对于处理这种目标文件是非常有效的。
给出ar指令的选项功能:
- r: 插入文件到归档中(如果归档已存在,这个选项会替换或添加文件)。
- c: 创建归档文件,如果它不存在。
- s: 创建一个对象文件索引(符号表),这对于链接器加速访问归档中的目标文件很重要。
- t: 列出归档内容。
- x: 从归档中提取文件。
- d: 从归档中删除文件。
- u: 只有当被添加的文件比归档中现有的同名文件更新时才添加文件。
总之,区别于我们用gcc一个一个链接目标文件,这种打包成库的方式简化了链接和构建的过程,显得非常的方便且灵活。
静态库的链接
当创建可执行文件时,如果程序依赖于某个静态库,链接器(linker)会将静态库中的相关对象文件整合到最终的可执行文件中。回到上面的代码样例。我们的main.c文件依赖于add与sub函数的实现,如果不链接库。按照我们之前的方式,只能一个一个链接源文件:
而现在我们已经将add.o与sub.o打包成了静态库libmymath.a,该怎么使用呢?
考虑使用以下指令:
gcc main.o -L/path/to/library -lexample -o main
- 其中
-L/path/to/library
告诉链接器在哪个目录下查找库文件。如果库文件在标准库路径下例如 /usr/lib/ 或 /usr/local/lib/,可以省略这个选项。 -lexample
指定链接器使用名为libexample.a
的库。注意这里的使用命名规则,并不是直接将库静态库的全名加上去,而是要进行一些“处理”。因为链接器会自动寻找以lib
开头,.a
结尾的文件。我们只需要提供去掉lib和.a的部分。-l
选项表示指定添加库。
库搜索路径:
- 从左到右搜索-L指定的路径
- 由环境变量指定的目录 (LIBRARY_PATH)
- 标准库路径 例如 /usr/lib/ 或 /usr/local/lib/
所以针对样例代码,我们可以这样链接静态库:
这样我们就成功的链接了一个静态库。
值得注意的是,一旦我们成功链接了某个静态库之后,该静态库中的所有数据和代码就存在可执行文件中了。所以我们之后即使把静态库删除也不会影响到程序的执行。这是一个一次性的过程。
动态库
动态库(Dynamic Libraries)是现代软件开发中常用的一种资源共享和模块化技术。动态库能够使多个程序共享同一份库代码,而不需要将这些代码复制到每个程序的可执行文件中,从而节省系统资源并便于维护和更新。
与静态库不同的是,动态库被链接后存在于进程的共享区域内,该区域内的数据和代码可以供多个进程使用。而且是用时访问,即程序在运行阶段会不断地访问。也就意味着,如果我们在生成可执行文件之后,将动态库删除,程序便会报链接错误。这是和使用静态库不一样的地方。
我们可以观察动态库在程序的内存分布:
其中动态库在内存的数据共享区,该区域于其它进程共享数据。而静态库中的数据被链接之后就成为了代码段与数据段的一部分。
创建动态库
创建静态库使用了归档工具ar,而创建一个动态库gcc工具就可以。
考虑以下指令:
gcc -fPIC -c sub.c add.c
gcc -shared -o libmymath.so sub.o add.o
首先解析第一条指令:
选项-fPIC
表示生成位置无关码。
什么叫位置无关码?
位置无关码的意思就是,生成的代码在内存中可以被加载到任何位置,而不是某个固定的地址。这种特性对于动态库尤其重要,因为动态库需要能够被多个不同的程序共享,并且每个程序可能将库加载到不同的地址空间。
位置无关码的寻址方式为相对寻址,可以将代码中的所有指针认为是一个个偏移量,不同的程序给与它不同的初始地址,这样就能灵活的将库加载到其它的地方。
生成位置无关码是创建动态库的标准做法,因为它确保库能在不同的应用程序和不同的运行实例中被正确地共享和使用。如果我们不用-fPIC选项生成位置无关码,程序在运行时就会报错。
所以第一条指令的意思就是将sub.c和add.c文件分别经过编译生成目标文件。
接下来就是将这些目标文件打包成库。
分析第二条指令:
选项-shared
表示 链接器 生成一个动态库而不是默认的可执行文件。生成的动态库文件一般以.so
结尾。
于是呢,经过这两条指令我们就得到了一个libmymath.so动态库.
加载动态库
当我们已经通过gcc编译器得到了一个动态库,该如何使用这个动态库呢?
如果我们直接像使用静态库那样直接链接,确实能编译通过,但是一旦我们尝试运行,就会得到以下报错:
为什么报错显示找不到这个动态库呢?我们不是在链接动态库的时候告诉了编译器动态库的路径了吗?
这一点非常容易理解。
我们确实将动态库路径告诉了gcc编译器,也确实编译成功了,得到了一个可执行文件。但是一个动态库是需要在运行时被访问的!编译器能找到这个动态库并不表示操作系统能找到这个动态库!
那为啥静态库这样运行就没问题呢?这是因为静态库是一次性工程,在链接阶段就把所有的代码和数据拷贝到程序的内部了!往后这个静态库文件在哪已经无所谓了。
所以,对于一个动态库而已,链接一个动态库的时候要告诉编译器库在哪,运行的时候就要告诉操作系统在哪。
解决方法:
运行时,操作系统会默认在/lib
(不同的操作系统名字可能不一样)这个目录下去找动态库
1.将我们自己的动态库拷贝到/lib
里面,就能成功运行了:
2.在/lib
目录下创建一个动态库的软链接:
这样操作系统也能通过软链接找到我们的动态库
3.修改动态库默认路径的环境变量LD_LIBRARY_PATH
:
考虑以下指令:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
其中/usr/local/lib
表示库路径
但是这样设置的环境变量在下一次登录就失效了,要想永久生效就得修改配置文件.bashrc
:
找到LD_LIBRARY_PATH
配置项,并在其路径下添加库的路径,这样每次登录都会自动生效。
如果你发现你的./bashrc
没有LD_LIBRARY_PATH
那你可以自己手动加一个环境变量LD_LIBRARY_PATH
过去:export LD_LIBRARY_PATH=/path/to/your/libs:$LD_LIBRARY_PATH
动静态链接
静态链接
静态链接是在程序编译时将所有需要的库文件(通常是 .a 或 .lib 文件)内容直接复制到最终的可执行文件中的过程。这样,程序在运行时不再需要任何外部库。注意与静态库的区别,链接是一个动作,而库是一个名称。我们常把链接静态库的过程称为静态链接。
值得注意的是,gcc默认是动态链接。我们可以用指令file观察到这一点。
那如何使gcc静态链接目标文件呢?
使用-static选项:gcc -o main main.c -L. -lmth -static
再使用file观察,发现提示该可执行文件不是一个动态链接文件
这里可能会有人有疑问,为什么之前我们用的是静态库,默认是动态链接,还显示是动态链接呢?
如果不显示使用-static选项,系统会只将声明的这个库进行静态链接,其它的库就还是动态链接。
比如我们之前的gcc -o main main.c -L. -lmth
,假如这个mth.a是一个静态库,那么就只会静态链接这个库,其它库都是动态链接。
动态链接
动态链接是编译过程中,程序被构建为在运行时加载外部共享库(如 .so 或 .dll 文件)的过程。这意味着程序在运行时依赖于这些库文件。动态链接一般用来链接动态库。
gcc默认就是动态链接,因此无需其它选项。值得注意的是,如果我们声明的库路径下面包含静态库和动态库,即同名的库,只是后缀不一样。gcc还是会优先考虑链接动态。
动静态链接的优缺点
优点:
- 静态链接生成的可执行文件包含了所有必要的代码,不依赖于外部的库文件,这使得部署更简单,只需要分发单一的可执行文件。
- 静态链接性能快。在某些情况下,静态链接的程序启动速度比动态链接的程序快,因为它们在启动时不需要加载外部库。
- 动态链接节省空间:多个程序可以共享同一个库的单一物理副本,这样可以显著减少系统的总体占用空间。
- 动态链接更新方便,不需要重新编译,只需要替换库文件即可。
缺点:
- 静态链接的可执行文件通常比动态链接的文件大,因为它包含了所有必要的库代码。
- 静态链接更新比较麻烦,需要重新编译整个程序。
- 动态链接过于依赖外部的动态库,一旦外部的库出现问题,导致很多程序都运行不了。
- 动态链接的性能开销会比较大,因为需要加载外部的库。
总的来说,动态链接是用时间换空间,静态链接是用空间换时间,如何选择哪种链接方式取决于具体的环境。