028 动静态库 —— 动态库

028 动静态库 —— 动态库
小米里的大麦动静态库 —— 动态库
1. 库的制作者 如何制作动态库
1. 编写库的源代码和头文件
创建头文件:声明库的对外接口函数。
创建源文件:实现头文件中声明的函数。
2. 编译为位置无关目标文件
-fPIC
关键作用:生成位置无关代码(Position Independent Code),使代码可被加载到内存任意位置,这是动态库的核心要求。
3. 链接生成动态库文件
-shared
参数:指示链接器生成共享库(.so 文件)。
4. 组织发布文件
将以下文件提供给使用者:
1 | ├── include/ # 头文件目录 |
2. 动态库 demo
1. mathlib.h
(头文件)—— 对外接口声明
1 | //相当于 #pragma once , 用于防止重复包含头文件 |
2. add.c
和 sub.c
(源文件)—— 函数定义
1 |
|
1 |
|
3. test.c
(测试文件)
1 |
|
4. 目录结构
1 | . |
5. 编译成位置无关码
1 | gcc -fPIC -c add.c -o add.o # 生成 add.o |
什么是 -fPIC?
位置无关代码就是一段“到哪都能跑”的代码,它不依赖自己必须加载到某个固定地址。
- 普通代码:写死了“我只能住在 0x123456 这个地址附近”。
- 位置无关代码:随便系统安排我住哪,我都能运行!
动态库
.so
是一种 可被多个程序同时加载 的“共享代码段”,但每个程序自己内存布局不同,比如:
- 程序 A:把
.so
放在内存地址0x100000
。- 程序 B:放在
0x200000
。如果
.so
里面代码写死了“我在 0x100000”,那程序 B 加载后就炸了!所以我们必须写出:可在任意地址运行的.so
代码 —— 这就是位置无关代码(PIC)。
怎么生成位置无关代码?
在编译.c
或.cpp
文件时加上:gcc -fPIC -c add.c -o add.o
。
所以,位置无关代码(
Position Independent Code
) 是动态库的“打包必需品”,它能让我们的代码“住哪都行,拷贝即跑”,这是 Linux 系统让多个程序共享一份.so
的关键机制。
用途 描述 创建动态库 gcc -shared ...
时要求.o
是位置无关的支持多个程序加载共享代码 系统可将同一个 .so
加载到任意地址降低代码冲突和内存浪费 PIC 允许系统更好地重用内存页(节省内存)
我们已经知道了 PIC 是位置无关码,那么
-f
又是什么?-f
是 GCC 用来启用/关闭编译“特性(feature)”的选项前缀,-fPIC
是告诉编译器生成位置无关代码,是动态库开发必备的特性之一。 反之如果看到-fno-XXX
那就是 禁用 某个特性。
6. 链接生成动态库 .so
1 | mkdir -p lib # 创建输出目录 |
7. 测试程序调用该动态库
编译时指定头文件和库路径:
1 | gcc test.c -I. -L./lib -lmathlib -o test // 注意:-I. 是头文件路径,-L./lib 是动态库路径,-lmathlib 自动匹配 libmathlib.so |
参数说明:
test.c
:测试源文件。-I.
:当前目录,告诉编译器头文件mathlib.h
在当前目录。-L./lib
:告诉编译器去./lib/
目录下找.so
文件。-lmathlib
:表示链接libmathlib.so
(前缀lib
和后缀.so
是自动补全的)。-o test
:输出最终的可执行文件test
。
运行前设置动态库查找路径:因为 libmathlib.so
在当前目录,不在 /usr/lib
等默认路径,需用 LD_LIBRARY_PATH
指定路径:LD_LIBRARY_PATH=./lib ./test
。
1 | 临时环境变量设置: |
如果遇到:
1 >./test: error while loading shared libraries: libmathlib.so: cannot open shared object file: No such file or directory这是一个 非常经典的 Linux 动态库加载错误,表示已经 链接成功,但是运行时报错。根本原因:Linux 在运行程序时会去“系统的动态库搜索路径”中查找链接的 动态库(
libmathlib.so
),但我们的库在./lib/
目录,不在默认路径中! 系统默认查找动态库的路径包括:
/lib
/usr/lib
/usr/local/lib
解决加载找不到动态库的方法:
- 拷贝到系统默认的库路径/lib64/ 或 /usr/lib64/。
- 在系统默认的库路径/lib64/ 或 /usr/lib64/下建立软连接。
- 将自己的库所在的路径,添加到环境变量
LD_LIBRARY_PATH
中。- /etc/ld.so.conf.d/ 建立自己的动态库路径的配置文件,然后重新
sudo ldconfig
命令刷新缓存即可。实际情况,我们用的库都是别人的成熟的库,都采用直接安装到系统的方式!
3. 静态库 VS 动态库
特性 | 静态库(.a ) |
动态库(.so ) |
---|---|---|
链接方式 | 编译期拷贝符号 | 运行时动态加载 |
可执行文件大小 | 大(包含所有库函数) | 小(只包含引用信息) |
升级方式 | 需重新编译应用 | 替换 .so 即可 |
使用场景 | 发布单文件程序、部署简单 | 支持版本隔离、插件式架构、共享资源 |
4. 动态库的加载(重点)
一旦动态库被加载,它的代码就会被映射到进程的地址空间。此后,该库的代码就是我们进程自己的一部分,任何执行都在进程内部完成。操作系统始终知道 当前有哪些库被加载、哪些进程在使用哪些库,这一切都由 内核 + 动态链接器 管理(先描述再组织)。
- 什么是“建立映射”:当运行一个程序,操作系统会为它创建虚拟地址空间(每个进程都有自己独立的)。动态库的内容,并不是直接“复制”一份到我们的进程里,而是操作系统建立了“映射关系”,访问它时就像是我们自己的内存一样。
- 每个进程的动态库加载情况在哪看:
cat /proc/<pid>/maps | grep .so
动态库通过映射机制变成我们进程的一部分,它的代码执行就在我们的地址空间里;但代码本体可能在物理内存中被多个进程共享,而操作系统负责管理这一切。
1. 动态库在进程运行时,是如何被加载的?
结论:动态库是由 操作系统的动态链接器 在程序运行时加载进内存的。
加载时机分为两种:
加载方式 | 说明 | 举例 |
---|---|---|
隐式加载(默认) | 程序启动时自动加载 .so 文件 |
gcc test.c -lxxx 编译出的程序 |
显式加载 | 程序运行过程中用 dlopen() 主动加载 |
插件机制、热更新系统常用 |
谁负责加载?
Linux 中负责加载 .so
的是:/lib64/ld-linux-x86-64.so.2
,这就是动态链接器,程序启动时它会完成:
- 找到依赖的
.so
。 - 将
.so
加载进内存。 - 解析符号地址表(.got / .plt)。
- 将函数地址绑定到调用位置(延迟绑定)。
我们可以通过 ldd
命令查看哪个程序使用了这个链接器。
2. 动态库加载后,是否会被“所有进程共享”?
结论:是的,部分共享! 动态库在被多个程序使用时,内存中可以共享一份代码段,但 数据段不会共享。
原因:代码段只读、可重定位、不包含状态数据;数据段:每个进程有自己的全局变量/堆栈。
例子:当运行两个程序时,操作系统发现他们都用到了同一个库:
.text
段代码是只读的,所以直接 映射到同一块物理内存;.data
段是每个进程自己的,分配在各自的地址空间中。
所以,动态库的“代码部分”是可以被多个进程共享的,从而节省内存!
共享库和缓存的关系
动态库一旦被加载一次(如被某个程序加载),其
.so
文件内容可能已经缓存在内存页中,之后被其他程序再次加载时,操作系统可以 直接使用缓存的页,而不必重新从磁盘读取。这也就是 Linux 内核的 Page Cache 机制。
3. 物理地址和虚拟地址
- 物理地址 很好理解,就是真实的内存地址,就是机器上内存条中某个具体物理位置。
- 虚拟地址 则是 操作系统为进程“伪造”的地址空间,每个进程以为自己从 0 开始拥有 4GB(其实并没有)。
谁来把虚拟地址 → 转成 → 物理地址?
操作系统 + CPU 一起完成,依赖于 MMU(内存管理单元)+ 页表
MMU 是 CPU 内负责“地址翻译”的硬件单元,它会根据当前进程的“页表”把虚拟地址转换为物理地址。
4. 编译、加载、运行三个阶段的“地址含义”
1. 编译阶段:编译器只生成“相对地址”
编译器不知道程序会加载到内存的哪一块,所以使用 偏移量 / 虚拟地址模板(编译器也要考虑操作系统)。例子:编译后并不会写死“某个变量在物理地址 0x80000000”,而是说:”该变量位于 .data
段中的偏移 +0x04”。
2. 程序尚未加载前(可执行文件中)
- 程序文件(ELF)中写的是 虚拟地址模板。
- 比如:
- 代码段
.text
映射地址是 0x08048000。 - 数据段
.data
映射地址是 0x0804a000。
- 代码段
- 这些只是“建议地址”,加载时由操作系统决定。
3. 程序加载运行后
操作系统为每个进程创建 虚拟地址空间,并按照 ELF 文件中的要求分配地址空间,将指令、数据装入虚拟地址中。然后,CPU 执行指令时看到的地址是虚拟地址!
CPU 读到的指令里面的地址,是虚拟地址吗?
是的! 程序运行期间,CPU 执行的所有地址操作(读取代码、读取变量、跳转函数)都是虚拟地址。然后由 MMU 将这些地址实时映射成物理地址。
编译完成的程序里有没有地址的概念?
有! 但它们是虚拟地址模板或偏移量(不是最终的内存地址)。
所以,编译看偏移,加载定地址,执行用虚拟,MMU 做翻译。