文章目录
📖 前言
本章将来学习一下动静态库的相关知识,从认识动静态库到自己手写简单版本的库,最后将库发布出去,如何去使用第三库等方面做详细讲解。目标已经确定,接下来就要搬好小板凳,准备开讲了…🙆🙆🙆🙆
1. 认识动态库与静态库
在我们之前学习gcc工具的时候,我们就简单了解过一些关于动静态库的相关知识:👉 【动静态库复习 - 传送门】
- 动态库Linux(. so) windows(. dll): 自己的程序中没有实现,通过链接别人写好的库,调用别的库中的实现。
- 静态库Linux (. a) windows(. lib): 自己的程序中是将库中的相关代码,直接拷贝到自己的可执行程序中!
2. 生成动静态库
库里面需不需要main函数:
库中是不能带main函数的,因为用户写了main函数,一旦把就出现main函数冲突,所以库里面不要main函数。
如果我把我的所有的. o文件
给别人,别人能链接使用吗?
- 链接:不就是把所有的
.o文件
链接形成一个可执行程序吗。 - 而我们又知道只有目标文件是不能将程序执行起来的。
- 必须要链接相应的库,我们之前讲过系统中有对应的库供我们使用。
- 所以,有目标文件又有库,经过链接就可以将程序执行起来。
上级目录的源文件(mymath.c和myprint.c)在当前目录下形成目标文件。
再将当前目录下的源文件(test.c)在当前目录下形成目标文件。
我们再讲这三个目标文件链接本地的库生成可执行程序(a.out)。
小结:
- 如果就是把所有的
.o文件
给别人,别人能连接我的.o文件
来使用,这是可以的。 - 把所有的源文件编译成
.o文件
,再把这些.o文件
给别人,链接的时候将自己的源文件也变成.o文件
,然后将所有.o文件
一链接就形成可执行了。 - 链接的时候本质上就是将三个
.o文件
合起来,形成一个可执行程序。 - 这就叫做静态链接。
要用到的:
2.1 静态库:
上述我们讲了将多个目标文件合起来形成可执行程序。如果包含了几百上千个源文件,在链接的时候容易丢失,所以打包一下就可以了。
ar指令:
在Linux中,“ar” 命令的单词缩写是Archiver"。该命令用于创建和管理静态库(archive) 文件。它通常与其他编译工具(如gcc)一起使用,用于将目标文件打包成一个静态库文件,以供其他程序使用。
静态库的命名:以llib开头 + 名字 + .a
结束。
所以静态库就相当于,源文件生成.o文件
,最后打包起来就可以了。
当别人想用我的时候,就是在我的这个库里面找对应的.o文件
拷贝到自己的的可执行程序当中。
2.1.1 静态库的发布
我们将所有的目标文件归档到静态库中,将库放在一个文件夹里,将所有的头文件打包放在一个文件夹里,整体打包成一个静态库发布出去。
2.2 动态库:
原理与静态库几乎一样,区别就是要用到gcc -fPIC
(产生与位置无关码)。
- 大家编译时,采用的地址空间策略是一样的,都是以4GB的地址空间为单位来排布自己的代码和数据的。
- 静态链接:将自己形成的.o文件拷贝到可执行程序的地址空间范围的某个区域当中就可以了,这种形成的代码叫做与位置有关。
- .o代码,不能在内存的任意位置去加载,必须得是拷贝到程序里面,在程序里面以地址空间的绝对地址的方案呈现在进程层面上,让系统去调用。
- 这就是myprint和mymath编译好之后,采用的与地址有关码。
-fPIC
与地址无关码,将来编译的源代码内部的代码,和将来将这两个.o文件加载到程序任意位置,都可以让程序执行。采用的是起始地址 + 偏移量对的方式,是一种相对地址的方案。
动态库的命名:以llib开头 + 名字 + .so
结束。
- -shared:表示生成共享库格式
- -fPIC:产生位置无关代码(position independent code)
2.2.1 动态库的发布
和上述静态库一样,将头文件和库分别打包最后放在一个文件夹里。
2.3 一并发布:
最后为了统一发布,我们将动静态库统一打包:
.PHONY:all
all:libmymath.so libmymath.a
libmymath.so:mymath.o myprint.o
gcc -shared -o libmymath.so mymath.o myprint.o
mymath.o:mymath.c
gcc -fPIC -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c
gcc -fPIC -c myprint.c -o myprint.o -std=c99
#要注意避免文件名冲突问题
libmymath.a:mymath_s.o myprint_s.o
ar -rc libmymath.a mymath_s.o myprint_s.o
mymath_s.o:mymath.c
gcc -c mymath.c -o mymath_s.o -std=c99
myprint_s.o:myprint.c
gcc -c myprint.c -o myprint_s.o -std=c99
.PHONY:lib
lib:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
mkdir -p lib-dyl/lib
mkdir -p lib-dyl/include
cp *.so lib-dyl/lib
cp *.h lib-dyl/include
.PHONY:clean
clean:
rm -rf *.o *.a *.so lib*
这样就能先make
生成各种.o文件
,再make lib
分别生成动静态库。
看一下目录结构:
3. 动静态库的使用
我们分别将动静态库拷贝到自己的目录中,用自己的代码来试用一下它们。
3.1 静态库的使用:
高亮是因为,gcc没办法找到我们对应的头文件:
" "
是在当前路径下找。< >
是在库目录下面找。
- 如何知道库中有哪些方法,先看头文件里面有哪些头文件~
上述包头文件的方式肯定是不行的:(解决办法)
- 我们当然可以指定头文件的路径,但是那样要写的路径太长了,太麻烦了。
- 将自己写的头文件导入到
/usr/include/
路径下,再将自己的库拷贝到系统库文,/usr/lib64/
。(云服务器都是/lib64
) -
- 不建议这么做,会污染系统库。
- 此时不再报头文件找不到的错误,而是报了链接错误。
- 我们之前没有用过第三方库,而
gcc和g++
默认就认识C/C+ +的库,所以不用指定链哪个库。 - 但是现在链接执行可执行,必须要指定链接哪个库。
gcc mytest.c -lmymath
-l
指明了,要link什么库。- 之前是
gcc和g++
默认会带上链接系统库,而我们写的是第三方库。 - 需要我们手动链接库。
光有这个还是不行的,我们还要指明静态库和头文件的搜索路径,正确使用静态库方法:
gcc mytest.c -o mytest -I ./lib-static/include/ -L ./lib-static/lib/ -lmymath
-L
选项后带的是库的路径。-I
选择后带的是头文件的搜索路径。-l
选项带的是库的名字,库名要去掉前面的lib
和后缀.a
。
如果将静态库安装到系统的库目录下(一般是/lib64/),就不用上述那么麻烦了, 编译的时候只需要用-l
指定库名即可~
gcc mytest.c -lmymath
静态库的特点:
静态库的特点是库中的实现已经被编译链接进入了可执行程序,即便我们将库给删除,也不影响可执行程序的运行。
3.2 动态库的使用:
动态库和静态库的链接方法基本是一样,既可以将动态库安装到系统目录下,也可以如下方式指定去找:
gcc mytest.c -o mytest -I ./lib-static/include/ -L ./lib-dyl/lib/ -lmymath
用法一样,而且选项的含义也是一样的。
不一样的是运行起来之后:
直接运行发现报错了,报错的内容是:这个错误意味着你的程序无法找到名为 “libmymath.so” 的共享库文件。
使用ldd
命令查看mytest
可执行文件的动态库结构,会发现我们写的动态库是没有找到的:
补充:
ldd 是 Linux 系统下的一个命令,用于查看可执行文件或共享库文件所依赖的动态链接库(Dynamic Link Library)。它会列出指定文件所需要的动态链接库及其路径。这个命令在解决程序依赖性问题、查找缺失的库文件或调试系统问题时非常有用。
原因解释:
-I -L -l
是编译器的选项,这些查找属性本身是告诉gcc在哪里找头文件在哪里找对应的lib,找的是什么lib。这些告诉的是gcc,并且形成了可执行程序。- 所以头文件和库的位置只告诉了gcc,程序一旦执行起来,可执行程序就成了进程了,但是没有人告诉进程这个动态库在哪里。
- 程序启动的时候找不到库,因为不在系统库目录里,系统找不到。
3.3 程序运行时找到动态库:
3.3.1 导入到系统路径下
- 当程序在运行时需要加载第三方动态库时,操作系统会在默认的库搜索路径中查找该库。
- 如果库文件没有在默认的搜索路径中,就会导致找不到库的情况。
- 将动态库拷贝到系统库目录下,一般都是安装到
/lib64
下,不推荐因为会污染系统库。
3.3.2 环境变量LD_LIBRARY_PATH
通过修改环境变量的方式来实现增加动态库的查找路径:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/Zh_Ser/linux/lesson22/test/lib-dyl/lib
此时环境变量就导入成功啦。
我们再ldd
,和运行一下看看:
ldd命令的结果也显示出了我们自己写的动态库的路径。
但是依旧有个缺点,在Shell重启之后导入的环境变量就不见了,还要重新导入。
3.3.3 修改系统配置文件
通过配置文件来向系统表明要向哪里搜索。
除了修改环境变量,我们还可以修改/etc/ld.so.conf.d
下的文件:
这个路径下表明的是,系统里面如果自定义了动态库,那么此时系统在扫描系统的路径时,除了在系统对应的库路径扫描对应的库之外,还去一个一个的读取这里面的配置文件,根据配置文件里面的内容来找到对应的动态库。
这里的操作非常简单,我们只需要在该目录下新增一个.conf文件
,并在里面写入动态库的绝对路径即可~
sudo touch /etc/ld.so.conf.d/my.conf
sudo vim /etc/ld.so.conf.d/my.conf
- 进入插入模式把自己自定义的动态库的路径粘贴进去。
- 我们刚刚写的配置文件里保存的是自己写的动态库的路径。
- 然后ldconfig指令,让这个文件生效。(sudo一下)
sudo ldconfig #子用户权限不够,需要加sudo
- 执行完之后就会默认的在我们系统的内存当中帮我们把刚刚自己新定义的conf加载到内存里。
- 再
ldd mytest
,我们就能看到libmymath.so =>我们自己写的库的路径
。 - 此时再运行就可以跑了,甚至将Shell关掉,重启之后还是能跑。
3.3.4 创建软连接
在/lib64
下创建一个软连接来帮我们查找动态库。
sudo ln -s /home/Zh_Ser/linux/lesson22/test/lib-dyl/lib/libmymath.so /lib64/libmymath.so
- 在Linux系统中,链接动态库时,默认情况下会搜索一些默认的路径来查找所需的动态库文件。
- 其中,一种常见的默认路径是 /lib 和 /lib64。
- 而我们则是在
/lib64/
下创建一个我们自己写的动态库的快捷方式libmymath.so
。
4. 动静态库对比
动静态库优缺点对比看之前我的博客,👉 【动静态库复习 - 传送门】。
接下来我们要好好讲讲为什么动态库在程序运行时要找库,而静态库则不需要。
4.1 为什么运行时找动态库:
刚刚我们讲了当可执行程序运行起来时,找不到动态库的四个解决办法,但是具体为什么会运行时找不到,我们来讲讲原因。
程序和动态库,是分开加载的:
- 当你运行一个可执行程序时,操作系统会加载可执行程序本身,并为其分配内存空间。然后,动态链接器会在需要的时候加载所需的动态库。
- 动态库是在运行时动态加载的,也就是说,在程序执行过程中,如果有代码调用了动态库中的函数或使用了其中定义的变量,动态链接器才会将相应的动态库加载到进程的内存空间中。动态链接器会解析可执行程序中的动态库依赖关系,并按照依赖关系来加载动态库。这样可以实现动态共享代码和资源的重复利用,减小可执行程序的大小。
- 动态库的加载是延迟的,即只有在需要时才加载。如果某个动态库在程序运行的过程中没有被使用到,那么它也不会被加载到内存中。
- 总结起来,可执行程序在运行时首先加载,然后动态库在需要时被加载,以满足程序对其中定义的函数和变量的调用和访问需求。这种分离加载的机制提供了更高的灵活性和代码复用性。
程序在运行期间可能找不到库的原因是:
- 程序和库是分开加载的,运行程序,程序形成进程,将程序代码加载到内存里。
- 同时也要将自己写的库加载到内存里,如果库不存在的话,还要映射到进程上下文当中。
将库文件加载到内存里,前提条件是:
- 进程运行的时候,如果要动态加载它需要的库。
- 前提条件是:先找到这个库在哪里。
- 要建立地址空间和库的映射关系。
- 我们生成可执行程序,有的代码是需要跳转到库中执行的!
- 用多少加载多少,必须将库加载到内存中去。
所以这个库如果要被加载,就必须要被找到, 所以就倒逼着进程在运行时,必须在运行的时候找对应的库,找不到就报错,进程直接终止。
所以在我们代码区域对库进行调用时,也是在进程地址空间当中进行来回跳转。
动态库
shared libraries
因为可以在运行期间被多个进程所共享,所以叫做共享库。
4.2 静态库不用找的原因:
如果是静态链接,每个进程都有一份代码,就是将库代码编进了自己的代码当中。
- 在编译可执行程序时,静态库的代码和数据会被完整地复制到可执行程序中。
- 当使用静态库时,编译器将库中的目标文件(已经经过编译的代码)直接嵌入到最终的可执行程序中。
- 这意味着在编译可执行程序时,静态库的代码和数据就已经被加载到可执行程序中,并成为可执行程序的一部分。
- 由于静态库在编译时被静态链接到可执行程序中,所以在运行时并不需要额外加载动态库。可执行程序已经包含了静态库的完整副本,所有所需的函数和符号定义都可以在可执行程序内部找到。
优点:
- 这种静态链接的方式使得可执行程序更加独立,不需要依赖外部的动态库文件。
- 同时,静态库的使用也简单直接,不需要考虑动态库的查找和加载过程。
缺点:
- 然而,静态库的缺点是占用了更多的磁盘空间,并且无法进行更新或替换。
- 每个使用该静态库的可执行程序都会有一份完整的库的副本,这可能导致可执行程序的大小增加和资源重复浪费。
动态库中形成的目标文件是加了-fPIC
选项:
-fPIC
是一个编译选项,用于告诉编译器生成位置无关代码(Position Independent Code)。- 这对于生成共享库(shared library)或动态链接库(dynamic link library)非常有用,因为这些库在不同的内存地址空间中加载,并且需要解析和重定位符号表。
-fPIC
选项的作用是将编译生成的目标文件中的代码和数据访问都使用相对地址,而不是绝对地址。- 这样做的好处是,当共享库被加载到内存中并进行地址映射时,不需要修改库中的代码和数据,而只需要调整引用该库的程序中的全局偏移量即可。
- 这使得共享库能够以更加灵活和独立的方式被多个程序共享使用。
5. 总结
- 无论动静态库在使用的时候,用
gcc
生成可执行程序时,都要指明库所在的路径。 - 而在运行时则是:静态库不需要再去找库的位置,而动态库则需要再找库的位置。
- 静态库是在编译时加载到可执行程序中的,当可执行程序启动时,静态库的代码已经完全集成在程序中,不需要额外的加载过程。
- 而动态库则是运行时才加载到可执行程序中的,当可执行程序启动时,操作系统会根据程序中引用的动态库的名称和路径来查找并加载这些动态库。