目录
一、前言
二、静态库与动态库
三、生成静态库
1、生成原理
2、完整过程
3、总结
四、生成动态库
1、环境变量
2、建立软链接
3、配置文件
五、动态库的加载
1、动态库加载的过程
2、动态库地址的理解
3、补充内容
一、前言
关于动态库与静态库的一小部分前置内容,我曾在文章《编译器 - gcc && 函数库》中有过详细的说明。
实际上,系统已经预装了C/C++的头文件和库文件。头文件提供方法的说明,库函数提供方法的实现,头和库是要组合在一起使用的。头文件在预处理阶段就引入了,链接的本质就是链接库。
所以我们在安装开发环境时,实际上就是在安装对应语言配套的库和头文件。在使用编译器时,都会有语法的自动提醒功能,这需要先包含头文件。语法提醒的本质是,编译器会自动将用户输入的内容,不断地在被包含的头文件中进行搜索,自动提醒功能是依赖头文件实现的。
在写代码时,环境之所以知道代码中哪些地方有语法错误,哪些地方定义变量有问题,是因为编译器工作模式有命令行模式和其他自动化的模式,在用户写代码时,会不断的进行预处理、编译操作,帮助用户不断地进行语法检查。
库存在的目的是为了提高开发效率。
二、静态库与动态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
- 一般云服务器,默认只会存在动态库,不存在静态库,静态库需要独立安装。
三、生成静态库
1、生成原理
这里先实现一个简单的加减法程序:
//myadd.h
#pragma once
int myadd(int d1, int d2);
//myadd.c
#include "myadd.h"
int myadd(int d1, int d2)
{
return d1 + d2;
}
//mysub.h
#pragma once
int mysub(int d1, int d2);
//mysub.c
#include "mysub.h"
int mysub(int d1, int d2)
{
return d1 - d2;
}
以上程序编写完成后,可以直接把源代码打包给其他程序使用:
编译运行:
结果符合预期。
如果我们想让其他人调用自己程序的一些功能,但是不想把源代码交给其他人,则可以把自己的程序经过预处理、编译、汇编,生成 .o 文件,即可重定位目标二进制文件,交给别人使用。
现在先把实现功能的源代码与使用功能的程序分别分离到 mylib 与 otherPerson 目录中:
使用指令 gcc -c [源文件] 生成 .o 文件,并把它放到 otherPerson 目录中:
进入到 otherPerson 目录中后,先把 main.c 文件也使用指令 gcc -c 生成 .o 文件,再与其他 .o 文件进行链接,最后生成可执行文件:
最终结果符合预期。
为了方便传输 .o 文件,我们通常会对这些文件进行打包。打包生成静态库指令:
ar -rc [lib文件名.a] [*.o]
把 *.a 文件和 *.h 文件都复制到 otherPerson 中后,编译 main.c 生成可执行程序:
出现了如上报错,这是因为当有了库之后,要将库引入项目中,必须要让编译器找到头文件与库文件。由于gcc 与 g++ 只认识 C语言 和 C++ 的库,我们自己引入的库属于第三方库,编译器不认识,当然也就找不到。
因此,必须自己说明需要链接哪一个库:
gcc -o [可执行程序] [被编译文件] -L[库所处路径] -l[库名]
结果符合预期。
2、完整过程
有了以上知识,我们已经知道静态库是怎么封装以及交给别人使用的了。但实际上静态库的传输与使用的真正步骤与上面演示的有所不同,具体过程如下:
形成静态库后,创建一个 include 目录和一个 lib 目录,分别放入 .h 文件与 .a 文件:
接下来为了让别人使用自己的静态库,则可以把这两个目录打包并上传,供别人下载使用:
使用静态库指令:
gcc -o [可执行文件] [源代码文件] -I[头文件路径] -L[库文件路径] -l[库文件名称]
结合静态库编译并运行观察结果:
结果符合预期。
如果不想在编译时指明这么一长串的路径,也可以直接把对应的头文件与库文件拷贝到 gcc、g++ 的默认搜索路径下:
之后就无需再指明头文件与库的路径,只需说明使用第三方库的名字就可以正常编译使用了。
3、总结
自己生成使用第三方库时,需要满足两个条件:
- 需要指定的头文件和库文件
- 如果库没有默认安装到系统gcc、g++默认的搜索路径下,用户必须指明对应的选项告知编译器:头文件在哪里、库文件在哪里、库文件是什么名字。
- 将下载下来的库和头文件,拷贝到系统默认路径下,这就叫做操作系统中库的安装。对于任何软件而言,安装和卸载的本质就是拷贝到系统特定的路径下,或者从特定路径下删除。
- 如果我们安装的库是第三方的(第一方是语言,第二方是操作系统调用接口),在使用时,需要用 -l 说明对应库的名字。
- 无论我们是从网络中直接下载的库,还是源代码(编译方法)。都会提供一个 make install 安装的命令,这个命令所做的就是安装到系统中的工作。我们安装大部分指令、库等等都是需要 sudo 提权的。
四、生成动态库
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
在生成动态库 .o 文件时,需要给指令gcc加上 -fPIC 命令选项,产生位置无关码:
接下来与静态库相同,也需要把 .o 文件打包以便上传。不同之处在于动态库是使用 gcc 指令进行打包:
gcc -shared -o [lib库名.so] [*.o]
形成动态库后,创建一个 include 目录和一个 lib 目录,分别放入 .h 文件与 .a 文件:
接下来为了让别人使用自己的动态库,则可以把这两个目录打包并上传,供别人下载使用:
使用动态库编译后运行观察结果:
发现程序运行报错,提示无法打开共享对象文件。
这是因为我们使用 -l 、-L 选项说明路径,是向编译器说明的,而不是向OS说明。在程序运行的时候,因为 .so 没有在系统的默认路径下,所以OS依旧找不到。
注意这里与静态库进行区分,静态库这样可以运行是因为,静态库的链接原则是将用户使用的二进制代码直接拷贝到目标可执行程序中,但是动态库不会。
OS查找动态库有以下几种方法:
- 设置环境变量:LD_LIBRARY_PATH。
- 在系统指定路径下建立软链接,指向对应的库。
- 配置文件。
1、环境变量
使用 echo 指令查看环境变量 LD_LIBRARY_PATH :
直接使用 export 指令将库的路径添加进该环境变量就可以了:
此时程序可以正常执行,且结果符合预期。
因为环境变量在我们退出终端再次登录的时候会被重置,所以我们下一次登陆时就需要重新设置一遍环境变量,否则无法正常运行。
2、建立软链接
因为库的默认搜索路径是 /usr/lib64 和 /lib64 。所以我们可以直接挑选一个路径,在该路径下建立对应库的软链接,这里以 /lib64 为例:
编译运行,结果符合预期。
因为软链接是一个正常的文件,永远保存在磁盘上,所以我们退出后再次登录时,程序依然可以正常运行。
3、配置文件
相关配置文件所处路径: /etc/ld.so.conf.d :
这些配置文件中存放库所在的路径。
现在我们自己在此目录下 touch 一个配置文件,并在该配置文件内输入所需库的路径:
更改完配置文件后,需要让该配置文件生效。采用指令:
ldconfig
运行程序,结果符合预期。
五、动态库的加载
1、动态库加载的过程
当使用动态库编译好了一个可执行文件后,该可执行文件存储在磁盘当中,并在运行时加载到内存里。
我们知道,程序被加载到内存后就变成了进程,OS会在内存中创建对应的 task_struct 、 mm_struct 、 页表 。用户在执行程序中的代码时,正常执行。当需要执行动态库内的代码时,OS会找到动态库,把动态库加载到内存中,并建立页表映射关系,映射到虚拟地址空间的共享区中。这些动作都是由OS自动完成的。
可执行文件在被编译完成时,就已经具备了对应的虚拟地址。以上动作完成后,再执行动态库内的代码,OS会自动识别,并跳转到虚拟地址的共享区部分,通过页表的映射关系,执行内存中对应的动态库代码,动态库代码执行完毕后,再回到虚拟地址的代码区部分,继续执行下面的其他代码。
换句话说,只要把库加载到内存,映射到进程的地址空间后,进程执行库中的方法,就依旧还是在自己的地址空间内进行函数跳转即可。
所以动态库节省资源的原因就在于,动态库里的所有方法在内存里只需要存放一份就可以了,其他所有进程都可以调用。
2、动态库地址的理解
在程序编译链接形成可执行程序的时候,可执行程序内部就已经有地址了,地址一共有两类,分别是绝对编址与相对编址。
动态库必定面临一个问题:不同的进程,运行程度不同,需要使用的第三方库是不同的,这就注定了每一个进程的共享区中的空闲位置是不确定的。因此,动态库中函数的地址,绝对不能使用绝对编址,动态库中的所有地址都是偏移量,默认从 0 开始。简单来说,库中的函数只需要记录自己在该库中的偏移量,即相对地址就可以了。
当一个库真正的被映射到进程地址空间时,他的起始地址才能真正的确定,并且被OS管理起来。OS本身管理库,所以OS知道我们调用库中函数时,使用的是哪一个库,这个库的起始地址是什么。当需要执行库中的函数时,只需要拿到库的起始地址,加上对应函数在该库中的偏移量,就能够调用对应函数了。
借助函数在库中的相对地址,无论库被加载到了共享区的哪一个位置,都不影响我们准确的找到对应函数。所以这种库被称为动态库,动态库中地址被称为与位置无关码。
3、补充内容
- 动态库和静态库同时存在,默认使用动态链接。如果想要使用静态链接,则需要在编译时加上 -static 命令选项。
- 如果不提供动态库,只提供静态库,且在编译时不加 -static 命令选项。那么程序在链接时,对于该库,会默认使用静态链接。而对于其他的,比如C库等等,依然默认使用动态链接。
关于动静态库的相关内容就讲到这里,希望同学们多多支持,如果有不对的地方,欢迎大佬指正,谢谢!