当前位置: 首页 > news >正文

028 动静态库 —— 动态库

🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022

在这里插入图片描述

文章目录

    • 动静态库 —— 动态库
      • 1. 库的制作者 如何制作动态库
        • 1. 编写库的源代码和头文件
        • 2. 编译为位置无关目标文件
        • 3. 链接生成动态库文件
        • 4. 组织发布文件
      • 2. 动态库 demo
        • 1. `mathlib.h`(头文件)—— 对外接口声明
        • 2. `add.c` 和 `sub.c`(源文件)—— 函数定义
        • 3. `test.c`(测试文件)
        • 4. 目录结构
        • 5. 编译成位置无关码
        • 什么是 -fPIC?
        • 6. 链接生成动态库 .so
        • 7. 测试程序调用该动态库
      • 3. 静态库 VS 动态库
      • 4. 动态库的加载(重点)
        • 1. 动态库在进程运行时,是如何被加载的?
          • 谁负责加载?
        • 2. 动态库加载后,是否会被“所有进程共享”?
        • 3. 物理地址和虚拟地址
        • 4. 编译、加载、运行三个阶段的“地址含义”
          • 1. 编译阶段:编译器只生成“相对地址”
          • 2. 程序尚未加载前(可执行文件中)
          • 3. 程序加载运行后
    • 共勉

动静态库 —— 动态库

1. 库的制作者 如何制作动态库

1. 编写库的源代码和头文件
  1. 创建头文件:声明库的对外接口函数。

  2. 创建源文件:实现头文件中声明的函数。

2. 编译为位置无关目标文件

-fPIC 关键作用:生成位置无关代码(Position Independent Code),使代码可被加载到内存任意位置,这是动态库的核心要求。

3. 链接生成动态库文件

-shared 参数:指示链接器生成共享库(.so 文件)。

4. 组织发布文件

将以下文件提供给使用者:

├── include/           # 头文件目录
│   └── mathlib.h      # 接口声明
└── lib/               # 库文件目录└── libmathlib.so  # 动态库二进制

2. 动态库 demo

1. mathlib.h(头文件)—— 对外接口声明
//相当于 #pragma once , 用于防止重复包含头文件
#ifndef MATHLIB_H
#define MATHLIB_Hint add(int a, int b);
int sub(int a, int b);#endif	//表明头文件结束
2. add.csub.c(源文件)—— 函数定义
#include "mathlib.h"
int add(int a, int b)
{return a + b;
}
#include "mathlib.h"
int sub(int a, int b)
{return a - b;
}
3. test.c(测试文件)
#include <stdio.h>
#include "mathlib.h"
int main()
{printf("add(3, 5) = %d\n", add(3, 5));printf("sub(10, 4) = %d\n", sub(10, 4));return 0;
}
4. 目录结构
.
├── add.c          // 加法实现
├── sub.c          // 减法实现
├── mathlib.h      // 接口声明头文件
├── lib/           // 输出的动态库放在这里
├── test.c         // 用于测试动态库的代码
5. 编译成位置无关码
gcc -fPIC -c add.c -o add.o     # 生成 add.o
gcc -fPIC -c sub.c -o sub.o     # 生成 sub.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
mkdir -p lib                                 # 创建输出目录
gcc -shared -o lib/libmathlib.so add.o sub.o # 链接成动态库
7. 测试程序调用该动态库

编译时指定头文件和库路径:

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

临时环境变量设置:
LD_LIBRARY_PATH=./lib ./test					   # 单次运行有效
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH	     # 当前会话有效

image-20250613230234991

如果遇到:

./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 ,这就是动态链接器,程序启动时它会完成:

  1. 找到依赖的 .so
  2. .so 加载进内存。
  3. 解析符号地址表(.got / .plt)。
  4. 将函数地址绑定到调用位置(延迟绑定)。

我们可以通过 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 将这些地址实时映射成物理地址。


编译完成的程序里有没有地址的概念?
有! 但它们是虚拟地址模板或偏移量(不是最终的内存地址)。

image-20250614193537214

所以,编译看偏移,加载定地址,执行用虚拟,MMU 做翻译。

共勉

在这里插入图片描述
在这里插入图片描述

http://www.lryc.cn/news/623280.html

相关文章:

  • duiLib 实现鼠标拖动标题栏时,窗口跟着拖动
  • Vue 3.5重磅更新:响应式Props解构,让组件开发更简洁高效
  • 分享一个Oracle表空间自动扩容与清理脚本
  • CPP多线程3:async和future、promise
  • MATLAB基础训练实验
  • 超越“调参”:从系统架构师视角,重构 AI 智能体的设计范式
  • 深度剖析Redisson分布式锁项目实战
  • 【数据分享】大清河(大庆河)流域上游土地利用
  • AutoDL使用学习
  • K8s核心组件全解析
  • 服务器配置开机自启动服务
  • GEEPython-demo1:利用Sentinel-2监测北京奥林匹克森林公园2024年NDVI变化(附Python版)
  • [CSP-J2020] 方格取数
  • Vue组件生命周期钩子:深入理解组件的生命周期阶段
  • Vue 3.5+ Teleport defer 属性详解:解决组件渲染顺序问题的终极方案
  • 【P14 3-6 】OpenCV Python——视频加载、摄像头调用、视频基本信息获取(宽、高、帧率、总帧数)
  • ESP32-S3_ES8311音频输出使用
  • CSS 核心知识点全解析:从基础到实战应用
  • 探索粒子世界:从基础理论到前沿应用与未来展望
  • 主从复制+哨兵
  • 【论文阅读】Multimodal Graph Contrastive Learning for Multimedia-based Recommendation
  • List容器:特性与操作使用指南
  • 《设计模式》代理模式
  • Java 9 新特性及具体应用
  • 什么是微前端?
  • XC6SLX45T-2FGG484C Xilinx AMD Spartan-6 FPGA
  • 两个简单的设计模式的例子
  • [Linux] Linux文件系统基本管理
  • 没学过音乐怎么写歌?豆包 + 蘑兔
  • Python Condition对象wait方法使用与修复