目录
十三.动态库与静态库
13.1 认识动静态库
13.2 深入理解动静态库
什么是库?
编译链接过程
动静态库的基本原理
13.3 静态库
静态库的打包:
静态库的使用:
13.4 动态库
动态库的打包:
动态库的使用:
13.5 动态库与静态库怎么选?
十三.动态库与静态库
13.1 认识动静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文 件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚 拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
- 在Linux当中,以
.so
为后缀的是动态库,以.a
为后缀的是静态库。 - 在Windows当中,以
.dll
为后缀的是动态库,以.lib
为后缀的是静态库。
ldd命令可以查看一个可执行程序依赖的共享库
ldd 可执行程序名
gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带 -static 选项:
# 动态链接
gcc -o my_program my_program.c
# 静态链接
gcc -o my_program_static my_program.c -static
13.2 深入理解动静态库
什么是库?
在计算机编程中,库(Library)是一组已经编写好的代码、函数和例程的集合,可以供程序员在自己的程序中重复使用。这些库中的代码通常提供了一些通用的功能,例如数据结构、算法、输入/输出操作等,以便开发人员能够更容易地实现特定的任务,而不必从头开始编写所有的代码
编译链接过程
我们都知道C/C++程序从源代码到可执行程序会经历下面四个步骤:
- 预处理 (Preprocessing):
在这一阶段,预处理器会对源文件进行处理,展开头文件(#include指令),执行宏替换,去除注释,处理条件编译(#if、#ifdef等),生成一个被称为预处理后文件(通常具有.i扩展名)。- 编译 (Compiling):
编译器接收预处理后的文件,进行词法分析、语法分析和语义分析,生成相应的汇编代码。这个阶段的输出是一个汇编文件(通常具有.s扩展名)。- 汇编 (Assembling):
汇编器接收编译生成的汇编代码,将其转换为机器语言(二进制代码)并生成目标文件(通常是.o文件)。目标文件包含与源文件对应的机器代码。- 链接 (Linking):
链接器接收一个或多个目标文件,以及可能的库文件,将它们组合在一起生成最终的可执行文件。链接的过程包括符号解析、地址绑定和重定位等步骤,最终生成可执行文件。
动静态库的基本原理
而动静态库的区别其实主要体现在程序链接 (Linking)阶段
静态库的链接阶段:
完全合并到可执行文件: 在链接阶段,静态库的目标文件会被完全合并到最终的可执行文件中。
可执行文件独立: 最终生成的可执行文件包含了程序的所有代码和数据,独立于外部库文件。
链接时解析符号: 编译器在链接时会将程序中引用的符号与静态库中的符号进行解析和匹配,将所有符号解析为具体的地址。
生成独立的可执行文件: 链接阶段生成的可执行文件是一个独立的二进制文件,不需要依赖外部库文件。
动态库的链接阶段:
引用和延迟加载: 在链接阶段,编译器并不将动态库的目标文件合并到可执行文件中。相反,它在可执行文件中留下对动态库的引用。
生成未定义符号: 编译器生成一个包含未定义符号引用的可执行文件,这些符号在运行时将由动态链接器解析。
符号解析延迟到运行时: 实际的链接和加载是在程序运行时由操作系统的动态链接器完成的。动态链接器负责加载和链接动态库。
生成依赖于动态库的可执行文件: 可执行文件包含有关如何在运行时加载和链接动态库的信息,但并不包含动态库的实际代码。
总体而言,静态库在链接阶段就将库的代码完全合并到可执行文件中,而动态库的链接是延迟到运行时进行的。这是为什么动态库能够实现共享和动态加载的原因。
光说不练假把式,下面我们在实操中进一步体会!
13.3 静态库
首先,创建一个项目目录,例如 calculator_project,在其中创建以下文件:
创建文件结构:
calculator_project/
|-- src/
| |-- add.c
| |-- multiply.c
| |-- main.c
|-- include/
| |-- calculator.h
|-- lib/
静态库的打包:
编写源文件:
编写 add.c:
// add.c
int add(int a, int b) {
return a + b;
}
编写 multiply.c:
// multiply.c
int multiply(int a, int b) {
return a * b;
}
编写头文件:
编写 calculator.h:
// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b);
int multiply(int a, int b);
#endif
编译源文件 :
这时候回到我们的回到calculator_project目录,使用以下命令编译源文件生成目标文件:
gcc -c src/add.c -o lib/add.o
gcc -c src/multiply.c -o lib/multiply.o
这将分别编译 add.c、multiply.c 并将生成的目标文件保存在 lib 目录中
创建静态库:
我们进入lib目录,使用以下命令创建静态库 libcalculator.a:
ar -rcs libcalculator.a add.o multiply.o
ar 是GNU下一个用于创建和操作静态库(archive)的工具,而 rcs 是 ar 工具的一组选项,用于创建、替换或显示归档文件。
具体而言,ar rcs 命令的含义是:
-r(replace):用于将指定文件插入到归档文件中。如果文件已经存在于归档中,则替换掉原有的文件。
-c (create): 创建归档文件。如果归档文件不存在,则创建一个新的归档文件。
-s (index symbol table): 创建索引。创建归档文件的索引,这样可以更快地查找文件。
综合起来,ar rcs 命令通常用于创建或更新静态库的归档文件,将新的目标文件插入到归档中,并为归档文件创建索引。
静态库的使用:
编写 main.c:
// main.c
#include <stdio.h>
#include "calculator.h"
int main() {
int result_add = add(5, 3);
int result_multiply = multiply(4, 2);
printf("Addition result: %d\n", result_add);
printf("Multiplication result: %d\n", result_multiply);
return 0;
}
编译源文件:
在src目录,使用以下命令编译源文件生成目标文件:
gcc -c main.c -o ../lib/main.o -I ../include
编译可执行文件:
使用以下命令编译可执行文件:
gcc lib/main.o -o calculator -L lib -lcalculator
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
运行程序:
运行生成的可执行文件:
13.4 动态库
关于动态库,我们会基于静态库的实现来进一步来讲解
动态库的打包:
编译源文件 :
这时候回到我们的回到calculator_project目录,使用以下命令编译源文件生成目标文件:
gcc -c -fPIC src/add.c -o lib/add.o
gcc -c -fPIC src/multiply.c -o lib/multiply.o
-fpic 是 GCC 编译器的一个选项,用于生成位置无关代码(Position Independent Code,PIC)。位置无关代码是一种编译生成的代码,可以在内存中的任何位置执行而无需修改。这对于动态链接库(共享库)非常重要,因为共享库可以加载到内存的任何位置。
具体来说,-fpic 选项的作用包括:
- 生成相对地址: 使用 -fpic 选项编译生成的代码中,引用地址都是相对地址而不是绝对地址。这使得代码可以在不同的内存位置正确执行。
- 用于共享库: 这个选项通常用于编译动态库,确保生成的库是位置无关的。
如果在创建动态库时不加 -fPIC 选项,会导致生成的目标文件包含绝对地址的引用。这样的目标文件是非位置无关的,即在加载时必须进行重定位以适应不同的内存地址
每个进程都需要独立的重定位: 因为库中的代码包含绝对地址的引用,加载到内存的每个进程都需要对代码段进行独立的重定位,以适应不同的内存地址。这意味着每个进程加载库时,都需要修改代码段的内容,以反映加载到的特定地址,从而创建独立的、可重定位的拷贝。
无法在多个进程之间共享: 由于每个进程都需要对代码段进行独立的重定位,这导致了每个进程都会维护一个独立的、特定于该进程的库拷贝。因此,这样的库不能在多个进程之间共享,因为每个拷贝都是特定于加载它的进程的。
内存占用可能更大: 每个进程都拥有自己的独立拷贝,这可能导致内存占用更大。相比之下,位置无关的库可以在多个进程之间共享,从而减少内存占用。
创建动态库:
我们进入lib目录,使用以下命令创建静态库 libcalculator.so:
gcc -shared -o lib/libcalculator.so lib/add.o lib/multiply.o
与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。
动态库的使用:
编译可执行文件:
使用以下命令编译可执行文件:
gcc lib/main.o -o calculator_dynamic -L lib -lcalculator
运行程序:
运行生成的可执行文件:
这时候我们会惊讶的发现动态链接生成的程序不能像静态链接一样直接运行
这时候我们看一下系统的报错,发现提示我们找不到链接的libcalculator.so库!
可是我们使用-I
,-L
,-l
这三个选项都是在编译期间已经告诉过编译器我们使用的头文件和库文件在哪里以及是谁,那么,在执行的时候是如何定位动态库文件的呢?
这时候我们使用ldd命令来查看程序的动态链接依赖关系时,我们发现c语言的动态库都能找到所在的位置,而我们自己编写的动态库却not found.
原来,系统中存在环境变量LD_LIBRARY_PATH ,用于指定动态链接器在运行时查找共享库的路径。当你运行一个程序时,系统的动态链接器会根据该变量的设置来搜索共享库的位置。
基于此我们可以有一下几种方法来使我们的程序正常执行
方法一:直接更改LD_LIBRARY_PATH
我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中即可。
我们先通过pwd命令得到我们自己lib目录的绝对地址:
这时候我们再将我们得到的库的地址(记作path)添加到LD_LIBRARY_PATH环境变量当中
export LD_LIBRARY_PATH= path:$LD_LIBRARY_PATH
这里我加上自己库的path后运行程序
方法二:将目录拷贝到系统的动态库目录下
同上,我们先通过pwd命令得到我们自己lib目录的绝对地址:
将目录中的所有共享库文件拷贝到 /usr/lib,这是系统默认的动态库目录之一。你可能需要使用 sudo 权限来执行这个命令
sudo cp path/*so /usr/lib
不推荐将自己写的库文件拷贝到系统路径下,这样做会对系统文件造成污染,该方法仅做演示
方法三:配置/etc/ld.so.conf.d/
我们可以通过配置/etc/ld.so.conf.d/的方式解决该问题,/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。 /etc/ld.so.conf.d/ 目录下的 .conf 文件来管理库文件的路径是一种更规范的方式。这样做可以使系统更清晰地了解到库文件的位置,并且不直接修改系统默认库目录,减小了对系统的影响。
创建一个 .conf 文件:
在你的库文件所在目录创建一个以 .conf 为后缀的配置文件,比如my_library.conf,并将库文件所在目录的路径存入
将库文件所在的路径写入配置文件中:
将 .conf 文件拷贝到 /etc/ld.so.conf.d/ 目录下:
sudo cp my_library.conf /etc/ld.so.conf.d/
这样系统会自动读取该目录下的配置文件。
使用 ldconfig更新配置:
sudo ldconfig
这个命令会重新加载配置文件,确保系统动态链接器能够找到新的库文件路径。 此时,系统应该能够正确找到你的库文件,你的可执行文件也应该能够正常运行了。
这是一种更安全和规范的方式,尤其在共享库比较多或者与其他应用程序存在依赖关系时非常有用。
13.5 动态库与静态库怎么选?
静态库的优势:
- 独立性: 静态库会将库的代码嵌入到可执行文件中,使得程序在运行时不再依赖外部的库文件。这使得静态库的使用更为简单,因为用户不需要担心库文件的版本问题。
- 性能: 静态库在编译时就已经链接到可执行文件中,因此在运行时不需要进行额外的加载和链接操作,有助于提高程序的启动速度。
动态库的优势:
- 共享性: 动态库可以被多个程序共享使用,这有助于减小可执行文件的大小,因为多个程序可以共同使用同一个动态库,而不是每个程序都包含一份库的拷贝。
- 更新维护: 如果库需要升级或修复 bug,只需更新动态库而无需重新编译所有依赖于它的程序。这降低了维护成本。
- 节省内存: 动态库在内存中只需要加载一次,多个程序可以共享同一份库的实例,因此可以减少内存的占用。
选择建议:
- 静态库: 适用于小型项目或独立的工具,可以简化部署和分发,避免依赖管理的复杂性。特别是对于一些简单的工具或嵌入式系统,静态库可能更为合适。
- 动态库: 适用于大型项目,特别是涉及到共享代码、更新频繁或需要动态加载的场景。对于框架和库的开发者,使用动态库通常更为灵活。
最终的选择取决于项目的具体需求、开发团队的偏好以及目标平台的要求。在实际开发中,有时候也会选择混合使用,即某些库采用静态链接,而另一些采用动态链接,以充分发挥各自的优势。