目录
引入:
本文虽会详细介绍动静态库的相关知识,但是gcc的相关语法以及编译的细分4小步,和此篇博客的一大点的内容,也是在下面博客中说过了:Linux环境基础开发工具->gcc/g++-CSDN博客
所以一定要看,不然肯定看不懂此篇博客
一:动静态库的介绍
1:库的本质
函数所处的.c文件编译后会形成.o文件,当多个.c形成多个.o后,这多个.o的集合就叫做库!!
多个.o并不是放在那里就是一个库,要分别用不同的方法才能让这多个.o去形成动态库或静态库!(在后面会讲解)
2:库的类别及优缺点
库一般分为静态库和动态库两种:
①:静态库是指编译链接时,把库文件的代码全部加入到可执行文件当中,因此生成的文件比较大,但在运行时也就不再需要库文件了,静态库一般以.a为后缀。
②:动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件当中,而是在程序运行时由链接文件加载库,这样可以节省系统的开销,动态库一般以.so为后缀。
二者的优缺点:
动态链接:
优点:省空间(磁盘的空间,内存的空间),且体积小,加载速度快。
缺点:依赖动态库,程序可移植性较差。静态链接:
优点:不依赖第三方库,程序的可移植性较高。
缺点:浪费空间。
3:动态链接
Q:那我们写的.c 和 .cpp文件在gcc进行链接操作的时候,是链接的哪一种库?
A:file指令就能知道
解释:dynamically linked 意味着动态链接,所以链接的是动态库!
4:静态链接
我们还可以强行对code.o文件进行静态链接到库,指令:gcc -static
注:当然也可以直接对code.c进行强行的静态链接,自由选择即可
gcc code.o -o code.out_static -static//选项-static
我们静态链接生成的可执行文件取名为 code.out_static,方便区别于code.out
此时我们就有两个可执行文件了:
解释:由静态链接的缺点可知,静态链接是把库文件的代码全部加入到可执行文件当中,所以这就是为什么我们的code.out_static的大小是861288,远远大于code.out的原因!
我们可以通过ldd来查看code.out_static是否真的被静态链接了:
解释: 信息告诉我们:这个程序是静态链接的,没有依赖任何动态库(.so
文件),因此无法列出动态库依赖关系;所以这正好说明了的确进行了静态链接
再通过file来看一下code.out_static:
解释:statically linked 意味着静态链接
总结:Linux中的编译器gcc和g++默认都是动态链接的!需要静态链接需要加 -static选项
二:头文件和库的查找
我们在使用库中方法的时候,第一件事情,就是要找到方法所处的库和声明方法的头文件,这一点,是毋庸置疑的!不然你的程序怎么跑?你用的方法,找不到声明和实现,肯定出错,而且是链接式报错!
Q1:为什么是链接式报错
A1:因为,链接的时候,就是找库的时候,所以找不到库,会报链接错误!
而当我们进行gcc的时候,直接生成了可执行程序,仿佛我们不用找库,不用找头文件?
其实不然,gcc只是帮我们做了这些事情,所以下面我们要了解gcc是怎么去查找库和头文件的!!!!
查找头文件:
gcc会去两个地方查找,一是默认搜索路径,二是main函数所处的.c文件的当前目录
查找库:
gcc只会默认搜索路径下查找
Q2:默认搜索路径是什么?
A2:头文件的默认搜索路径就是Linux默认存储头文件的目录的绝对地址;同理,库的默认搜索路径就是Linuxx默认存储库的目录的绝对地址!
这就是为什么我们在Linux下的代码使用了C方法或者C++方法的时候,只需gcc编译,就能形成可执行程序,因为gcc能够在这两个搜索路径下找到对应的库和头文件!
所以像C和C++的库,都是已经被放在默认搜索路径中的!
Q3:你讲这么多,我理解了去默认搜索路径下找头文件和库,但是你还说头文件还能在main函数所处的.c的同级目录下找,这是什么意思?
A3:这点在后面会验证,但是现在也可以进行解释,我们用一个在vs中编写代码的例子来解释,
add.h
#pragma once
int add(int, int);
add.c
int add(int a, int b)
{
return a + b;
}
main.c
#include<stdio.h>
#include"add.h"
int main()
{
int sum = add(1, 2);
printf("%d", sum);
return 0;
}
运行结果:
这个例子中,我们头文件并不在什么默认搜索路径下,而是就在我们的main.c的同级目录下,如下:
但是我们的程序依旧正确的运行起来,这就证明了在编译的时候,其还会在当前目录下找头文件!
而且在main.c中,我们包含add.h的格式,和包含stdio.h的格式不同:
#include<stdio.h>
#include"add.h"
这是官方对于默认搜索路径下的头文件,和在当前目录下的头文件,提供的不同的格式,这也证明了的确是有在当前目录下找头文件的行为
总结:
查找头文件:
①:默认搜索路径
②:当前目录
查找库:
①:默认搜索路径下查找
而以上这些都是对标准库和标准库对应的头文件的查找方法,这些是Linux已经存储好的文件,所以按照上面的查找方法必然都能够找到!
那请问如果是我们自己制作出的库和头文件呢,很显然,其按照这些方法,是必然找不到的!因为自己做的库和头文件不会在默认的搜索路径下!
在下面的第三和第四大点中,我会自己去实现一个静态库和动态库,并且能够正确的使用自己做出来的库,所以不妨在这里,就先介绍一下,如果让gcc找到我们自己定义的库和头文件!
三个选项:
-I
:指定头文件搜索路径 (大写字母 i)-L
:指定库文件搜索路径 (大写字母 L)-l
:指明在-L的
路径下的哪一个库-l
(小写字母 l)
当我们的gcc带上这三个选项,并且在选项后面跟着正确的路径或库名字的时候,就能够找到我们自己制作的库和头文件!因为我们已经清晰的告诉了gcc我们的头文件和库在哪!
三:静态库的制作和使用
1:制作
首先我们已经介绍了库是一堆.o文件的集合,而静态库让这堆.o集合形成静态库的方法就是打包!
指令为:
ar -rc//不存在则创建该.a静态库 存在则替换该.a静态库
现在有这么一个场景,我写了一个加法和减法的.c和.h,如下:
add.h:
int add(int, int);
add.c:
int add(int a, int b)
{
return a + b;
}
sub.h:
int sub(int, int);
sub.c:
int sub(int a, int b)
{
return a - b;
}
现在有一个用户,他需要我写的加法和减法,所以我就要把我的这两个方法打包为一个库给他,然后再把头文件也给他,这样他就能直接使用我的方法了
但是任何的方法或者函数,都不是直接把.c给用户,而是将其变成.o后打包成库给用户
原因主要是两点:
①:保护我们的实现代码(因为.o文件都是二进制文件,所以起到保护作用;)
②:让客户操作简单(直接打包成库给用户,用户直接使用即可,而不是还要手动的自己打包成库)
所以现在我们先让这两个.c形成.o:
gcc -c add.c
gcc -c sub.c
下一步我们应该进行打包,但是我们可以先尝试下不打包以体现对于用户的不便性!
现在来了个用户,其已经写好了main.c了,就差我们把所需的东西给他了:
注:main.c在user的下级目录
用户的main.c如下:
//用户的main.c
#include<stdio.h>
#include"add.h"
#include"sub.h"
int main()
{
int sum = add(20,10);
int dif = sub(20,10);
printf("sum=%d dif=%d\n",sum,dif);
return 0;
}
所以既然我们不打包,我们就直接把两个.o和两个.h给他:
cp add.o ./user
cp sub.o ./user
cp add.h ./user
cp sub.h ./user
现在用户就要开始形成自己的可执行程序了,所以其要先把自己的main.c形成.o:
gcc -c main.c //-c 代表把.c形成.o文件
然后现在.o文件都有了,下一步就是链接形成可执行程序了:
gcc main.o add.o sub.o //gcc形成可执行程序
成功的生成了a.out这个可执行程序,运行效果如下:
解释:达到了用户所需的效果
但是如果某个场景中,.o文件多达数百个,用户在网站中下载我们这一大堆零散的.o文件,出现遗漏,那造成的后果不堪设想!
2:指令打包
所以为了避免这种场景,大家都会选择一种做法:
将.o打包形成一个库再放进lib目录中,再将.h都放进一个include目录中,最后再把lib和included都放进一个目录中(lib就是库的缩写,include代表存放头文件的目录)
树形图如下:
所以指令如下:
ar -rc libcal.c add.o sub.o //让两个.o形成一个名为libcal.c的静态库
mkdir -p mathlib/lib //在当前目录下创建一个mathlib/libm目录
mkdir -p mathlib/include //在当前目录下创建一个mathlib/include目录
cp ./*h mathlib/include/ //将当前目录下的所以.h文件拷贝到mathlib/include中
cp ./*o mathlib/lib/ //将当前目录下的libcal.c静态库 拷贝到mathlib//lib中
所以,这就是为什么,往往你在网上找到一个mod或者小程序,你去下载的时候,会发下你其有很多文件夹,并且文件夹里面还有文件夹,本质就是和我们的做法大同小异!
注:我们生成这个树状图的过程叫做"发布"!
3:makefile打包
当然,这些所有的指令,我们都可以全部在makefile中完成!当我们以后再要生成静态库以及组织头文件和库文件时就可以一步到位了,不至于每次重新生成的时候都要敲这么多命令,这也体现了Makefile的强大。
会用到一个新的指令make output ,叫做"发布",当我们需要发布一个库的时候,就需要使用make output 指令,其内部一般会执行的就是我们上文说的:"将.o打包形成一个库再放进lib目录中,再将.h都放进一个include目录中,最后再把lib和included都放进一个目录中!"
另外,需要先make生成静态库,然后才能make output进行发布!
毕竟你连库都没有生成,谈何发布?
makefile如下所示:
无注释版:
mylib=libcal.a
CC=gcc
$(mylib): add.o sub.o
ar -rc -o $(mylib) $^
%.o: %.c
$(CC) -c $<
.PHONY: clean
clean:
rm -f $(mylib) ./*.o
.PHONY: output
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include
cp ./*.a mathlib/lib
注释版本:
# 定义静态库名称
mylib=libcal.a
# 定义使用的编译器
CC=gcc
# 默认目标:构建静态库
$(mylib): add.o sub.o
# 将add.o和sub.o打包成静态库libcal.a
# -rc 表示创建新库(r)并添加文件(c)
# -o 指定输出文件名
# $^ 表示所有依赖文件(add.o sub.o)
ar -rc -o $(mylib) $^
# 模式规则:从.c文件生成.o文件
%.o: %.c
# 编译C源文件生成目标文件
# -c 表示只编译不链接
# $< 表示第一个依赖文件(%.c)
$(CC) -c $<
# 伪目标:清理生成的文件
.PHONY: clean
clean:
# 删除静态库和所有.o文件
rm -f $(mylib) ./*.o
# 伪目标:组织输出目录结构
.PHONY: output
output:
# 创建include目录
mkdir -p mathlib/include
# 创建lib目录
mkdir -p mathlib/lib
# 复制所有.h文件到include目录
cp ./*.h mathlib/include
# 复制所有.a文件到lib目录
cp ./*.a mathlib/lib
makefile的效果:
目前的状态:
make后:
mak output后:
符合预期!
4:使用
谈了这么多,也终于到使用我们的库的时候了!当然,客户肯定使用gcc来编译我们发布的库!
现在用户来了:
其第一步就是把,mathlib这个目录拿走(类似于下载),所以指令如下:
mv mathlib/ user/ //模拟用户的下载行为
现在用户已经准备好了.o,也有了mathlib这个目录,其内部有库有头文件
现在我们知道其肯定是需要使用gcc的三个选项的,因为库和头文件都不在默认搜索路径下,头文件也不在main.c的同级目录下!但是我们还是模拟一下错误的过程吧:
①:直接gcc
解释:报错找不到头文件!因为gcc默认只在当前目录和系统路径中找头文件
②:仅-I
解释:报错add
和 sub
函数未定义!因为虽然找到了头文件(声明了函数),但 未链接函数实现的库(libcal.a
或 libcal.so
),导致链接器找不到函数定义。
③:仅-I -L
解释:报错找不到库!-lcal
会让链接器查找 libcal.a
或 libcal.so
,但未用 -L
指定库路径,链接器只在系统默认路径(如 /usr/lib
)中查找,而你的库在 mathlib/lib
中。
这里涉及到一个库的名字的获取规则,我们的库为libcal.a,但其实这个库的名字为cal,因为去掉前缀lib,去掉后缀.a,因为任何库都是lib前缀,任何静态库都是.a后缀 ,所以-I的时候要去掉!!
④:三个选项齐全
解释:符合预期,未报错!
四:动态库的制作和使用
动态库的制作和打包与静态库有些许的不同,共两点:
①:gcc形成.o文件,需要加上 -fPIC选项
②:形成库不再用ar指令打包,而是使用gcc的-shared选项即可打包
1:制作
先生成.o文件
gcc -fPIC -c add.c
gcc -fPIC -c sub.c
//动态库生成.o 一定要带-fPIC选项
2:指令打包
对.o文件进行打包:
gcc -shared -o libcal.so add.o sub.o //动态库-shared选项即可打包
然后将.o打包形成一个库再放进lib目录中,再将.h都放进一个include目录中,最后再把lib和included都放进一个目录中
gcc -shared -o libcal.so add.o sub.o //形成动态库
mkdir -p mathlib/include //创建目录
mkdir -p mathlib/lib //创建目录
cp ./*.h mathlib/include //将所有头文件拷贝到mathlib/include中
cp ./libcal.so mathlib/lib //将动态库拷贝到mathlib/lib中
树形图如下:
和静态库一样的套路,我们依旧换成makefile来进行
3:makefile打包
无注释:
mylib=libcal.so
CC=gcc
$(mylib): add.o sub.o
$(CC) -shared -o $(mylib) $^
%.o: %.c
$(CC) -fPIC -c $<
.PHONY: clean
clean:
rm -rf $(mylib) ./*.o
.PHONY: output
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include
cp ./*.so mathlib/lib
含注释:
# 定义使用的编译器
CC=gcc
# 默认目标:构建动态库
$(mylib): add.o sub.o
# 将add.o和sub.o打包成动态库libcal.so
# -shared 表示生成动态链接库
# -o 指定输出文件名
# $^ 表示所有依赖文件(add.o sub.o)
$(CC) -shared -o $(mylib) $^
# 模式规则:从.c文件生成.o文件(用于动态库需要-fPIC选项)
%.o: %.c
# 编译C源文件生成位置无关代码(PIC)的目标文件
# -fPIC 生成位置无关代码(Position Independent Code)
# -c 表示只编译不链接
# $< 表示第一个依赖文件(%.c)
$(CC) -fPIC -c $<
# 伪目标:清理生成的文件
.PHONY: clean
clean:
# 删除动态库和所有.o文件
rm -rf $(mylib) ./*.o
# 伪目标:组织输出目录结构
.PHONY: output
output:
# 创建include目录
mkdir -p mathlib/include
# 创建lib目录
mkdir -p mathlib/lib
# 复制所有.h文件到include目录
cp ./*.h mathlib/include
# 复制所有.so文件到lib目录
cp ./*.so mathlib/lib
makefile效果:
make后:
make output后:
符合预期!
4:使用
不再演示三个选项缺失的报错了,直接使用三个选项吧~
gcc main.c -I./mlib/include -L./mlib/lib -lcal
生成了a.out
执行a.out
解释:竟然报错了?!!报错加载共享库时出现错误,也就是找不到动态库!
用ldd指令看一下链接的库的信息
这就是动态库和静态库的区别!!
五:系统查找动态库
a.out无法执行的原因是因为,操作系统找不到动态库!
是的,不仅gcc需要找动态库,OS也需要找动态库!
Q1:那静态库的时候,为什么没有报错?OS不找静态库?
A1:静态库的特点就是已经写在了代码里,所以不需要找,代码中就有,而动态库,没有写在代码里,所以,我们的三个选项只是告诉了gcc编译器位置,而没有告诉OS位置!
需要明白的是,gcc要找动态库/静态库和头文件,而OS只需要找动态库!因为头文件只有在编译的时候才有用,在本文最开始 引入中的博客中谈过,编译的第一步预处理才需要包含头文件,所以OS只需要找动态库!
Q2:那OS找动态库和gcc找动态库的方式有什么区别?
A2:无任何区别,依旧是去库的默认搜索路径去找库!因为你没告诉OS,所以OS不知道!
Q3:第一大点中说过gcc是默认动态链接的,也就是是链接到动态库的,那为什么我们写代码的时候,OS没有报错找不到动态库?
A3:因为不管是什么库,只要是标准库,就已经被存放在了默认搜索路径下的目录中,OS找得到!!
Q4:那怎么告诉OS我们写的动态库在哪?
A4:四种方法!
四种方法,都会在使用方法前后进程ldd指令的对比,因为ldd指令可以查看os是否找到了动态库!!!
1:拷贝到默认搜索路径下
这种方法是最简单的,也是最好理解的,既然你OS和gcc都要先去默认搜索路径下找库,那我干脆直接把库放进默认的路径中,但是这种方法需要sudo或者root才可以:
操作如下:
如果你是centos,则你:
sudo cp mathlib/lib/libcal.so /lib64
如果你是ubuntu,则你:
sudo cp mathlib/lib/libcal.so /lib/x86_64-linux-gnu/
因为不同版本下,OS对动态库的默认搜索路径不一样~
如何把1的操作去除:
以下每个操作后,都会去除上种方法的效果,避免影响后面的方法效果!
2:修改环境变量修改
和环境变量PATH类似,PATH是找可执行程序的路径,而找动态库的路径也是可以修改对应的环境变量的
指令如下:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxxxx/mathlib/lib
//xxx代表你的mathlib所处的路径
所以我的指令如下:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:mathlib/lib/
效果如下:
注:你是给 LD_LIBRARY_PATH 这个环境变量一个路径,所以你填写到 mathlib/lib/ 就嘚停止,而不是mathlib/lib/libcal.so!!
如何去除2的效果:
unset LD_LIBRARY_PATH
因为我的这个路径本来就没值,所以可以直接使用unset指令清空
但是如果你的环境变量里面有值,则你需要:
export LD_LIBRARY_PATH=$(echo $LD_LIBRARY_PATH | sed 's|:mathlib/lib||g')
或者直接关闭客户端重启即可
3:软链接
sudo ln -s /home/wtt1/lesson4/user/mathlib/lib/libcal.so /lib/x86_64-linux-gnu/libcal.so
注:
①:格式:
ln -s <源文件绝对路径> <目标位置完整路径>
②:必须指定链接文件的完整路径和名称(不能只写目录路径,精确到动态库文件)
效果:
如何去除?
sudo rm /lib/x86_64-linux-gnu/libmyc.so
4:动态库路径配置文件
我们可以通过配置/etc/ld.so.conf.d/路径下的文件,来让OS找到动态库!
/ld.so.conf.d中的ld译为加载,so译为动态库,conf.d译为配置文件的目录
所以名字就很好理解,"加载动态库的配置文件的目录"
/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。
ls该目录如下图:
解释:该路径下全是.conf结尾的文件,而我们需要做的就是再创建一个.conf为后缀的文件,该文件的文件名随便取,假设我就取wtt1.conf
所以我就在当前目录创建一个wtt1.conf 然后将其mv到/etc/ld.so.conf.d/路径下:
sudo mv wtt1.conf /etc/ld.so.conf.d/
Q:那文件的内容写什么?
A:就写你动态库所处的路径即可!
echo /home/wtt1/lesson4/user/mathlib/lib > /etc/ld.so.conf.d/wtt1.conf
//将动态库所处的路径 写进/etc/ld.so.conf.d下的配置文件wtt1.conf
此时ldd a.out 发现依旧没有生效:
因为还要执行sudo ldconfig 指令才行:
解释:ldconfig 指令会让配置文件生效!
至此,4种让OS找到动态库的方法介绍完了!
官方下载的库建议用第一种方法,自己实现的库建议用第三z种方法
六:动静态库的优先级规则
①:如果我们同时提供动态库和静态库,gcc默认使用的是动态库
解释:可以在同时有t同名的动态库和静态库下验证,但是我们在第一大点中就已经验证了,默认的gcc形成的程序,进行ldd指令或者file指令,提示消息都表明了其是动态链接!
②:如果我们非要静态连接,我们必须使用static选项③:如果我们只提供的静态库,那我们的可执行程序也没办法,即使你不指名static,其也会对该库进行静态连接,但是程序不一定整体是静态连接的,因为不止链接这一个库
④:如果我们只提供动态库,默认只能动态连接,非得静态连接,会发生连接报错
七:动态库的加载
上面六点,我们介绍了这么多关于动静态库的知识,所以下面大致讲解一下动静态库加载到内存中之后是如何影响进程的内核数据结构的!
1:共享区的作用
共享区就是磁盘中的动态库被加载到内存,然后通过页表映射到进程的进程地址空间的位置!所以动态库又被叫做动态库!
2:编址
Q:我们都知道进程地址空间存放的是虚拟地址,那请问进程地址空间的虚拟地址,是谁给他的?你任何一个值,首先得被初始化才有吧?
A:程序编译期间就会形成虚拟地址,对你没听错,程序还没成为进程,还没占用内存,其仅仅是在编译期间,就会让每行代码都有自己的虚拟地址
比如下面是一个test.s的文件,也就是远远还没有形成.o的时候,其就已经有地址了:
解释:这是在反汇编下观察到的结果,能够看出其的确是有很多的地址,这个行为就叫作"编址"!
编址分为两种:绝对编址(又叫作平坦模式)和相对编址(又叫作逻辑编址)
绝对编址就是像上图中这样,从一个地址开始,整个程序都是递增式的地址,又称为“平坦模式”!上图中的401010 --->401015--->401019--->401020 是连续的,所以是绝对编址
而相对编址,每一个代码块会有一个起始地址,然后该代码块里面的每句代码前面不是地址,而是偏移量,所以一句代码的地址,就是该句代码位于的代码块的起始地址+偏移量
所以现在我们知道了,一个.c中的所有代码和数据和变量等等一切东西,都会在编译期间,就为其分配好了虚拟地址!所以此时当程序加载到内存中的时候,由于其被加载到内存中,所以其就能得到自己在内存中的物理地址,所以现在即有了物理地址,又有了虚拟地址,所以能够通过页表映射到进程地址空间!
而我们知道静态库是存在于代码里面的,编译的时候,静态库就会在直接拷贝进main.c文件中,而动态库却是被加载到内存,然后映射到进程地址空间的共享区的,所以这里面分别涉及到了绝对编址和相对编址!
3:动态库的加载过程
静态库在编译链接时,库中所有代码已经被拷贝到了用这个库的main.c函数中,所以静态库会随着.c文件一起被采取绝对编址的方式进行编址。当程序加载时,操作系统直接按照形成的可执行文件中绝对编址得到的虚拟地址,将静态库的代码和数据段映射到进程的进程地址空间中!页表将这些虚拟地址转换为物理内存地址,形成映射!
动态库不随mian.c文件被绝对编址,其是位于磁盘中的一份独立文件,而且动态库通过-fPIC选项编译,所以其在编译的时候,在内部使用相对偏移进行编址。加载时,操作系统将动态库的代码映射到进程地址空间的共享区。而因为其是响度编址,所以任何方法都会有一个起始地址,方法中的语句都会有一个偏移量,所以当你的代码中调用的库的函数的时候,此时,当cpu读取到这行调用库方法的代码时,其会得到一个起始地址和偏移量,然后跳转到共享区对应的虚拟地址处,然后再通过该虚拟地址和页表找到在内存中的具体实现方法,从而实现调用!