【Linux】动态库和静态库
👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、动静态库
- 1.1 什么是库
- 1.2 动静态链接
- 1.2.1 动态链接
- 1.2.1 静态链接
- 1.3 动静态库总结
- 二、如何设计静态库
- 2.1 制作步骤
- 2.2 安装库
- 三、如何设计动态库
- 3.1 制作步骤
- 3.2 解决程序加载到内存无法找到动态库(四种方法)
- 3.3 总结一波
- 四、动态库是如何做到被所有进程所共享
- 五、-fPIC与地址无关码
一、动静态库
1.1 什么是库
我们平时写的C
程序,一上来就写上#include <stdio.h>
,然后就可以使用诸如printf
、scanf
等函数,可在stdio.h
中只包含函数的声明而不包括函数的定义(实现),那么函数的定义在哪里呢?
答案是在库文件中。库是一组已经编写好的代码或程序模块,通常包含了函数的实现,使用库可以帮助程序员节省时间和精力,使得程序员不需要花费精力造轮子,提高了代码的可维护性和可重用性。
而库又分为动态库和静态库。它们都是将多个目标文件打包成一个文件
- 动态库: 在
Linux
中,.so
后缀为动态库;在Windows
中,.dll
后缀为动态库。 - 静态库: 在
Linux
中,.a
后缀为静态库;在Windows
中,.lib
后缀为静态库。
查看Linux
当前环境中的库文件
ls /lib64
注意,库文件名也是有命名规则:以lib
为前缀,name
为中缀,.so.版本
为后缀(.版本
可能有可能没有),而库真正的名字只有中缀那一块。比如 libstdc++.so.6
去掉前缀跟后缀,最终库名为 stdc++
1.2 动静态链接
既然有了库,那么源文件和库是如何链接的?常见的链接方案是以下两种:
-
动态链接
-
静态链接
1.2.1 动态链接
动态链接的过程其实是将库(动态库)加载到物理内存中。当程序运行时,如果发现程序需要调用动态库中的某个函数或者使用动态库中的某些数据,加载器会将动态库加载到程序的内存空间中,以便程序可以访问其中的函数和数据。这种动态加载的方式允许多个程序共享同一个动态库的实例,因此动态库也称共享库。这也意味着程序在运行时需要依赖于动态库文件,一旦缺失可能导致很多程序无法正常运行。
在Linux
中 默认使用动态链接的方式,我们可以通过 指令ldd
来查看可执行文件所依赖的动态库。
我们还可以通过 file
命令查看文件详细信息。
1.2.1 静态链接
在链接阶段。如果程序调用静态库,链接器会将所需的目标文件从库中选择并提取出来,然后将它们的内容复制到最终的可执行文件。这样,可执行文件中就包含了所有所需的代码和数据,因此在程序运行时,操作系统只需要将可执行文件加载到内存即可,不需要再加载静态库(后续不需要再依赖静态库)。这意味着目标程序可以独立运行并且最终生成的可执行文件会变大,比较占空间,所以静态链接这种方式我们很少使用。
如果想采用静态链接的方式编译程序,需要在编译时加上 -static
选项
gcc [源文件] -o [自己取] -static
因为体积大的原因,有些云服务器可能没有提供静态库,那么可以通过以下指令下载:
# C静态库
sudo yum install -y glibc-static
# CPP静态库
sudo yum install -y libstdc++-static
当然,我们也可以通过ldd
和file
指令查看链接情况
1.3 动静态库总结
注:以上大部分都来自于往期博客:点击跳转
二、如何设计静态库
2.1 制作步骤
【代码示例】
现在我要将cacl.c
封装成库文件,需要经历以下步骤:
- 将函数实现
xxx.c
文件编译成目标文件xxx.o
gcc -c xxx.c -o xxx.o
- 这里解释一下为什么要将
xxx.c
文件编译成目标文件xxx.o
如果没有库的话,其他人要调用你写的方法就只能拿着你的
.c
文件一起编译,这无疑增加了编译代码的时间,而且编译必然逃不过链接的过程。
因此,链接库的过程实际上就是将库文件中的目标文件(通常是.o
文件)与用户程序的目标文件一起链接,形成可执行文件。
- 将生成的目标文件打包成库
ar rc xxx.a xxx1.o xxx2.o ...
其中:
ar
是归档工具的命令。rc
是选项,表示创建(create
)一个新的归档文件,并将指定的文件添加到其中。如果归档文件已经存在,则会替换(replace
)原有的文件。
- 将库打包成目录文件给别人
mkdir -p lib/include/
mkdir -p lib/mylib/
cp xxx.h lib/include/
cp libmylib.a lib/mylib
- 链接静态库
gcc <源文件> -I 头文件路径 -L 库文件路径 -l 库名1 -l 库名2 ...
其中
-
-I
选项:表示指定搜索头文件的路径。如果不指定默认会在路径/usr/include
或者该进程路径下查找 -
-L
选项:表示指定搜索库文件的路径。不指定默认在/lib64
路径下搜索。 -
-l
选项:由于库文件路径下可能存在多个库文件,所以要明确指定库名称。gcc/g++
默认找的就是stdc/stdc++
库
这里就的人会好奇,我们为什么不指定头文件名称?
别忘了,源代码中已经明确告诉编译器gcc/g++
头文件名称了 ~
注意:以上不是官方提供的库,而是由独立的团队或个人开发库我们称之为第三方库,在编译时需要带上选项。
测试调用代码如下:
执行命令,并运行程序查看结果
2.2 安装库
命令写这么长看的都累,你也可以直接自己把头文件和库文件拷贝到系统指定的头文件和库文件路径之下,此时就不需要再指定头文件路径了,系统编译器此时在默认路径下就可以找到,我们称这一步为安装库。注意:安装到系统需要提升用户权限。
但是由于我们的库不是系统级别的库,还是一个第三方库,所以仍然需要指明链接的库的名字
注意: 将自己写的文件安装到系统目录下是一件危险的事(导致系统环境被污染),用完后记得手动删除。(企业开发除外)
三、如何设计动态库
3.1 制作步骤
- 将函数实现
xxx.c
文件编译成目标文件xxx.o
。此时需要加上-fPIC
选项来产生与地址无关码。(在第五部分会讲述为什么要加上此选项)
gcc -fPIC -c xxx.c ...
- 将生成的目标文件
.o
打包成动态库。使用-shared
选项将目标文件打包成动态库。
gcc -shared -o libxxx.so *.o
我们可以注意到,最后生成的动态库是具有可执行权限的,但是它是无法被执行的,毕竟连程序的入口main
函数都没有。
那么这个可执行权限是什么意思呢?
对于静态库,它未来在链接时是直接将静态库中的内容拷贝过去。从此以后这个程序的死活就与这个静态库没有任何关系了。更重要的是他是不会被加载到内存当中的。
而对于动态库,当程序要用到动态库时,就会跳转到动态库,那么注定了动态库需要加载到内存中。而加载就要有可执行权限!
- 将头文件 + 库打包给别人(具体看静态库中的制作步骤)
- 使用动态库。像使用静态库一样使用动态库(指定路径及库名)
gcc xx.c -I 头文件路径 -L 库文件路径 -l 指令库文件名称
接下来查看结果
上面提示:无法打开动态库目标文件。可是我们在编译的时候明确指定了库文件路径以及要连接的库文件名称。
这是因为当前只告诉了编译器gcc
动态库的位置,一旦当程序myexe
形成以后,就和编译器没关系了;当你把执行./myexe
,就意味着程序要加载到内存,这里出现的问题恰好是加载的时候没有找到库。因此,除了编译生成动态库之外,还需要告知系统的加载器动态库的位置。
为什么动态链接的过程中,程序加载到内存还要找到动态库?
- 动态链接的过程中,程序加载到内存后还需要找到动态库,主要是因为在链接阶段,动态链接库的代码并没有被直接包含在可执行文件中,而只是在程序需要时才去动态加载。因此,程序加载到内存后需要知道动态库的位置,以便在需要时能够正确地加载并调用其中的函数。这也就是为什么动态链接可以减小可执行文件的体积,同时也使得动态链接库可以被共享!
- 而对于静态链接,在
gcc
链接时,内容已经被拷贝到可执行文件中,而不需要在运行时再去查找和加载外部的静态库文件(运行时不再依赖静态库)。
3.2 解决程序加载到内存无法找到动态库(四种方法)
- 法一:在
Linux
中,加载器会根据默认的搜索路径(如/lib64 or /usr/lib64
等)来查找动态库,因此可以把动态库放在/lib64
或者/usr/lib64
路径下。(永久有效且最常用)
- 法二:在系统默认的库路径
/lib64 or /usr/lib64
下建立软连接(创建快捷方式)
- 法三:加载器也会根据环境变量
LD_LIBRARY_PATH
来搜索库文件路径。这个路径是搜索用户自定义的库文件路径。因此我们可以添加动态库路径至LD_LIBRARY_PATH
环境变量中。注意:这种方法一旦重启xshell
,原来添加的所有内容都会消失(临时)
- 法四:可以通过配置
/etc/ld.so.conf.d/
目录下的配置文件来指定加载器在程序加载时查找动态库的路径。(永久有效)
在/etc/ld.so.conf.d/
目录下创建一个新的配置文件(通常以.conf
结尾),然后在其中添加需要动态链接器搜索的库文件路径,每个路径占据一行。
# 先将内容写进配置文件
echo 库文件路径 > tmp.conf
# 再将配置文件剪切到/etc/ld.so.conf.d/
sudo mv tmp.conf /etc/ld.so.conf.d/
最后运行以下命令使配置生效:
sudo ldconfig
3.3 总结一波
-
动态库在程序运行时会被动态加载到内存中,只有一份,并且可以被多个进程共享,这样可以大大减少内存占用,特别是当多个进程同时使用相同的动态库时。
-
与此不同的是,静态库在编译链接时会被整体地拷贝到可执行文件中,导致内存占用增加。
四、动态库是如何做到被所有进程所共享
在程序执行过程中,当需要调用动态库中的函数或者访问其中的数据时,操作系统会在磁盘上找到所需要的动态库文件(文件系统),然后加载器会负责将动态库文件直接加载到物理内存,并通过页表映射到进程地址空间的共享区中(专门给动态库用的区域)。
当程序在执行过程中需要调用动态库中的函数时,程序会从自身的代码区跳转到共享区中动态库的代码位置执行相应的函数。一旦函数执行完毕,程序会再次跳转回自身的代码区,继续执行程序的其他部分。所以这样的话,从此我们执行的任何代码,都是在我们的进程地址空间中进行执行的!
除此之外,一个程序可能会链接多个动态库,那么操作系统必定要对这些动态库进行管理,所以要请出管理六字真言:先描述,后组织。所以,操作系统非常清楚所有动态库的加载情况。因此,当未来第二个进程还需要用到这个同样的共享库的时候,就不会再去磁盘重新将动态库文件加载到内存,而是直接将已加载的动态库通过页表映射到新进程的地址空间中,避免重复加载同一份动态库文件到内存中,提高了系统的效率和资源利用率,这就是为什么动态库在内存中只需要一份就够了!
注意:如果这个动态库里面有一个全局变量,那么当一个进程中对这个变量进行了修改以后,根据往期学习进程相关知识,为了确保进程的独立性,必然会发生写时拷贝!
五、-fPIC与地址无关码
一个源程序被编译成可执行文件后,这个可执行文件通常包含了程序的机器指令、数据、符号表等信息。
注:可执行文件中的指令和数据是按照程序加载进程地址空间的偏移量编码的,这个偏移量描述了程序加载到进程地址空间起始位置的偏移量。
在Linux
中,你可以使用objdump
命令来查看可执行文件中的程序信息
objdump -d <可执行文件> # 查看可执行文件的机器指令
objdump -s <可执行文件> # 查看可执行文件的数据
objdump -t <可执行文件> # 查看可执行文件的符号表
而当可执行文件运行起来时,操作系统会为该程序创建一个进程,并为其分配进程地址空间,这个进程地址空间包含了程序的机器指令、数据等其他相关信息;
为了让程序能够正确地在实际的硬件上执行,操作系统还要通过页表(页表是操作系统用来管理虚拟地址到物理地址映射的数据结构)将进程地址空间中的虚拟地址映射到物理内存。
接着CPU
就开始读取可执行文件上的指令,它实际上是在读取虚拟地址。而在涉及函数调用时,机器指令中通常会包含一个跳转指令(例如 call
指令),它告诉CPU
要跳转到哪个地址执行代码。这个地址通常是一个相对地址(相对于当前指令的地址)
-
相对地址跳转:如果跳转地址是相对地址,CPU 会将当前指令的位置与跳转地址相加,得到实际的目标地址。然后,CPU 将程序计数器
PC
(存储下一个目标指令的地址)设置为目标地址,从而执行跳转。 -
绝对地址跳转:如果跳转地址是绝对地址,CPU 会直接将程序计数器(PC)设置为跳转地址,从而执行跳转。
有了以上知识,接下来就可以解释为什么制作动态库时需要加上-fPIC
,即需要设置与位置无关码
一个源程序被编译成可执行文件后,这个可执行文件通常包含了程序的机器指令等,当这个指令时调用动态库的一个函数,随之动态库也就需要加载到物理内存,然后通过页表映射到进程地址空间的共享区。
因为调用动态库函数的机器指令的地址(虚拟地址)是在编译时确定的,但动态库通过页表可能会被映射的位置是不确定的(动态)。那么这样加载到不同位置的话,进程就无法正确调用其中的函数或访问其中的变量,因为它们的地址已经发生了偏移。
因此,我们需要保证动态库能够在虚拟内存中的任意一个位置加载。因此,程序在链接动态库函数时,是通过 动态库起始地址
+ 所链接函数偏移量
的方式进行链接访问的,而这个偏移量就是 fPIC
与位置无关码。
因此,通过使用位置无关码选项,可以确保动态库在被加载到任何地址时都能正确执行。注意:操作系统会管理加载的动态库,因此动态库的起始地址操作系统是知道的!