动静态库【Linux操作系统】
文章目录
- 动静态库
- 制作静态库
- 如何把第三方库安装在Linux系统中,如何使用第3方库
- 方案一:
- 为什么我们之前使用gcc/g++编译C/C++标准库的时候不用加选项-l xxx呢?
- 方案二:
- 方案三:
- 为什么不同平台的库不一样呢?
- 动态库的制作和使用
- 动态库的制作
- 动态库名字格式:
- 使用动态库
- 动态库的加载
- 动态库的具体加载过程(可能需要下一篇文章的知识)
- 动静态库的优缺点
- 静态库
- 优点
- 缺点
- 动态库
- 优点
- 缺点
- 选择建议
动静态库
库的本质就是.o文件的集合,即把所有.o文件链接在一起就成了库
如果编译时同时提供去掉前缀和后缀之后同名的静态库和动态库
gcc/g++会优先使用动态库
此时如果非要用静态库,那就在编译时增加一个选项-static
-static
选项的作用是:只使用静态库
库的作用主要是:
①把我们自己实现的方法(源文件)隐藏起来,不能直接给别人看
②提供给别人使用
头文件的作用是:
①方便自己管理函数
②方便使用我们的库的人查看函数的返回值,参数表
所以头文件不需要隐藏,反而必须给别人看
制作静态库
步骤:
①把所有源文件编译成可以被直接链接
的.o文件
②使用命令ar -rc
自定义库文件名 所有.o文件
ar -rc解析
1.命令ar:表示要把.o文件打包形成静态库
2.选项-rc:表示如果被打包的.o文件中有重复的,那么就去重
静态库文件名的格式:
①最开头的3个字符必须是lib
②后缀必须是.a或者.a.1.23[.a后面的数字表示库的版本]
如何把第三方库安装在Linux系统中,如何使用第3方库
逻辑链:
使用第3方库→得先有第三方库的代码→要安装第三方库→库有头文件+库本体,这两个都得安装→写代码时包含第三方库头文件→编译器编译时得能找到第三方库的头文件和库代码→编译器就得知道去什么目录下找第三方库的头文件,去什么路径下找第三方的库本体→目录下可能有多个库→所以还要知道第三方库的名字
方案一:
具体流程:
-
把库的头文件都拷贝进Linux中存放头文件的目录
/usr/include
(这个目录的路径由编译器决定) -
把我们的库(我们写的库对于Linux来说也是第3方库)拷贝到Linux中存放库文件的目录/lib64
-
安装好之后,编写代码的时候就可以直接使用<>包含我们写的库的头文件,使用我们的库的方法了
-
但是如果使用了第3方库,编译的时候就要在末尾加选项:-l xxx [
xxx表示第3方库的名字,去掉前缀lib和后缀.a(.so)之后的字符串
]
并且包含了几个第3方库,就要多加几个这个选项
因为/lib64目录下不止一个库,你不指定名字,编译器怎么知道是哪个库?
例
第三方库:libsss.a
方案二编译命令:g++ -o test.cpp -l sss
其中:test.cpp是包含main函数的源文件
为什么我们之前使用gcc/g++编译C/C++标准库的时候不用加选项-l xxx呢?
这是因为gcc/g++编译器是C/C++语言编译器,C/C++有自己的标准库,也在Linux中安装了标准库
所以C/C++编译器默认肯定就知道自己标准库的名字
我们写的库对于C/C++来说也是第三方库,他肯定不知道我们的名字
Linux中安装的库那么多,他不知道名字怎么找到第3方库?
方案二:
如果不把库放进Linux专门存放库的目录下,因为编译器查找库的时候,默认只会去专门存放库文件的目录下查找
所以要找到这个第3方库除了使用选项-l xxx还要指明它在哪条路径下
这样编译器就会在用户指定路径和Linux专门存放的目录下都找一找
如果同名了,就先找到哪个用哪个
方法:
编译链接的时候在方案一的基础上,再新增选项-L 路径
例
第三方库:libsss.a
方案二编译命令:g++ -o test.cpp -l sss -L ./lib
其中:test.cpp是包含main函数的源文件,./lib就是存放库libsss.a的路径
方案三:
发布库和头文件
-
生成静态库
-
创建在新建一个目录a,然后在里面再创建lib和include目录
-
把所有库放进lib目录,把所有库的头文件放进include目录
-
使用zip/tar命令打包目录a
-
别人获得我打包的目录之后,可以安装到操作系统中
也可以在编译时
通过选项-I(这个字母是大写的i)
路径找到头文件所处目录,再通过选项-L 路径找到库所处目录,最后通过选项-l 库名找到要使用第库
例
第三方库:libsss.a
方案三编译命令:
g++ -o test.cpp -l sss -L ./ilib -I ./include
其中:test.cpp是包含main函数的源文件,./lib就是存放库libsss.a的路径,./include就是存放库头文件的路径
为什么编译时不需要指定头文件的名字?
为啥第三方库的名字要用-l选项指定,头文件却不需要呢?
因为你自己编写代码的#include的时候,就已经指定了
为什么不同平台的库不一样呢?
因为库本质是.o文件的集合
而不同平台的编译规则可能不一样
,所以编译之后形成的.o文件就不一样,进而导致库不一样
在一个不同平台安装库,本质就是用同一份源代码,在对应平台上编译一下,此时生成的.o文件就是根据这个平台的编译规则而生成的了
动态库的制作和使用
动态库的制作
动态库本质也是.o文件的集合
但是形成动态库的.o文件比较特殊
在使用gcc/g++编译源文件的时候生成.o文件时,要在gcc/g++后面加选项-fPIC
生成拥有与位置无关码的.o文件
制作命令:
gcc -o 动态库文件名 所有的.o文件 -shared -fPIC
选项-shared的作用是
告诉编译器不要生成可执行程序,而是生成动态库
动态库名字格式:
①最开头3个字符为lib
②后缀为.so
使用动态库
编译器找到动态库生成可执行程序的方法和找静态库的方法一模一样
都是
①通过-I找到头文件的位置
②通过-L找到库的位置
③通过-l找到具体是那一个库
但是形成可执行程序之后,运行进程的是操作系统,与编译器没有任何关系了
所以我们如果直接运行可执行程序,可能也会报错
因为编译器编译形成可执行程序之后,只会把可执行程序依赖的库的名字告诉操作系统
而运行的时候,调用库函数时,是得在内存中找到动态库,去动态库里面执行代码
所以:操作系统还得把动态库加载到内存,并映射到进程的地址空间
所以:必须得先找到动态库
因为操作系统运行时查找动态库的时候,也只会在Linux专门存储库的目录(/lib64)里找
如果找不到,就报错了
如何解决?
-
把我们写的第3方动态库,拷贝到系统默认查找库的目录下(即/lib64)
-
使用软链接
,在系统默认查找库的目录下建立一个指向我们写的动态库的软连接 -
配置环境变量
LD_LIBRARY_PATH
,如果bash环境变量表里面里面还没有这个环境变量的话,直接手动导入即可(配置信息的时候尽量使用绝对路径)
因为进程知道自己依赖的动态库名字,只是不知道库的路径
所以配置环境变量的时候,只需要路径,不需要在路径最末尾加上库名字
注意:
只把环境变量LD_LIBRARY_PATH导入到进程自己的环境变量表中,不导入bash的环境变量表中是没用的
因为在进程运行之前,操作系统/bash就会为该进程找动态库了,找不到就报错了 -
在Linux中的/etc/ld.so.conf.d目录下,新建一个后缀为.conf的配置文件,在里面写上我们写的库的所处目录的路径
因为系统找库时,默认会在这些配置文件里面的路径中找
再使用ldconfig更新系统的默认查找库路径
上面4种方法中,常用的方法就是第1种和第3种
动态库的加载
动静态库也是文件
动静态库本质都是.o文件的集合
而.o文件又是编译好的可以直接被CPU调用的二进制代码
所以只需要把动态库加载到内存,并让依赖这个动态库的进程能看见它
进程再执行到库函数的时候,就可以找到这个动态库,拿着它里面的代码给CPU
如何让进程看见动态库?
只需要把动态库放进,进程的虚拟地址空间中的共享区
(即共享区的虚拟地址通过页表,映射物理内存中存储动态库的物理地址
)
所以我们进程执行库函数,是在自己的进程地址空间中跳转运行的
但是可能出现多个进程依赖同一个动态库的情况
此时需要多次加载动态库吗?
肯定不需要啊
只需要加载一份动态库,再把所有进程的虚拟地址空间中的共享区,通过页表映射到动态库就可以了
这也是为什么操作系统喜欢使用动态库的原因,使用动态库的进程无论有多少,都只需要一个动态库,节省内存
如果进程都使用静态库
静态库的方法都存储在进程自己的代码中
①不仅可执行程序的大小会变大
②多个进程加载到内存时,因为都有静态库占用的内存也会变大
动态库的具体加载过程(可能需要下一篇文章的知识)
库的文件格式也是ELF格式
所以库也会给自己的代码和数据编址
库的编址也是采用平坦模式
所以逻辑地址=偏移量
所以库里面的函数的地址=偏移量
进程如何在运行时,找到动态库中的方法:
-
操作系统找到依赖的动态库对应结构体变量,如果内存中没有该动态库,就先
把该动态库加载到内存
-
通过动态库的结构体,拿到动态库的起始物理地址,再为库创建一个
struct vm_area_struct
节点,再在页表中建立库的物理地址与虚拟地址的映射 这样库就映射到了进程地址空间中的共享区 -
在链接动态库形成可执行文件的时候,就把可执行文件中调用库函数的那一个代码,修改成了
动态库的名字+那个函数在动态库里面的偏移量
比如调用printf的代码就会被修改成:libc.so:0x1363
(此处的偏移量是乱写的)
而在动态库映射到进程地址空间中之后,我们就得到了这个动态库的起始虚拟地址
然后就可以再把进程代码区中的动态库的名字也替换成它的虚拟起始地址
所以:libc.so:0x1363→0x1002:0x1363
(这个过程就叫地址重定位) -
再运行进程时,CPU执行到库方法的时候,直接
通过动态库的虚拟起始地址和方法的偏移量
,去共享区找到库中对应的方法,执行完再返回
所以动态库被映射到一个进程的共享区中的任意位置都可以,同一个动态库映射到多个进程中时起始地址也随意,这就是动态库与位置无关的特性
动静态库的优缺点
静态库
优点
-
独立性
代码在编译时直接嵌入到可执行文件中,==即使静态库没了,程序也能继续运行 -
性能
==函数调用无动态链接的开销(如符号解析、加载等),运行时速度略 -
兼容性
不依赖系统环境中的库版本,避免因动态库版本冲突导致的问题。
缺点
-
体积大
库代码会被复制到每个使用它的可执行文件中,导致最终文件体积较大(尤其是多进程场景下内存占用更高) -
更新困难
库更新需重新编译所有依赖它的程序,不适合频繁升级的场景。 -
冗余性
多个程序无法共享同一份库代码,内存利用率低。
动态库
优点
-
节省资源
多个程序共享同一份库文件,减少磁盘和内存占用 -
更新灵活
更新库文件后,所有依赖它的程序无需重新编译即可生效
即:库文件与程序文件独立,只要输出接口不变,更换库文件不会对程序文件造成任何影响,因而极大地提高了可维护性和可扩展性 -
模块化
支持运行时加载(如插件系统),增强程序扩展性
缺点
-
依赖性强
程序运行时需确保系统中存在正确版本的库文件,否则会崩溃 -
性能开销
需要映射到进程地址空间中,即需要给库分配虚拟地址,可能有轻微性能损失 -
兼容性风险
库版本升级若破坏 ABI(二进制接口兼容性),会导致依赖程序无法运行。
选择建议
用静态库的场景
要求部署简单(如单文件工具)、对性能敏感
,或目标环境缺乏稳定的库支持。
用动态库的场景
注重资源共享(如系统基础库)、需要热更新,或开发大型模块化应用。