Linux - 动静态库(下篇)
前言
在上篇博客当中,对静态库是什么,怎么使用,简单实现自己的静态库,这些做了描述,具体请看上篇博客:
本篇博客将会对 动态库是什么,怎么使用,简单实现自己的动态库,这些做描述。
动态库
动态库的创建
动态库的创建其实说和 静态库是非常类似的,都是先想要把 源程序文件生成 ".o" 为后缀的文件,也就是编译成功过的文件,然后再 进行链接。
只是在链接过程当中链接方式不同。
在静态库当中,我们使用的编译源文件的方式是直接使用 "-c" 选项的方式生产源程序的 ".o" 编译文件。
gcc -c $^ // -c 选项不在后续 $@ 确认生成的目标文件名,默认生成的 与源程序名相同的 .o 文件
但是在动态库当中,我们在上述生成 ".o" 文件使用 "-c" 选项 之外,还需要带上 "-fPIC" (其中 PIC 要大写)选项,这个 fPIC 代表的意思是 产生位置无关码。
gcc -fPIC -c xxx.c
而gcc 是默认 优先链接 动态库的,也就是,当 同一个动态库和静态库同时存在的时候,gcc 会优先链接动态库吗,除非是 当前只有 静态库,没有动态库,那么 gcc 没有办法只能使用静态库;又或者是 用户强制要求要用某一个静态库。
所以,静态库在安装的过程当中,需要用户手动去安装,但是对于 动态库,gcc 就可以自己帮助我们实现一个 ".so" 文件。
利用gcc命令,使用 "-shared" 选项,生成一个动态库:
gcc -shared -o libxxx.so *.o
上述就是利用在当前目录当中的所有后缀为 ".o" 的文件,打包为一个 xxx 动态库(动态库必须是 "lib" 开头 ,".o" 结尾的文件名)
上述的 "-shared" 选项就是生成一个 分享动态库。
静态库只是该源程序提供代码,本质上也就是提供一些二进制数据,所以,静态库的作用其实就是把源程序需要的代码拷贝到源程序当中,拷贝完之后,源程序当中是如何进行执行的,就和 这个静态库没关系了;静态库是不用被加载到内存当中执行的。
而动态库不一样,当源程序当中执行到 动态库当中的代码的时候,是直接跳转到 动态库当中去执行,所以,注定了 动态库当中的代码是需要被加载到内存当中供 cpu 执行的。
从 两 ".a" 和 ".so" 文件的 默认权限你也可以看出,动态库的文件权限当中是带 "x" 可执行的,而 静态库不是。
动态库:
静态库:所以,上述动态库我们直接执行就会报错:
并不是 这个程序不能执行,而是这个动态库不能单独执行,他需要源程序来调用其中的代码来执行。
而,动态库和静态库不是不能执行,只是不能单独执行,他们都是未来 链接出的 可执行程序当中代码的一部分。
现在我们给出几个源程序文件:
// mylog.c
#include "mylog.h"
void Log(const char*info)
{
printf("Warning: %s\n", info);
}
//mylog.h
#pragma once
#include <stdio.h>
void Log(const char*);
//mymath.c
#include "mymath.h"
int myerrno = 0;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
if(y == 0){
myerrno = 1;
return -1;
}
return x / y;
}
//mymath.h
#pragma once
#include <stdio.h>
extern int myerrno;
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
//myprint.c
#include "myprint.h"
void Print()
{
printf("hello new world!\n");
printf("hello new world!\n");
printf("hello new world!\n");
printf("hello new world!\n");
}
//myprint.h
#pragma once
#include <stdio.h>
void Print();
所以,有了上述动态库创建,和上篇博客当中 对于 静态库的 创建,现在我们在makefile 当中写上对应 生成的目标文件,就可以一键生成的 对应的 动态库 和 静态库了:
dy-lib=libmymethod.so #目标动态库文件的生成
static-lib=libmymath.a # 目标静态库文件的生成
.PHONY:all #同时生成多个目标文件
all: $(dy-lib) $(static-lib) all 这个文件依赖于 dy-lib 和 static-lib
$(static-lib):mymath.o # 生成静态库(把 .o 文件打包生成 libmymath.a文件)
ar -rc $@ $^
mymath.o:mymath.c # 由源程序 生成 对应 .o 文件
gcc -c $^
$(dy-lib):mylog.o myprint.o # 生成动态库(把 .o 文件打包生成 libmymethod.so文件)
gcc -shared -o $@ $^
mylog.o:mylog.c # 由源程序 生成 对应 .o 文件
gcc -fPIC -c $^
myprint.o:myprint.c # 由源程序 生成 对应 .o 文件
gcc -fPIC -c $^
.PHONY:clean
clean:
rm -rf *.o *.a *.so mylib
.PHONY:output # 用于发布的出来 的 生成的目标文件
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp *.h mylib/include
cp *.a mylib/lib
cp *.so mylib/lib
先 make 把对应的 源程序文件 编译生成 ".o" 文件:
下述就是上述生成之后的结果:
把上述这些 ".o" 文件 打包到 各自的 动静态库 当中:
此时就发布完成了:
上述的这个 lib 文件夹当中的内容,就是我们上述由 各个源程序文件生成的 各自的 .o 文件的动态库,和 静态库的 。
此时,直接使用这种 "#include<mylog.h>" ,然后直接编译的话,他就会提示找不到这个头文件,所以,我们还需要把 这个存储 动静态库 和 各自头文件的 mylib 文件夹,拷贝到 系统默认路径 当中,这种才能使用 这种 "#include<mylog.h>" 方式来引用头文件,和使用其中的代码。
上述拷贝到 默认 路径当中,其实就是库的安装,这种方式使用最多的,当然,你也可以使用 gcc 命令当中的 "-i" "-L" "-l" 三个选项来实现。
上述我们就创建了一个 动态库 和 一个静态库。
如何让可执行程序找到动态库(LD_LIBRARY_PATH 环境变量)
现在我们来用上述的 动态库,来实现一个 可执行程序,然后编译运行:
在上述,生成的 动态库 和 静态库的基础之上,我们来使用上述的库,来写一个程序,看看能不能编译成功:
//main.c
#include "mylog.h"
#include "myprint.h"
int main()
{
Print();
Log("hello log function");
return 0;
}
上述引用的两个头文件都是动态库当中头文件,也就是说上述源程序当中只引入了 我们上述实现的 动态库。
但是我们发现,虽然我们上述的 动静态库,没有在 系统默认路径下安装,但是我们还用了gcc 当中的三个指令来引入 头文件 和 库;虽然成功生成了 可执行文件,但是,当我们运行这个可执行程序的时候,却报错:找不到我们上述实现的 动态库:
但是,当我们 file 命令 这个 a.out 可执行文件之时,却告知我们 这个可执行文件是有 动态库链接的:
但是在 ldd 命令当中,显示这个 动态链接是 not found 的 :
但是我们上述在 gcc 命令当中不是已经对 对应的 头文件 和 库名称,和库所在路径做了声明吗?已经告诉编译器了,头文件是什么,什么库,在哪里了。
是的,你已经告诉了编译器了,编译器已经知道了,但是别忘了,动态库是在加载到 内存当中,进行调度执行的,他不像静态库一样单独拷贝一份给对应的源程序当中,拷贝完就不管了;
动态库要加载到内存当中进行调度执行。
所以,上述只是 编译器知道了这个动态库要被执行,也只是编译器知道这个 库是什么,在哪里;但是,系统并不知道。
所以,系统要知道,也就是 加载器 要知道。
所以,首先你要明白的是,动态库是一个单独的文件,他和 可执行文件是两个文件,我没在执行可执行文件之时,是带上了 路径的,就算没有带上路径,要想成功运行,必须在 PATH 环境变量当中存储的路径下,有这个可执行文件,才能够不带上路径执行。
所以,我们要想执行一个 动态库,就要让 系统找到这个动态库。
第一个方法就是把 这个 动态库拷贝到 C程序默认的系统路径当中,这样,系统就可以在 这个默认路径当中找到这个 动态库,从而执行。
如上所示,拷贝到 lib64 这个目录当中,系统 就可以找到这个 动态库,从而执行。
除了上述 拷贝 的方式,还可以 ln 命令 在上述 lib64 当中创建软链接,链接到 libmymethod.so 这个 动态库 所在的存储路径当中。
软链接建立结果:
此时他直接就指向了 动态库的存储位置。
如上所示,我们都没有重新对 a.out 重新编译,但是上述使用 ldd 命令却已经可以看到链接 到 libmymethod.so 动态库的链接的结果了。 也可以运行了。
除了上述的 在 默认目录当中进行 拷贝,软链接 的操作,和 系统找命令的可执行程序的 PATH 环境变量一样,上述的 在C当中和 默认 查找 库的也有一个环境变量 --- LD_LIBRARY_PATH
说得更详细一点,LD_LIBRARY_PATH 环境变量当中存储的就是 系统用来搜索动态库的路径。
这个环境变量,可能你的系统当中会没有,但是如果你配置过 VIM 的话,可能会有。
如果没有也没关系,可以自己配置一下:
在冒号后面跟上 动态库所在的路径就可以了 ,不需要加上 动态库文件名。
因为,需要链接哪一个库,在 gcc 当中 选项当中已经给出了。
此时,我们再 ldd 命令查看 链接情况,发现也是链接成功了的。
但是,上述我们是修改了 环境变量,但是我们修改的是 bash 的环境变量,因为 当前我们所有的运行的命令,都是bash 的为我们生成的子命令,export 命令是一个内建命令,因为它要修改 bash 的环境变量,所以,export 是不需要创建子进程,而是由 bash 自己的来执行。
但是,修改的终究是 bash 的环境变量,bash 当中的环境变量是在 程序启动之时,由对应的 环境变量生成脚本,一层一层 加载(添加)出来的。
所以,也就是说,如果我们不修改 每一次开机,系统脚本对 bahs 当中的环境变量的添加进行修改的话,那么,当我们关机之后,再次启动,脚本生成的还是之前的 那些环境变量。
所以,要想根本解决上述 由 LD_LIBRARY_PATH 环境变量当中存储的 路径的话,需要修改 生成的 bash 环境变量的脚本。
除了上述方式,还有一个访问可以实现:
在系统当中,有一个目录当中存储的是 后缀为 ".conf" 的文件,这些文件我们称之为 配置文件,在这些文件当中存储的都是路径信息。
这个目录是 : /etc/ld.so.conf.d
我们可以随便打开一个看看:
所以,如果像系统找到 对应 动态库的话,就可以在 /etc/ld.so.conf.d 这个 路径下,创建一个 后缀为 ".conf" 的文件(文件名随便取),在这个文件当中,直接像上述一样,存放 对应 动态库的存储位置(注意此时 不需要带上 库名称)
然后,很重要的一步,需要使用 ldconfig 命令 ,重新加载一下 上述路径下的配置文件。
上述这种 在 /etc/ld.so.conf.d 路径当中进行修改的操作,只要我们不修改我们对于其中的配置文件,那么这个修改就是永久有效的;
其实只有 上述的第三种使用 LD_LIBPARY_PATH 环境变量这个操作,如果不修改 bash 启动的脚本的话,关机之后就无效了。
同样的,如果我们在上述程序当中加上 我们在上篇博客当中实现的静态库的话,只要我们gcc 编译链接成功了,把静态库当中的代码已经拷贝到 可执行程序当中用于链接了,而且此时我们运行程序是可以正常运行的,结果也是正确的。
那么,在之后,不管这个静态库被移动到什么其他位置了,不在之前链接的位置了,或者更加夸张一点,直接把静态库删除;
我们上述生成的可制成程序一样说可以跑的,因为,静态库链接,就是把静态库当中的代码直接拷贝到 对应源程序的当中,成为 源程序的 一部分。
而不像 动态库链接,链接成功之后,动态库的位置是不能移动的,因为编译链接成功之后,不仅仅是编译器在编译之时需要找到这个动态库位置,因为 动态库是要被加载到 内存当中进行执行的。
连接成功后,执行可执行程序,当 可执行程序执行到 需要动态库当中的代码之时, 就需要直接跳转到 动态库 当中执行,此时动态库需要被加载到内存当中,配合 原可执行程序 进行 执行。
所以,动态库在被加载之前,还需要被 系统知道在什么位置存储的,也就是 加载器需要知道 此动态库在什么位置存储的。
至此才能保证 动态库可以被加载到 内存当中进行运行。
如下所示:
在上述代码当中:"mymath.h" 就是上一篇博客当中实现的 静态库。
上图就在之前 gcc 链接动态库的基础之上,加上 "-lmymath.h" 链接上静态库。
删除静态库,并且在此执行程序:
由上图你会发现,当我们删除掉静态库,没有重新编译链接 形成可执行文件,但是,在之前形成的 可执行文件 还是可以运行。没有出错。
当然,不管是静态库 还是 动态库,都是可以被多个 源程序所引用的。但是,由静态库 链接生成 的 多个 可执行文件,如果把此静态库删除掉,这些个 可执行程序还是可以跑的;
但是如果是 由 动态库 链接形成的多个可执行文件,如果是把此动态库删除掉了,那个由此动态库链接的所有 可执行程序就都不能跑了。
如上图所示:我们把上述 main 和 mytest 两个可执行文件 需要的 libmymethod.so 动态库删除了,发现就不能运行了。
所以,动态库再被系统加载之后,会被所有的 进程所共享!!!
使用了动态链接的方式 形成 的 可执行文件,都是需要系统加载到内存,运行这个 动态库的。
而且,这个动态库被加载到内存,只需要加载一次,因为一次也已经够所有 进程所使用了。这样的话,在系统当中, 就减少了重复的代码。
所以,一个进程,在磁盘(外存)上有自己独立的一份代码,而在 内存当中又有 和其他 进程 有相同的 代码,共享这些相同的代码。
所以,我们把 动态库 又叫做 共享库。
那么,一个系统当中肯定会加载很多个 动态库到内存当中,操作系统如何管理这些海量的 库呢?
我们说,只要是加载到内存当中,被操作系统所管理的,都离不开 这 六字真言:先描述,再组织
先把 管理一个库,所需要的所有属性,都用一个 struct 结构体描述起来(这叫做 先描述对象),然后用某种数据结构,把上述生成的 很多个 struct 结构体都链接起来(这叫做 再组织)。
动态库是如何做到被所有进程共享的?(重谈进程地址空间)
如上所示,在每一个进程的 地址空间当中,每一个地址都是一个 虚拟地址,通过 页表映射到 物理内存地址。
在磁盘当中,每一个进程 独立的 可执行程序,从磁盘当中被加载到 内存当中,通过 页表来映射到 进程地址空间的 代码区当中。
而,如果在 进程 独立的 可执行程序 当中发现,比如是 printf()函数并没有在 可执行程序当中实现,但是,在编译链接之时,又有一个动态库当中有 printf()函数的实现,那么在运行到 printf()函数之时,就会 跳转到 共享区 当中去寻找 printf()实现的虚拟地址,通过页表映射到物理内存当中。
在执行完之后,就会跳回 代码区当中继续执行 这个进程的 代码。
至此,动态库当中的代码就被进程所使用,同样的,上述只是一个进程,同样可以是多个进程。
那么,动态库加载到内存当中的 代码数据,就可以被 多个进程所使用了。
所以,在页表当中建立映射之后,从此往后,进程在执行任何的代码,不管是自己 可执行程序当中的代码,还是 共享区当中的 动态库的共享代码,都是可以在 进程地址空间当中进行 执行的。
此时,对于动态库 和 自己可知程序当中的代码,就都可以看做是一份代码了,只不过在执行到 动态库当中的代码之时,需要跳转到 动态库当中代码进行执行,只是可能两份代码跳转的比较远罢了。
所以,在操作系统是如何 跳转到共享区当中,找到对应的 共享代码区执行的。其实就是上述所说的,先描述在组织。
操作系统只需要管理好 先描述在组织 的数据结构,就可以管理好其中的 对象,而管理好一个一个的 存储 库 属性的 对象,就可以管理好 一个一个库在内存当中的 使用情况:比如 , 当前是否正在被使用?大小占多少?进程要使用的库,有没有被加载到内存当中?起始地址是什么?·········
所以,在上述 操作之后,OS 对于 库的 加载情况就非常清楚了。
共享库当中的 errno 变量存储的问题
共享库当中的代码是共享的,那么其中的数据是不是共享的呢?
如果我们在一个 C 程序当中,对 errno 这个 C库当中的全局变量进行了修改,其他C 程序会不会受到影响?
答案是不会的,因为 共享区是在 栈 和 堆区当中的,这也就意味着,当 某一个进程对 errno 变量做了修改,就会发生写时拷贝,谁写谁就自己拷贝一份 自己存储 自己修改的数据。
这个写时拷贝的 出来的空间 在共享区当中存储,同时,在 C 管理 要输出到文件当中的数据,也有一个 struct_file 结构体来管理,在这个结构体当中就有 C 帮我们提供的 语言级别的缓冲区(用户级缓冲区)。
总结
静态库 只需要在 编译时期 ,可以找到这个 静态库在哪,把这个静态库当中的数据,拷贝到 对应源程序当中。
但是动态链接不一样,如果要是用到 动态库的话,动态库就需要再 内存当中被加载,从而执行。而 静态库是不需要 加载到内存当中执行的,它完成了 拷贝的工作就可以 下岗了,在当前可执行程序的链接就不需要他了。
在上述说明的 四种 可执行程序链接 动态库 的方式的当中,其实用得最多就是 拷贝 这种方式。因为这种方式最直接了当,你会发现,这种方式其实是最方便的。
第三方库链接操作例子:【Linux拓展】ncurses库的安装和使用 {ncurses库的安装方法,ncurses库的使用手册,基于终端的贪吃蛇游戏}_芥末虾的博客-CSDN博客
上述的 ncurses库 是一个 基于 终端的 图形界面的库,有兴趣的可是尝试链接这个第三方库。
上述的 ncurses 库是在Linux 下的,如果是在windows 当中其实还有 easyx 库,在windows 当中这些库会做的更酷炫一些。还有 QT。