目录
一、可执行文件的形成
二、静态库
2.1 制作静态库
2.2 使用静态库
三、动态库
3.1 动态库的制作
3.2 动态库链接
2.3 动态库的加载
一、可执行文件的形成
在C语言中,函数库文件分为两种类型:
- 静态库(库程序是直接注入目标程序的,不分彼此,库文件通常以.a结尾)
- 动态库(库程序是在运行目标程序时(中)加载的,库文件通常以.so结尾)
静态库链接源文件生成可执行文件的过程如下:
接下来我们回顾一下源文件到可执行文件的编译过程。【Linux】基础开发工具——gcc/g++使用
预编译/处理
预编译是使用预编译器进行处理.c源文件和.h头文件,最终生成一个.i的文件。预编译过程就是处理源代码中以#开头的预编译指令,如#include #define 等。预编译过程等价于如下命令:
gcc -E hello.c -o hello.i
#include 就是将包含的头文件全部展开到#include的位置,所以一个.c源文件如果包含多个头文件,头文件的顺序是需要注意的地方。
编译
编译的过程就是将 预处理 完的文件进行一系列的词法分析、语法分析、语义分析及优化,最后生成 .s 汇编代码文件。编译过程等价如下命令:
gcc -S hello.i -o hello.s
编译过程是整个程序构建的核心部分,也是最复杂的部分之一。
汇编
汇编器是将汇编代码转变成机器可以执行的指令, 每一条汇编代码几乎都对应着一条机器指令。最后生成一个 .o 目标文件。汇编过程等价如下命令:
gcc -c hello.s -o hello.o
汇编器的汇编过程相对简单一些,只需要根据汇编指令和机器指令对照表一一翻译就可以了。
链接
将我们编译出来的目标文件和我们代码所用到的库文件一起打包成一个可执行文件的过程。例如myprint.c中的打印函数Print,这个函数不是凭空出现的,在链接的过程中就要连同对应库文件一起打包,最终可执行文件才能正常运行。
二、静态库
首先明确一点:无论动态库还是静态库,库中都不能有main函数
静态库,其中的代码会被拷贝进目标程序的。编译的过程中已经载入到可执行文件中,所以最后生成的可执行文件相对较大。
而动态库在实际没有被链接之前就会编译好,让可执行程序和动态库产关联。
静态库的名字一般是libxxx.a 在编译的时候直接编译进可执行文件中,运行环境中可以不用存在库文件,但是如果库文件更新了,可执行文件需要重新编译。
接下来我们制作静态库。
2.1 制作静态库
首先我们创建两个文件一个头文件和函数实现的文件,.h文件中写入声明,.c文件中是实现
然后编写源文件main.c和mymath.c、myprint.c一起进行编译并运行一下:
程序暂时跑起来没有问题,因为我们要制作库,所以将库中的文件都放到 mklib 文件夹中。
我们只需要将源文件编译为.o,然后将所有的.o文件与 main.o 链接起来即可形成一个可执行。
此时我们再将.o、.h文件放到main.c的目录下
然后我们再把main.c文件汇编为.o文件,再将所有的 .o 文件链接起来,就形成可执行程序了。
但是这个.o 文件太多了,我们想让所有的.o文件打包方便使用,那这个包就是静态库。
Linux下使用ar命令进行操作静态库:
-- ar archivefile objfile --
参数:
- archivefile:archivefile是静态库的名称
- objfile: objfile是已.o为扩展名的中间目标文件名,可以多个并列
选项:
- -r 将objfile文件插入静态库尾或者替换静态库中同名文件
- -c 创建静态库文件
- -r 将objfile文件插入静态库尾或者替换静态库中同名文件
- -d 从静态库中删除文件objfile
-s 重置静态库文件索引
-v 创建文件冗余信息
注意!!!
- 库的前缀必须是 lib(规定),静态库库的后缀必须是 .a(规定)
这样的操作同样麻烦,我们还可以使用Makefile的方式来打包文件。
那接下来我们要将我们打包的这个库进行发布
库中有两个文件夹
- include(库中的所有头文件)
- lib(对应的库文件)
此时makefile中添加一组伪方法,将头文件放在include文件夹中,库文件放到lib文件夹中:
接下来我们 make 生成一下:
2.2 使用静态库
接下来就是使用我们打包好的库了。
有两种使用方法:
- 将库拷贝到系统的路径下(gcc的默认搜索路径)
- 直接指定库的位置
第一种方法:
头文件gcc的默认搜索路径是:/usr/include
库文件默认搜索路径:/lib64 或 /usr/lib64
所以我们将hello/include中的文件全部拷贝到 /usr/include下即可使用hello中的头文件;将hello/lib中的文件拷贝到/lib64目录下即可。
接下来我们编译main.c文件
(报错表示函数未定义,即表示gcc找到了头文件,没找到库文件)
为什么没找到库文件,不是已经放在了默认的搜索路径下了吗?
因为我们编写的库属于第三方库,不是系统内部或gcc/g++的库,gcc不能直接查找到,所以我们在编译时要加上-l选项告诉gcc/g++具体链接哪个库。(注意,需要去掉前缀和后缀,前缀为lib,后缀为.a)
而刚刚将拷贝库到系统默认路径下,就叫做库的安装。
但是不推荐将第三方库添加到系统的默认路径下,因为会污染系统默认搜索路径,接下来我们赶快把文件删掉
第二种方法:
是直接在当前路径下找头文件,gcc中-I选项可以在指定路径下寻找头文件,-L指定库文件所在目录。
可是,gcc还是无法找到库中的函数,因为我们无法保证该目录下只有一个库,所以我们还是要指定-l选项告诉gcc/g++具体链接哪个库。
三个选项的具体介绍:
三、动态库
3.1 动态库的制作
还是以上面的mymath.c 和myprint.c举例。我们先将mymath.c 和myprint.c 汇编为.o文件。但是要添加一个特殊选项:
-fPIC 是创建与地址无关的二进制目标文件(pic,position independent code),这样动态库就形成了可以在系统中的任意位置进行加载,是为了能够在多个应用程序间共享。
然后我们将.o文件进行归档打包,使用 -shared 选项指定生成动态链接库。
使用 -shard 选项的作用是告诉 gcc 接下来链接的是动态库,而不是可执行文件(因为可执行文件要有main函数)
在Makefile中生成动态库的编写:
接下来让我们使用Makefile生成动静态库吧~
3.2 动态库链接
接下来我们使用一下动态库,一些选项的意义我们重温一下:
接下来我们运行可执行程序:
文件是生成了,可以无法执行。
因为在我们指定库的时候,只指定了 hello (去掉了前缀和后缀),而静态库和动态库都叫hello,那gcc 默认使用的是静态库还是动态库呢?
如果我们将动态库移出,gcc使用静态库,a.out文件可以正常运行。
所以,如果只有静态库,那只能将静态库以静态链接的方式拷贝到可执行文件中。其他的动态库再使用动态链接。如果动静态库同时存在,默认使用动态库。
如果动静态库同时存在,想指定使用静态库,要添加-static选项。
好的,现在来讨论为什么这个动态库链接失败了。
2.3 动态库的加载
首先我们来了解以下动态库是如何被我们C语言程序所调用的。
- 动态库是一个独立的库文件
- 动态库和可执行文件,分批加载。
可执行文件和动态库都存放在磁盘中,当我们执行可执行程序时,a.out被加载到内存。
当我们要调用动态库时,内存就会加载动态库,然后通过页表将动态库的数据加载到地址空间的共享区供代码区的调用。
那为什么 a.out 显示找不到libhello.so库呢?
因为我们形成可执行程序时,添加那么多选项是给 gcc 设置的,可是当gcc执行结束,gcc退出之后,a.out 运行起来后就找不到 libhello.so文件了。
所以可以理解为,我们还需要提前在系统中配置,当我们使用到 libhello.so 时,让操作系统把libhello.so 加载到内存中。
方法一:
添加到系统的环境变量中LD_LIBRARY_PATH(LD表示加载,LIBRARY表示库,PATH表示路径)表示库加载时的搜索路径。
但是LD_LIBRARY_PATH这种方法有一个缺点,如果你退出终端的话,设置的LD_LIBRARY_PATH环境变量就被重置了
因为这个环境变量是内存级的环境变量。所以这个方法不是很推荐。
方法二:
修改系统的配置文件,即修改动态库默认搜索的库文件。
第一步在/etc/ld.so.conf.d中自定义添加一个 conf 文件。
然后将动态库所在路径放在 .conf 文件中。、
最后使用 ldconfig 加载conf文件的文本信息即可。
sudo ln -s /home/wzh/23.1.6/uselib/output/lib/libhello.so /lib64/libhello.so
然后我们就可以正常运行了 ,其中动态库的链接地址就是/lib64/libhello.so