前言
在日后的项目中,我们会用到很多第三方库。人们常说,开发就是在用轮子,而这个轮子其实就是第三方库,但是我们作为学习阶段,我们还需要学会如何造轮子,这样才会对轮子更为了解,能更好的掌握,所以本篇博客就记录一些动静态库的知识。
文章目录
- 前言
- 一. 库
- 二. 模拟静态库
- 1. 简单程序
- 2. 打包
- 3. 解包并使用
- 3. 默认搜索路径
- 三. 模拟动态库
- 1. 形成可执行
- 1. 运行时链接
- 1. 环境变量
- 2. 软链接
- 3. 配置文件
- 四. 动静态库的加载
- 1. 静态库的加载
- 2. 动态库的加载
- 五. 琐碎小知识
- 结束语
- 结束语
一. 库
我们用过库吗?
答案是肯定的,我们编写C/C++代码时,所使用的C标准库,C++输入输出流,这些都是库。
我们在编写C/C++代码时,经常是将函数的定义写在 .h文件
中,函数的实现写在 .c或者 .cpp
中。
而.h文件,我们叫做头文件
.c / .cpp 在预处理,编译,汇编后形成的二进制文件就是库文件
在Linux下,我们就可以看一下我们有哪些头文件和库文件
我们在下载VS等编译器时,就需要下载语言的库
系统已经预装了C/C++的头文件和库文件,头文件提供方法说明
,库提供方法的实现
,头和库是有对应关系的,是要组合在一起使用的
头文件是预处理阶段就引入的,链接的本质其实就是链接库
二. 模拟静态库
在Linux下,静态库的后缀是 .a,动态库的后缀是 .so
在Window下,静态库的后缀是 .lib,动态库的后缀是 .dll
在Linux下,我们看到的动态库是这样的
但实际上,动态库的文件名需要去掉lib前缀
,.及后面的后缀
。命名时,第一个 . 后面必须是 so
所以这个动态库的名称是stdc++
1. 简单程序
我们简单提供加减功能的包
首先编写 .h 和 .c 文件
简单使用一下
这里main.c内部包含了myadd.h和mysub.h的头文件,所以会在main.c中展开。
main.c,myadd.c和mysub.c会预处理,编译,汇编,形成二进制文件
,然后因为main.c中调用了相关函数,所以会对另外两个二进制文件进行链接
,链接相关函数,最后形成可执行文件
但是,一般的库,除非开源,不然不会直接提供源代码,而是提供汇编后形成的二进制文件,这样可以防止机密的泄漏。因为最后是实现二进制文件的链接。
所以我们尝试以写库的角度,模拟给用户提供库
我们将 .c文件转成 .o,并且提供给otherPerson
仅有 .h 和 .o 文件,同样可以形成可执行。形成可执行本质就是链接 .o 文件
但是提供这么多文件非常的难看,我们更应该打包交给用户
2. 打包
将所有的 .o 文件打包
ar -rc lib前缀+静态库名+ .a后缀
-r选项是replace
,在有部分-o文件改变时,可以只替代部分
-c选项是create
,没有该静态库时创建
但是第三方库,在 gcc/g++ 时,编译器在默认路径下找不到 libmymath.a这个静态库,所以会产生链接错误。
所以我们要告诉编译器我们要使用这个第三方库
gcc -o mytest main.c -L. -lmymath
-L
选项,告诉在哪个路径
下搜索
第三方库
-l
指明第三方库的库名
,注意:此处的库名需要去掉lib前缀和.a后缀
但是,实际中,.h 文件一般放在include文件夹,二进制文件 .o 文件放在lib文件夹,
然后将这两个文件夹打包,变成一个压缩包
打包:
tar -czf mymath.tgz lib include
3. 解包并使用
现在我们有这个加减法库的压缩包,那我们如何使用呢?
解压:
tar xzf 压缩包
接下来,编译需要指明头文件路径和库文件路径
gcc -o mytest main.c -I./include -L./lib -lmymath
-I(大写i)
指令:指明头文件路径
-L
指令:指明库文件路径
-l(小写l)
指令:指明链接的库
所以,第三方库的使用
需要指定的头文件和库文件
- 如果没有默认安装到系统gcc,g++默认的搜索路径下,用户必须指明对应的选项,告知编译器:
a. 头文件在哪 b. 库文件在哪 c. 要链接的库的名称
3. 默认搜索路径
gcc / g++ 的
头文件
默认搜索路径之一是/usr/include
库文件
的默认搜索路径之一是/usr/lib64
但是编译时,还是需要指明要链接的静态库的名称
在Linux下,安装库就是将头文件和库文件下载到编译器默认的搜索路径下
卸载则是从搜索路径下去掉
如果我们安装的是第三方库(非语言,非操作系统系统接口)库,我们要正常使用,即便是已经全部安装到了系统中,gcc / g++ 必须使用-l 指明具体库的名称
!
三. 模拟动态库
1. 形成可执行
动态库的 .o文件,有特殊的形成指令
gcc -fPIC -c 文件
-fPIC:形成的 .o :与位置无关码
形成动态库
gcc -shared -o 动态库名 .o文件
-shared选项:表明要形成动态库
我们照样将 lib 和 include这两个文件夹打包,交给用户。用户解压获得这两个文件夹,编写自己的main函数使用。
但是此时无法运行这个可执行文件
为什么我们已经形成了可执行程序,但无法运行呢?
这是因为,前面的
编译
,我们告诉了编译器我的动态库在哪,所以成功形成了可执行程序,但是现在是运行
,运行我们并未告诉操作系统
,我们要使用的第三方库在哪,所以仍然在链接时出现链接失败
。
那为什么静态库就能找到呢?
因为静态库的链接原则:将用户使用的二进制代码直接拷贝到目标可执行程序中,但是动态库不会!
1. 运行时链接
所以要想运行使用第三方动态库的程序,必须在运行时,指明动态库的路径
PS:我们可以通过ldd 可执行程序
查看该可执行文件依赖的动态库是有链接成功
可以看到,此时libmymath.so 并没有链接成功
接下来介绍三种方法:
1. 环境变量
LD_LIBRARY_PATH
该环境变量存储的路径就是操作系统运行时默认搜索库的路径
所以我们可以将要使用的动态库的路径添加到这个环境变量中
这样就可以链接成功了
但是这一方法是临时方法
,因为重新登录Linux后,环境变量会重置
,因为环境变量其实是配置文件
,每次打开Linux,环境变量会跟着加载进来,我们这里的添加环境变量并不会写进配置文件
,所以每次加载,我们新添加的并不会存在
2. 软链接
我们可以在默认搜索路径下添加软链接,如在 lib64下建立
3. 配置文件
gcc/g++还会在配置文件中搜索第三方库的链接
Linux默认的配置文件的路径是 /etc/ld.so.conf.d
所以我们可以在这个配置文件中添加一个新文件,并在该文件中添加动态库的路径,这样编译器就可以找到了。
添加完配置文件,我们还需要让配置文件生效 ldconfig
命令
这样就同样可以在运行时找到动态库了。
四. 动静态库的加载
1. 静态库的加载
静态库的加载
因为静态库内部直接存储二进制文件,程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
静态库形成的可执行程序,本身就有静态库的对应方法的实现。
这样的好处是不依赖库,即使静态库被删除,形成的可执行程序依然有对应方法。
缺点是,不同可执行调用同一静态库,内存中就会很多重复的部分,非常的占用资源。磁盘,可执行程序体积变大加载占用内存,下载周期变长,占用网络资源。
2. 动态库的加载
而动态库的加载和静态区就有所不同,动态库需要加载到内存,但
只加载一份
。程序在运行
的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表
,而不是外部函数所在目标文件的整个机器码。
在可执行文件运行以前
,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接
。
动态库可以在多个程序间共享
,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
且给予可执行程序的进程的只是动态库函数在库中的偏移量,也就是相对位置。
==================================================================================
地址
地址在存储时有两种形式,一种是绝对编址,还有一种是相对编址。
就比如下面的这张图
假设这是一个跑道,跑道的起始位置设为0,而人所在的位置是50,树所在的位置是70,这些都是绝对编址。
而人在树的左边20米,这就是相对编址
为什么要讲这个呢?
首先我们要问可执行程序形成时,内部是否有函数的地址?
答案是,肯定的
静态库的时候,是直接将函数的内容拷贝
到可执行程序,当然包含函数的地址
用动态库形成的可执行程序当然也有函数的地址,但是这个地址绝对不可以是绝对编址
。其实都是相对编址
,默认起始位置是0
我们先看下面这个图
在可执行文件运行前,所使用的动态库就被加载到内存中了,这个过程叫做动态链接。
然后可执行程序被加载到内存中,建立pcb,进程,页表。并且动态库需要在页表,进程虚拟地址空间完成映射。动态库会被映射到共享区。
这里就有一个问题:那就是,
函数需要地址
,但是每个进程空闲的地址并不一样
,一个进程调用的动态库数量也不一样,如果函数使用绝对编址,加载到相对于共享区起始地的哪个位置,无法保证
该位置是否有被别的函数占用。所以动态库的函数绝对不能使用绝对编址,应该使用相对编址
而相对编址
是这样解决问题的:
一个进程包含一个动态库,那么需要将该动态库都映射到共享区
,但是内部的函数的地址是相对于该动态区起始位置的偏移量
,如上图的printf的123,就是相对于共享区起始地址,偏移123
。这样就可以保证该位置绝对是空闲的。栈区看到调用动态库函数,去代码段查找到偏移量,然后到共享区找到函数的地址,再通过页表映射,找到内存中的动态库,最后找到函数的实现。
动态库,在进程的地址空间中,随便加载,与我们加载到地址空间的什么位置,毫无关系了。
动态库地址,都是偏移量,这就是与位置无关码
五. 琐碎小知识
- 既有动态库,又有静态库,
编译默认采用动态库
。
要想强制使用静态链接
,需要在指令最后加上-static
。
此时file 静态链接形成的可执行,会看到statically linked
,用ldd 查看链接情况,会显示not a dynamic executable
- 当没有动态库,只有静态库,但是编译时不加
-static
,最后对于该静态库,还是静态链接
,但是还有链接C/C++的动态库。C语言的静态库安装:yum install -y glibc-static
C++的静态库安装:yum install -y libstdc++-static
结束语
结束语
本篇知识记录较杂,请多谅解。本着记笔记分享的目的,望佬指点。
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。