动静态库原理与实战详解
目录
一、什么是库?
1、库主要分为两种类型:
2、Ubuntu系统中的动静态库示例
C语言库:
C++库:
3、CentOS系统中的动静态库示例
C语言库:
C++库:
二、动静态库的基本原理
三、认识动静态库
四、静态库
1、静态库的特点
2、静态库的创建
1. 准备示例文件
2. 创建静态库
步骤1:编译源文件为目标文件(.o)
步骤2:使用ar命令打包成静态库
(可选)查看静态库内容
3、使用静态库
编译并链接静态库
运行程序
4、添加更多功能到静态库
5、使用更新后的静态库
6、静态库的三种使用场景
准备工作:创建一个干净的工作目录
场景1:链接系统标准库(模拟)
1. 创建示例文件
2. 编译链接
3. 运行程序
场景2:链接当前目录的自定义库
1. 创建文件
2. 创建静态库
3. 编译链接
4. 运行程序
场景3:指定头文件和库文件路径
1. 创建目录结构
2. 移动文件到指定位置
3. 更新main.c位置
4. 编译链接
5. 运行程序
验证静态库独立性
关键命令总结
编译选项说明
静态库的打包与使用
打包
第一步:让所有源文件生成对应的目标文件
第二步:使用ar命令将所有目标文件打包为静态库
第三步:将头文件和生成的静态库组织起来
使用Makefile
使用
方法一:使用选项
方法二:把头文件和库文件拷贝到系统路径下
为什么之前使用gcc编译的时候没有指明过库名字?
扩展:
(1)删除头文件
(2)删除静态库
五、动态库
1、动态库基本概念
动态库的特点
动态库文件扩展名
2、动态库的打包与使用
打包
第一步:让所有源文件生成对应的目标文件
总结要点:
核心结论:
关于上面知识的补充:
1、什么是 "使用相对地址而非绝对地址"?
2、为什么共享库(.so)需要相对地址?
3、什么是 "代码重定位"?
第三步:将头文件和生成的动态库组织起来
使用Makefile
使用
可能的原因(我认为最有可能是Linux 发行版的问题)
方法一:拷贝.so文件到系统共享库路径下
方法二:更改LD_LIBRARY_PATH
方法三:配置/etc/ld.so.conf.d/
3、动态库管理工具
1. ldd - 查看程序依赖的动态库
2. ldconfig - 管理共享库缓存
3. nm - 查看库中的符号
4. objdump - 查看库信息
动态库搜索路径
六、动静态库知识总结归纳
1、默认链接方式
2、Linux库安装特性
3、库与应用程序关系
4、VS编译器的能力
七、Linux中动静态库的区别和联系
1、核心区别
2、工作原理
1. 静态库
2. 动态库
3、关键联系
4、实际应用场景
5、验证工具
6、注意事项
八、使用外部库 - 课后实践
安装方法
示例代码
一、什么是库?
库是由预先编写好、经过验证且可复用的代码组成的集合。在实际开发中,大多数程序都需要依赖基础库的支持,因此库的存在极大地提高了开发效率。本质上,库是以二进制形式存在的可执行代码,能够被操作系统直接加载到内存中运行。
1、库主要分为两种类型:
- 静态库:在Linux系统中以
.a
为后缀,Windows系统中则是.lib
- 动态库:在Linux系统中以
.so
为后缀,Windows系统中则是.dll
2、Ubuntu系统中的动静态库示例
C语言库:
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
C++库:
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
3、CentOS系统中的动静态库示例
C语言库:
$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
C++库:
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 -> libstdc++.so.6.0.19$ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a
二、动静态库的基本原理
动静态库的本质是可执行程序的“半成品”。
我们都知道,一堆源文件和头文件最终变成一个可执行程序需要经历以下四个步骤:
- 预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
- 编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
- 汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。
- 链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序。
例如,用test1.c、test2.c、test3.c、test4.c以及main1.c形成可执行文件,我们需要先得到各个文件的目标文件test1.o、test2.o、test3.o、test4.o以及main1.o,然后再将这写目标文件链接起来,最终形成一个可执行程序。
如果我们在另一个项目当中也需要用到test1.c、test2.c、test3.c、test4.c和项目的main2.c或者main3.c分别形成可执行程序,那么可执行程序生成的步骤也是一样的。
而实际上,对于可能频繁用到的源文件,比如这里的test1.c、test2.c、test3.c、test4.c,我们可以将它们的目标文件test1.o、test2.o、test3.o、test4.o进行打包,之后需要用到这四个目标文件时就可以之间链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。
实际上,所有库本质都是一堆目标文件(xxx.o)的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的“半成品”。
三、认识动静态库
在Linux下创建文件编写以下代码,并生成可执行程序。
#include <stdio.h>int main()
{printf("hello world\n"); //库函数return 0;
}
这是最简单的代码,运行结果大家也都知道,就是hello world。
下面我们就通过这份简单的代码来认识一下动静态库
在这份代码当中我们可以通过调用printf输出hello world,主要原因是gcc编译器在生成可执行程序时,将C标准库也链接进来了。
在Linux下,我们可以通过ldd 文件名
来查看一个可执行程序所依赖的库文件。
这其中的libc.so.6
就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6
实际上只是一个软链接。
实际上该软链接的源文件libc-2.17.so
和libc.so.6
在同一个目录下,为了进一步了解,我们可以通过file 文件名
命令来查看libc-2.17.so
的文件类型。
此时我们可以看到,libc-2.17.so实际上就是一个共享的目标文件库,准确来说,这还是一个动态库。
- 在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
- 在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。
这里可执行程序所依赖的libc.so.6实际上就是C动态库,当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
而gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static选项。
gcc -o mytest-s mytest.c -static
此时生成的可执行程序就是静态链接的了,可以明显发现静态链接生成的可执行程序的文件大小,比动态链接生成的可执行程序的文件大小要大得多。
静态链接生成的可执行程序并不依赖其他库文件,此时当我们使用ldd 文件名
命令查看该可执行程序所依赖的库文件时就会看到以下信息。
此外,当我们分别查看动静态链接生成的可执行程序的文件类型时,也可以看到它们分别是动态链接和静态链接的。
四、静态库
在 Linux 系统中,静态库(Static Library)是一种在编译时将库代码直接嵌入到可执行文件中的库类型。它与动态库(动态链接)不同,静态库会成为程序的一部分,因此生成的可执行文件无需依赖外部库文件即可运行。
1、静态库的特点
-
文件扩展名:
.a
(Archive,归档文件),例如libmath.a
。 -
编译行为:在链接阶段,静态库的代码会被完整地复制到最终的可执行文件中。
-
运行时行为:程序运行时不需要外部的
.so
文件,因为所有依赖的库代码已经包含在可执行文件里。 -
优点:
-
独立性:程序不依赖系统环境,适合嵌入式开发或需要独立分发的场景。
-
性能略高:由于库代码已经嵌入,无需动态加载,启动速度稍快。
-
-
缺点:
-
可执行文件体积较大(因为库代码被完整复制)。
-
更新库需要重新编译程序(如果库有更新,必须重新链接)。
-
内存占用较高(如果多个程序使用同一个静态库,每个程序都会在内存中保存一份副本)。
-
-
可执行程序可能使用多个库,这些库可能是静态库或动态库。默认情况下,编译器优先链接动态库(.so),仅当找不到同名动态库时才会使用静态库。可通过gcc的-static选项强制使用静态链接。
2、静态库的创建
静态库本质上是多个目标文件(.o
文件)的归档集合,使用 ar
(archive)命令打包生成。
1. 准备示例文件
首先,我们创建两个简单的源文件:
math_functions.c (我们的库源文件):
int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}
math_functions.h (库的头文件):
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_Hint add(int a, int b);
int subtract(int a, int b);#endif
main.c (使用库的程序):
#include <stdio.h>
#include "math_functions.h"int main() {printf("2 + 3 = %d\n", add(2, 3));printf("5 - 3 = %d\n", subtract(5, 3));return 0;
}
2. 创建静态库
步骤1:编译源文件为目标文件(.o)
gcc -c math_functions.c -o math_functions.o
解释:
-
-c
表示只编译不链接 -
-o math_functions.o
指定输出文件名
步骤2:使用ar命令打包成静态库
ar rcs libmath.a math_functions.o
解释:
-
ar
是GNU归档工具 -
rcs
参数:-
r
:替换已存在的文件 -
c
:创建库(如果不存在) -
s
:写入索引(加快链接速度)
-
-
libmath.a
是静态库名称(约定以lib开头,.a结尾) -
math_functions.o
是要打包的目标文件
(可选)查看静态库内容
ar -vt libmath.a # 列出库中的目标文件
nm libmath.a # 查看库中的符号(函数/变量)
- t:列出静态库中的文件
- v:显示详细信息(verbose)
3、使用静态库
编译并链接静态库
gcc main.c -L. -lmath -o myprogram
解释:
-
-L.
:告诉编译器在当前目录(.)查找库文件 -
-lmath
:链接libmath.a(-l后接库名,省略lib和.a) -
-o myprogram
:生成可执行文件myprogram
运行程序
./myprogram
输出应该为:
4、添加更多功能到静态库
如果你想向库中添加更多功能:
math_functions.c (更新后):
int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}int multiply(int a, int b) {return a * b;
}
更新头文件 math_functions.h:
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_Hint add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);#endif
然后重新编译和更新静态库:
gcc -c math_functions.c -o math_functions.o
ar rcs libmath.a math_functions.o
5、使用更新后的静态库
更新 main.c:
#include <stdio.h>
#include "math_functions.h"int main() {printf("2 + 3 = %d\n", add(2, 3));printf("5 - 3 = %d\n", subtract(5, 3));printf("4 * 3 = %d\n", multiply(4, 3));return 0;
}
重新编译并运行:
gcc main.c -L. -lmath -o myprogram
./myprogram
输出:
6、静态库的三种使用场景
准备工作:创建一个干净的工作目录
mkdir static_lib_demo && cd static_lib_demo
场景1:链接系统标准库(模拟)
1. 创建示例文件
main.c:
#include <stdio.h>
#include <math.h> // 标准数学库头文件int main() {double x = 4.0;printf("√%.1f = %.2f\n", x, sqrt(x));return 0;
}
2. 编译链接
gcc main.c -lm -o math_demo
说明:
-
-lm
链接系统数学库libm.so
(标准库通常已在系统路径中) -
不需要手动创建这个库,它是系统自带的
3. 运行程序
./math_demo
输出:
场景2:链接当前目录的自定义库
1. 创建文件
math_functions.c:
int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}
math_functions.h:
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_Hint add(int a, int b);
int subtract(int a, int b);#endif
main.c:
#include <stdio.h>
#include "math_functions.h"int main() {printf("2 + 3 = %d\n", add(2, 3));printf("5 - 3 = %d\n", subtract(5, 3));return 0;
}
2. 创建静态库
# 编译为目标文件
gcc -c math_functions.c -o math_functions.o# 创建静态库(命名为libmymath.a)
ar rcs libmymath.a math_functions.o
3. 编译链接
gcc main.c -L. -lmymath -o custom_lib_demo
说明:
-
-L.
在当前目录查找库 -
-lmymath
链接libmymath.a
4. 运行程序
./custom_lib_demo
输出:
场景3:指定头文件和库文件路径
1. 创建目录结构
mkdir -p include lib
2. 移动文件到指定位置
mv math_functions.h include/
mv libmymath.a lib/
3. 更新main.c位置
将main.c放在项目根目录下,内容不变。
4. 编译链接
gcc main.c -I./include -L./lib -lmymath -o path_specified_demo
说明:
-
-I./include
指定头文件路径 -
-L./lib
指定库文件路径 -
-lmymath
链接库
5. 运行程序
./path_specified_demo
输出:
验证静态库独立性
# 删除静态库
rm lib/libmymath.a# 程序仍然可以运行
./path_specified_demo
生成可执行文件后,静态库可以删除而不影响程序运行:
关键命令总结
场景 | 关键命令 | 说明 |
---|---|---|
1 | gcc main.c -lm -o demo | 链接系统标准库 |
2 | gcc main.c -L. -lmymath -o demo | 链接当前目录的库 |
3 | gcc main.c -Iinclude -Llib -lmymath -o demo | 指定路径链接库 |
创建库 | ar rcs libname.a object.o | 创建静态库 |
编译选项说明
-L
: 指定库文件搜索路径-I
: 指定头文件搜索路径-l
: 指定链接的库名(去掉前缀lib
和后缀.so
/.a
,如libc.so
对应-lc
)
经过上面的讲解,我们下面再举一个完整的例子:
静态库的打包与使用
为了更容易理解,下面演示动静态库的打包与使用时,都以下面的四个文件为例,其中两个源文件add.c
和sub.c
,两个头文件add.h
和sub.h
。
add.h当中的内容如下:
#pragma onceextern int my_add(int x, int y);
add.c当中的内容如下:
#include "add.h"int my_add(int x, int y)
{return x + y;
}
sub.h当中的内容如下:
#pragma onceextern int my_sub(int x, int y);
sub.c当中的内容如下:
#include "sub.h"int my_sub(int x, int y)
{return x - y;
}
打包
下面我们就利用这四个文件打包生成一个静态库:
第一步:让所有源文件生成对应的目标文件
第二步:使用ar命令将所有目标文件打包为静态库
ar命令是GNU的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包。
- r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
- c(create):建立静态库文件。
ar -rc libcal.a add.o sub.o
此外,我们可以用ar
命令的-t
选项和-v
选项查看静态库当中的文件。
-t
:列出静态库中的文件。-v
(verbose):显示详细的信息。
ar -tv libcal.a
第三步:将头文件和生成的静态库组织起来
当我们把自己的库给别人用的时候,实际上需要给别人两个文件夹,一个文件夹下面放的是一堆头文件的集合,另一个文件夹下面放的是所有的库文件。
因此,在这里我们可以将add.h和sub.h这两个头文件放到一个名为include的目录下,将生成的静态库文件libcal.a放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mathlib给别人使用了。
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include/
cp ./*.a mathlib/lib
tree mathlib/
使用Makefile
当然,我们可以将上述所要执行的命令全部写到Makefile当中,后续当我们要生成静态库以及组织头文件和库文件时就可以一步到位了,不至于每次重新生成的时候都要敲这么多命令,这也体现了Makefile的强大。
# 定义变量
mylib = libcal.a
CC = gcc# 编译目标:生成静态库 libcal.a
$(mylib): add.o sub.oar -rc -o $(mylib) $^# 模式规则:编译 .c 文件生成 .o 文件
%.o: %.c$(CC) -c $<# 清理生成的文件
.PHONY: clean
clean:rm -f $(mylib) ./*.o# 输出:将头文件和静态库复制到指定目录
.PHONY: output
output:mkdir -p mathlib/includemkdir -p mathlib/libcp ./*.h mathlib/includecp ./*.a mathlib/lib
编写Makefile后,只需一个make
就能生成所有源文件对应的目标文件进而生成静态库。
一个make output
就能将头文件和静态库组织起来。
使用
创建源文件main.c
,编写下面这段简单的程序来尝试使用我们打包好的静态库。
#include <stdio.h>
#include <add.h>int main()
{int x = 20;int y = 10;int z = my_add(x, y);printf("%d + %d = %d\n", x, y, z);return 0;
}
现在该目录下就有main.c
和我们刚才打包好的静态库。
方法一:使用选项
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
gcc main.c -I./mathlib/include -L./mathlib/lib -lcal
此时就可以成功使用我们自己打包的库文件并生成可执行程序。
说明一下:
- 因为编译器不知道你所包含的头文件add.h在哪里,所以需要指定头文件的搜索路径。
- 因为头文件add.h当中只有my_add函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。
- 实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
- -I,-L,-l这三个选项后面可以加空格,也可以不加空格。
方法二:把头文件和库文件拷贝到系统路径下
既然编译器找不到我们的头文件和库文件,那么我们直接将头文件和库文件拷贝到系统路径下不就行了。
sudo cp mathlib/include/* /usr/include/
sudo cp mathlib/lib/libcal.a /lib64/
需要注意的是,虽然已经将头文件和库文件拷贝到系统路径下,但当我们使用gcc编译main.c生成可执行程序时,还是需要指明需要链接库文件路径下的哪一个库。此时才能成功使用我们自己打包的库文件并生成可执行程序。
为什么之前使用gcc编译的时候没有指明过库名字?
因为我们使用gcc编译的是C语言,而gcc就是用来编译C程序的,所以gcc编译的时候默认就找的是C库,但此时我们要链接的是哪一个库编译器是不知道的,因此我们还是需要使用-l选项,指明需要链接库文件路径下的哪一个库。
扩展:
实际上我们拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染,我们要删除它们!!!
(1)删除头文件
sudo rm /usr/include/add.h /usr/include/sub.h # 替换为实际的头文件名
或删除所有匹配的文件(谨慎使用):
sudo rm /usr/include/your_lib_*.h # 使用通配符匹配你的库文件
(2)删除静态库
sudo rm /lib64/libcal.a
五、动态库
动态库(Dynamic Library),也称为共享库(Shared Library),是 Linux 系统中重要的代码共享机制。与静态库不同,动态库在程序运行时才被加载和链接,而不是在编译时就被打包进可执行文件中。
1、动态库基本概念
动态库的特点
-
运行时链接:程序运行时才加载
-
共享性:多个程序可共享同一个库文件
-
独立性:库更新无需重新编译依赖它的程序
-
节省空间:磁盘和内存中都只有一份副本
-
链接机制:与动态库链接的可执行文件仅包含所用函数的入口地址表,而非外部函数的完整机器码。
-
动态链接:程序运行前,操作系统会将所需函数的机器码从磁盘上的动态库加载到内存中,这一过程称为动态链接(dynamic linking)。
共享优势:
- 节省磁盘空间:多个程序可共享同一动态库,减小可执行文件体积。
- 节省内存空间:操作系统的虚拟内存机制允许不同进程共享物理内存中的同一动态库副本。
动态库文件扩展名
-
.so
:共享对象文件(Shared Object) -
.so.version
:带版本号的共享库文件
动态库在程序运行时才进行链接,多个程序可以共享同一动态库的代码。与动态库链接的可执行文件仅包含所用函数的入口地址表,而非外部函数的完整机器码。
程序运行前,操作系统会将所需外部函数的机器码从磁盘上的动态库加载到内存中,这一过程称为动态链接。动态库可实现多程序共享,有效节省磁盘空间。操作系统通过虚拟内存机制,使物理内存中的同一份动态库能被多个进程共用,从而节省内存和磁盘空间。
2、动态库的打包与使用
动态库的使用场景与静态库的使用三个场景一样!!!可以类比!!!这里就不详细说明了!!!
打包
动态库的打包相对于静态库来说有一点点差别,但大致相同,我们还是利用这四个文件进行打包演示:
第一步:让所有源文件生成对应的目标文件
此时用源文件生成目标文件时需要携带-fPIC
选项:
-fPIC
(position independent code):产生位置无关码。
gcc -fPIC -c add.c
gcc -fPIC -c sub.c
总结要点:
-
-fPIC 的作用
-
生成位置无关代码(Position Independent Code),使用相对地址而非绝对地址,使代码可加载到内存任意位置执行。
-
是共享库(
.so
)的关键特性,因为共享库的加载位置不固定。
-
-
不使用 -fPIC 的问题
-
代码需通过重定位修改绝对地址,导致:
-
每个进程独享一份代码段拷贝(因加载位置不同,重定位结果不同)。
-
无法实现多进程间共享同一代码段,增加内存开销。
-
-
-
使用场景
-
共享库(.so):通常必须用
-fPIC
,确保代码可共享。 -
静态库(.a):一般无需
-fPIC
(因代码直接链接到程序,地址固定)。 -
无
-fPIC
的.so
虽可编译,但加载时需重定位,丧失共享优势。
-
核心结论:
-fPIC
是共享库的必备选项,通过避免绝对地址引用实现多进程代码共享,提升效率;非 PIC 代码会导致冗余内存占用。
关于上面知识的补充:
1、什么是 "使用相对地址而非绝对地址"?
在编程中,代码和数据通常需要访问内存中的某些位置(比如变量、函数等)。访问这些位置的方式有两种:
-
绝对地址:直接使用固定的内存地址(如
0x12345678
)。-
如果程序被加载到不同的内存位置,这些地址会失效,导致程序崩溃。
-
-
相对地址:基于当前指令的位置计算目标地址(如 "从当前指令往后偏移 100 字节")。
-
无论程序加载到内存的哪个位置,相对地址仍然有效,因为偏移量不变。
-
2、为什么共享库(.so)需要相对地址?
-
共享库会被多个程序同时使用,但不同程序可能把它加载到不同的内存地址。
-
如果使用绝对地址,不同程序加载时,代码里的固定地址会指向错误的位置,导致崩溃。
-
相对地址(
-fPIC
)让代码不依赖具体内存位置,无论加载到哪里都能正确运行。
3、什么是 "代码重定位"?
当程序或共享库(.so
)被加载到内存时,操作系统需要调整代码中的某些地址(比如函数调用、全局变量的引用),使其指向正确的内存位置。这个过程就叫 重定位(Relocation)。
第二步:使用-shared选项将所有目标文件打包为动态库
与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。
gcc -shared -o libcal.so add.o sub.o
第三步:将头文件和生成的动态库组织起来
与生成静态库时一样,为了方便别人使用,在这里我们可以将add.h和sub.h这两个头文件放到一个名为include的目录下,将生成的动态库文件libcal.so放到一个名为lib的目录下,然后将这两个目录都放到mlib下,此时就可以将mlib给别人使用了。
mkdir -p mlib/include
mkdir -p mlib/lib
cp ./*.h mlib/include/
cp ./*.so mlib/lib/
tree mlib
使用Makefile
当然,生成动态库也可以将上述所要执行的命令全部写到Makefile当中,后续当我们要生成动态库以及组织头文件和库文件时就可以一步到位了。
mylib=libcal.so
CC=gcc$(mylib): add.o sub.o$(CC) -shared -o $(mylib) $^%.o: %.c$(CC) -fPIC -c $<.PHONY: clean
clean:rm -rf $(mylib) ./*.o.PHONY: output
output:mkdir -p mlib/includemkdir -p mlib/libcp ./*.h mlib/includecp ./*.so mlib/lib
编写Makefile后,只需一个make
就能生成所有源文件对应的目标文件进而生成动态库。
一个make output
就能将头文件和动态库组织起来。
使用
我们还是用刚才使用过的main.c
来演示动态库的使用。
#include <stdio.h>
#include <add.h>int main()
{int x = 20;int y = 10;int z = my_add(x, y);printf("%d + %d = %d\n", x, y, z);return 0;
}
说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I,-L,-l这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l选项指明需要链接的库名字来生成可执行程序,下面我们仅以第一种方法为例进行演示。
此时使用gcc编译main.c生成可执行程序时,需要用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明需要链接库文件路径下的哪一个库。
gcc main.c -I./mlib/include -L./mlib/lib -lcal
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。(但是实际上我却能够运行!!!我当时气疯了!!!哈哈哈哈!!!)
需要注意的是,我们使用-I
,-L
,-l
这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd
命令进行查看。(但实际上我操作得到的却是可以找到,于是我搜索了相关的原因,如下)
ldd 可执行程序
输出显示 libcal.so
被成功找到了(显示了一个内存地址 0x00007fab48f5d000
),而不是 [not found]!!!!!
可能的原因(我认为最有可能是Linux 发行版的问题)
-
当前目录在库搜索路径中:
-
某些 Linux 发行版默认将当前目录 (
.
) 包含在LD_LIBRARY_PATH
中 -
或者shell 配置文件中已经设置了
LD_LIBRARY_PATH
包含当前目录
-
-
系统配置差异:
-
不同 Linux 发行版对动态链接器的默认配置可能不同
-
系统可能有不同的安全策略设置
-
-
环境变量影响:可能之前设置过
LD_LIBRARY_PATH
环境变量(但是如下,我发现并没有设置!!!!)
我们使用理论来看的话,解决该not found的问题的方法有以下三个:
方法一:拷贝.so文件到系统共享库路径下
既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。
sudo cp mlib/lib/libcal.so /lib64
我们就假装可执行程序能够顺利运行了,呜呜呜!!!可恶的发行版!!!
方法二:更改LD_LIBRARY_PATH
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/hmz
此时我们再用ldd
命令查看该可执行程序就会发现,系统现在就可以找到该可执行程序所依赖的动态库了。(无力感,还是一样的结果。。。。。。)现在我们也就能正常运行该可执行程序了。
方法三:配置/etc/ld.so.conf.d/
我们可以通过配置/etc/ld.so.conf.d/的方式解决该问题,/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。
首先将库文件所在目录的路径存入一个以.conf为后缀的文件当中。
然后将该.conf文件拷贝到/etc/ld.so.conf.d/
目录下。
但此时我们用ldd
命令查看可执行程序时,发现系统还是没有找到该可执行程序所依赖的动态库(我的服务器并非如此,你懂的,我们假设没有嘛)。这时我们需要使用ldconfig
命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了。
sudo ldconfig
3、动态库管理工具
1. ldd - 查看程序依赖的动态库
ldd mathdemo
输出示例:
linux-vdso.so.1 (0x00007ffd3a3f0000)
libmath.so => ./libmath.so (0x00007f8c12345000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8c11f53000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8c12349000)
2. ldconfig - 管理共享库缓存
sudo ldconfig # 更新共享库缓存
ldconfig -p # 打印当前缓存的所有共享库
3. nm - 查看库中的符号
nm -D libcal.so
4. objdump - 查看库信息
objdump -T libcal.so # 查看动态符号表
动态库搜索路径
系统按以下顺序搜索动态库:
-
编译时指定的
-rpath
路径 -
LD_LIBRARY_PATH
环境变量指定的路径 -
/etc/ld.so.cache
中缓存的路径(由/etc/ld.so.conf
配置) -
默认路径:
/lib
和/usr/lib
六、动静态库知识总结归纳
1、默认链接方式
- 使用链接器(如ld)链接
- GCC/G++编译器默认使用动态库链接
- 如需静态链接必须显式添加
-static
选项 - 当仅存在静态库时,程序对该库只能进行静态链接
示例场景:
在Linux系统下使用GCC编译程序时,若需要静态链接数学库libm.a
(而非默认的动态库libm.so
):
# 默认动态链接(即使同时存在动/静态库)
gcc main.c -o dynamic_demo -lm # 强制静态链接(必须显式添加-static)
gcc main.c -o static_demo -static -lm
关键说明:
-
选项必要性:
- 不加
-static
时,即使系统同时存在libm.a
和libm.so
,GCC也会优先选择.so
动态库。 - 添加
-static
后,编译器会强制寻找.a
静态库,若不存在则报错(如图中结论1强调的"必须存在对应的静态库")。
- 不加
-
极端情况:
若系统中仅存在静态库libm.a
(无动态库),则编译时:gcc main.c -o demo -lm # 仍会自动静态链接(符合结论1的"只能静态链接")
此时无需
-static
也能静态链接,但这是因动态库缺失导致的被动行为,非编译器默认逻辑。 -
VS对比:
结论4所示,Visual Studio通过项目属性即可选择生成/链接动静态库(如/MT
静态链接),而GCC必须通过命令行选项显式控制。
注:强调的-static
是GCC工具链中静态链接的核心开关,与Linux动态库优先策略(结论2)形成直接对比。
2、Linux库安装特性
- Linux系统默认优先安装动态库(.so文件)
- 动态库是大多数Linux发行版的默认选择
3、库与应用程序关系
- 库文件与应用程序呈1:n关系
- 单个库文件可被多个应用程序共享使用
4、VS编译器的能力
Visual Studio不仅能生成可执行程序,还支持同时生成动态库和静态库
七、Linux中动静态库的区别和联系
在Linux系统中,库(Library)是预编译代码的集合,用于提供常用的功能。库分为静态库(Static Library)和动态库(Dynamic Library,也称共享库 Shared Library),它们在链接方式、内存占用、更新维护等方面有显著区别。以下是详细解析:
1、核心区别
特性 | 静态库(.a) | 动态库(.so) |
---|---|---|
文件扩展名 | .a (Archive) | .so (Shared Object) |
链接时机 | 编译时直接嵌入到可执行文件 | 运行时由系统动态加载 |
可执行文件大小 | 较大(库代码被复制到程序中) | 较小(仅存储库的引用) |
内存占用 | 每个进程独立占用内存 | 多个进程共享同一库的代码段 |
更新维护 | 需重新编译程序 | 替换.so 文件即可生效 |
加载速度 | 更快(无运行时加载开销) | 稍慢(需运行时解析符号) |
依赖关系 | 无运行时依赖 | 需确保运行时库路径正确 |
2、工作原理
1. 静态库
-
编译过程:
静态库通过ar
工具将多个.o
目标文件打包成.a
文件。程序编译时,链接器(ld
)将库代码直接复制到最终的可执行文件中。# 创建静态库 ar rcs libfoo.a foo1.o foo2.o # 链接静态库 gcc main.c -L. -lfoo -o main
-
优点:
-
独立性:程序无需外部依赖。
-
性能:无运行时加载开销。
-
-
缺点:
-
浪费空间:多个程序包含相同库代码。
-
更新困难:库更新需重新编译程序。
-
2. 动态库
-
编译过程:
动态库通过gcc -shared
生成.so
文件。程序编译时仅记录库的符号信息,运行时由动态链接器(ld-linux.so
)加载。# 创建动态库 gcc -shared -fPIC foo1.c foo2.c -o libfoo.so # 链接动态库 gcc main.c -L. -lfoo -o main
-
运行时加载:
程序运行时需通过LD_LIBRARY_PATH
或/etc/ld.so.conf
定位库文件。# 临时设置库路径 export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main
-
优点:
-
节省空间:多个进程共享同一库。
-
易维护:更新库无需重新编译程序。
-
-
缺点:
-
依赖管理:需确保库版本兼容。
-
轻微性能损耗:运行时符号解析。
-
3、关键联系
-
共同目标:两者均用于代码复用,提供标准函数接口(如C标准库
libc
既有静态版libc.a
也有动态版libc.so
)。 -
混合使用:同一程序可同时链接静态库和动态库(例如:核心模块静态链接以保证独立性,非关键模块动态链接节省空间)。
-
工具链共享:均通过
gcc
和ld
处理,静态库用ar
打包,动态库用gcc -shared
生成。
4、实际应用场景
-
静态库适用场景:
-
嵌入式系统(无动态链接器)。
-
需分发独立二进制文件(避免依赖问题)。
-
对性能敏感的场景(如启动时间要求严格)。
-
-
动态库适用场景:
-
桌面/服务器程序(共享系统库如
glibc
)。 -
库需要频繁更新(如安全补丁)。
-
减少磁盘和内存占用(如多进程服务)。
-
5、验证工具
-
查看依赖的动态库:
ldd /bin/ls
输出示例:
-
查看静态库内容:
ar -t libfoo.a
-
查看动态库符号表:
nm -D libfoo.so
6、注意事项
-
版本控制:动态库需注意符号版本兼容性(如
libfoo.so.1
与libfoo.so.2
可能不兼容)。 -
安全更新:动态库的热更新可能引发安全问题(需确保原子替换)。
-
调试符号:动态库调试时需确保符号表未剥离(编译时保留
-g
选项)。
八、使用外部库 - 课后实践
目前我们接触的库还不多,主要是C/C++标准库。这里推荐一个有趣的图形库:ncurses
安装方法
- CentOS:
sudo yum install -y ncurses-devel
- Ubuntu:
sudo apt install -y libncurses-dev
系统中有许多功能库,它们通常由一组相互关联的函数组成,用于完成特定任务。比如ncurses库就是专门处理屏幕显示功能的。
示例代码
以下是使用ncurses库创建进度条的示例代码(已调试通过):
#include <ncurses.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2)
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000 // 300毫秒int main() {initscr();start_color();init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分:绿色前景,黑色背景init_pair(2, COLOR_RED, COLOR_BLACK); // 剩余部分:红色背景(仅用于演示)cbreak();noecho();curs_set(FALSE);int max_y, max_x;getmaxyx(stdscr, max_y, max_x);int start_y = (max_y - WINDOW_HEIGHT) / 2;int start_x = (max_x - WINDOW_WIDTH) / 2;WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);box(win, 0, 0); // 添加边框wrefresh(win);int progress = 0;int max_progress = PROGRESS_BAR_WIDTH;while (progress <= max_progress) {werase(win); // 清除窗口内容box(win, 0, 0); // 重新绘制边框// 计算进度int completed = progress;int remaining = max_progress - progress;// 显示进度条int bar_x = BORDER_PADDING + 1;int bar_y = 1;// 已完成部分attron(COLOR_PAIR(1));for (int i = 0; i < completed; i++) {mvwprintw(win, bar_y, bar_x + i, "#");}attroff(COLOR_PAIR(1));// 剩余部分attron(A_BOLD | COLOR_PAIR(2));for (int i = completed; i < max_progress; i++) {mvwprintw(win, bar_y, bar_x + i, " ");}attroff(A_BOLD | COLOR_PAIR(2));// 显示百分比char percent_str[10];snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) / max_progress);int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2;mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);wrefresh(win);progress += PROGRESS_INCREMENT;usleep(DELAY);}delwin(win);endwin();return 0;
}
可以尝试运行并了解这个进度条的实现原理。(可选!!!!!)
推荐一篇实用指南:ncurse编程指南_ncurses教程-CSDN博客