hello,各位读者大大们你们好呀
系列专栏:【Linux初阶】
✒️✒️本篇内容:动静态库初识,库的含义,静态库的生成与链接,gcc/g++默认链接方式,动态库的生成与动态链接,查看动态链接的方法,动静态库的加载原理
作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
静态库(.a)
:程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。动态库(.so)
:程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。动态链接(dynamic linking)
。我们可以创建一个测试程序:
测试程序
/add.h/
#pragma once
#include
extern int Add(int a, int b);
/add.c/
#include "my_add.h"
int Add(int a, int b)
{
printf("enter Add func, %d + %d = ?\n", a, b);
return a + b;
}
/sub.h/
#pragma once
#include
extern int Sub(int a, int b);
/add.c/
#include "add.h"
int sub(int a, int b)
{
return a - b;
}
///main.c
#include "my_add.h"
#include "my_sub.h"
int main()
{
int a = 10;
int b = 20;
int res = Sub(a, b);
printf("result: %d\n", res);
res = Add(a, b);
printf("result: %d\n", res);
return 0;
}
程序的编译分为四个阶段:预处理、编译、汇编、链接
。输入下面三条指令,分别会形成 3个汇编之后的二进制文件,这种二进制文件无法运行,因为它缺了链接的步骤。
gcc -c main.c -> main.o
gcc -c my_add.c -> my_add.o
gcc -c my_sub.c -> my_sub.o
这种 .o
文件,我们称它为:可重定位目标二进制文件
。通过对 .o文件的链接,可以形成一个统一的可执行文件。
我们在上面说过,.o文件是一个二进制文件,无法阅读。如果我们不想给对方我们的源码,可以给对方提供 .o可重定位目标二进制文件让别人用你的代码进行链接即可。
也就是说,如果要在没有源码的情况下,形成可执行文件,我们需要给对方提供 .o(方法的实现)
,.h(都有什么方法)
,还要有自己调用方法的文件(main.o)
。
在上面的基础上,我们可以试着给所有的 .o文件打一个包,最后只给对方提供一个库文件即可。
库文件:.o文件 -> 一个文件 -> 库 -> (根据打包工具和方式不同)静态库和动态库!
总结:库文件就是
.o
文件的集合。
为了方便理解,我们使用 Makefile工具
libmymath.a:my_add.o my_sub.o
ar - rc $@ $^ #ar是gnu归档工具,rc表示(replace and create)
my_add.o:my_add.c
gcc - c my_add.c - o my_add.o #形成.o文件
my_sub.o : my_sub.c
gcc - c my_sub.c - o my_sub.o
.PHONY : output #发布
output :
mkdir - p mylib / include #-p,建立多级目录
mkdir - p mylib / lib
cp - f *.a mylib / lib #将生成的libmymath.a和头文件拷贝到特定的目录中
cp - f *.h mylib / include
.PHONY:clean
clean :
rm - rf *.o libmymath.a mylib #删除所有.o文件和对应的库
至此,我们生成了对应的目录文件 mylib
,而这个mylib就是我们的静态库。
总结:交付库 = 库文件(.a/.so)+ 匹配的头文件。
———— 我是一条知识分割线 ————
方法如下:
查看静态库中的目录列表
[root@localhost linux]# ar - tv libmymath.a
rw - r--r-- 0 / 0 1240 Sep 15 16:53 2017 add.o
rw - r--r-- 0 / 0 1240 Sep 15 16 : 53 2017 sub.o
t : 列出静态库中的文件
v : verbose 详细信息
———— 我是一条知识分割线 ————
tar命令
对静态库进行压缩。总结:安装的本质就是拷贝。库的安装就是将库文件和头文件分别拷贝到对应的默认路径中。库的卸载就是将库文件和头文件从默认路径下删除。
———— 我是一条知识分割线 ————
这里:-I(大写i)后跟头文件路径,-L (大写l)后跟库文件路径,-l(小写l)后跟库文件名
。
补充:上述字母后面空格可带可不带。gcc/g++只能在当前路径下搜索,因此需要具体的头文件、库文件路径、还有库文件名称。
总结:链接第三方库时,必须指明头文件路径、库文件路径、库文件名称。
注意:我们平时编译代码不用提供路径是因为库文件和头文件在系统的默认路径下。gcc/g++默认能识别C/C++的库。
———— 我是一条知识分割线 ————
gcc、g++是默认使用动态链接的
。总结:gcc、g++是默认使用动态链接的
在学习动态库生成之前,我们需要了解几个名词:
示例:
#生成共享库格式的 .o文件
[root@localhost linux]# gcc -fPIC -c sub.c add.c
#生成对应的动态库文件
[root@localhost linux]# gcc -shared -o libmymath.so *.o
接下来,我们要生成一个目录,存放库文件和头文件
至此,我们创建好了我们的动态库。
———— 我是一条知识分割线 ————
创建好目录之后我们尝试链接,发现会存在报错(err:找不到文件或目录),因此动态链接并没有我们想象中那么简单。
我们不是已经告诉 gcc/g++,头文件、库文件路径还有库文件名称了吗?为什么还是不能链接呢?答案是,gcc/g++只完成了编译的步骤,我们在运行的时候还需要操作系统通过动态链接调用我们的代码,也就是说,OS也需要知道我们库的位置,但是库不在系统路径下,无法找到。
下面介绍动态链接的四种方法
:
拷贝.so文件到系统共享库路径下, 一般指/usr/lib。
更改环境变量 LD_LIBRARY_PATH
,系统除了会在默认路径下搜索库,还会在这个变量下搜索。
[root@localhost linux]# export LD_LIBRARY_PATH=. (=后跟库的具体路径)
[root@localhost linux]# gcc main.c -lmymath
[root@localhost linux]# ./a.out
我们通过更改自己的配置文件,也可以让操作系统找到。
ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
注意,配置文件操作大多需要 root权限,普通用户可以通过 sudo提权实现配置文件的创建和更新。
操作系统可以查找当前路径下的库文件,所以我们可以在当前路径下创建一个软连接文件,链接库文件,实现OS的访问。
———— 我是一条知识分割线 ————
ldd 文件名 #指令
静态库不存在加载
,因为程序在编译完成之后,就会将静态库的内容拷贝到我们的可执行程序中。当可程序运行起来之后再整体加载到内存。
注意:程序加载 != 静态库加载,静态库加载不存在。
当其他可执行程序再次调用静态库时,就需要再次将静态库内容拷贝到可执行程序中。静态库的使用可能会导致代码的冗余。
代码在编译的过程中,就已经以虚拟地址空间的方式将我们的代码编译好了,因此,静态库拷贝的本质:将静态库的代码展开,将代码拷贝到可执行程序的代码区中
。
总结:代码在编译好后,会按照虚拟地址空间的排布规则对代码进行排布,静态库在编译过程中会将展开的代码拷贝到代码区中。未来这部分代码,必须通过相对确定的地址位置进行访问。
———— 我是一条知识分割线 ————
动态链接并没有将代码拷贝到我们的可执行程序中,它是将动态库中指定函数的地址,写进了可执行程序中。
举个例子:假设我们有一个 my.exe可执行文件、libc.so动态库,在可执行程序中有一个 printf函数,函数在可执行文件中具有对应的地址,完成动态库加载之后,它会将 printf的地址写进可执行程序中
。当程序需要的时候,可以通过地址找到对应的方法。
在动态库的生成中,我们讲解了一个名词: fPIC
:产生位置无关码(position independent code)。它具体的含义是什么呢?与位置无关:用特定的参照系来定位某个人或物体所处位置的相对定位的方式
。
也就是说,我们是用这个方法实现动态库的连接和加载的。动态库加载会将函数的地址写入到可执行程序中,这个地址是偏移地址(记录了函数在 .so中的偏移量)。
总结:静态库拷贝,依据确定起始点进行拷贝,方法的位置确定,这种编址方式称为绝对编址;动态库加载会将函数的偏移地址写入到可执行程序中,根据不同的起始地址查找方法,这种编址方式为相对编址。
动态库加载和访问的逻辑(详解):
还是以 printf函数为例,当我们的计算机需要调用代码的 printf函数,通过页表读取之后,发现 printf的实现代码在可执行程序中并不存在,这是编译时就标识好的,同时还会发现代码区中的这个地址是一个外部地址。
此时操作系统就知道要访问这个库了,接下来操作系统不会继续执行 printf的代码,它会先将磁盘中的动态库加载到内存
。
再将内存中库的内容通过页表映射到虚拟地址空间的共享区
中,映射完成后库天然就拥有了起始地址
。不同的程序可能会加载不同的动态库,因此在动态库没有完成加载映射之前,它的起始地址是不确定的。
在虚拟地址空间的代码区中,因为我们的代码存有库方法的偏移地址,所以在库完成动态加载映射之后,当我们想调用 printf函数(跳转动态库的执行方法),我们就可以在确定了库的起始地址的前提下,根据代码中保存的偏移地址直接跳转到共享区的库方法之中
。
至此,我们就可以在有需要调用库函数的时候,直接在上下文中跳转。最终实现动态库加载和访问。
总结:调用库方法前需要对动态库进行加载映射,使动态库具有起始地址,然后代码区中的代码就可以通过偏移量在上下文中进行跳转,最终找到共享区的库方法。
补充:动态库加载时,操作系统会根据一定的策略加载库方法,而不会将所有方法一次性加载。
补充:操作系统会在有需要的时候对库进行加载链接,当100个进程都用了同一个库,内存中这个库的代码也就只有一份。因此我们可以通过使用动态链接的方式,实现节省内存的目的。
基础IO - 动静态库 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux 的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!