动静态库的相关概念
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
我们以如下的几个头文件和源文件作为测试用例
//add.h
#pragma once
#include<stdio.h>
int add(int,int);
//add.c
#include "add.h"
int add(int x,int y)
{
return x+y;
}
//sub.h
#pragma once
#include<stdio.h>
int sub(int,int);
//sub.c
#include"sub.h"
int sub(int x,int y)
{
return x-y;
}
//mul.h
#pragma once
#include<stdio.h>
int mul(int,int);
//mul.c
#include"mul.h"
int mul(int x,int y)
{
return x*y;
}
一、为什么要有库?
如果我们要编译TestMain.c这个文件,我们一般的做法是将TestMain.c和其他源文件一起编译链接成一个可执行文件,具体操作如下
(之所以没有在gcc编译选项里带头文件,是因为编译器会在当前目录和指定目录下查找头文件)
但是如果有其他的文件也需要生成可执行文件,并且也都包含了上面的几个头文件,那么我们就又需要将add.c sub.c mul.c 这几个源文件重新进行编译,这样太浪费时间了,所以我们要先将头文件对应的源文件分别编译成为.o文件,然后我们只需要将我们需要的.o文件和我们需要编译的文件进行链接生成可执行文件即可,省去了源文件重复预处理、编译、汇编的过程
具体操作如下
(Makefile的语法不了解的,可以去了解一下,这里简单说一下这个文件的内容:用 .o文件生成All这个目标文件,但是当前目录没有.o文件,所以它会在文件中能不能找到规则推导出需要的.o文件,也就是第四、五行的作用,最后三行是用来删除生成文件的.o文件)
执行的效果如下
然后我们只要将TestMain.c文件编译成.o文件在和其他的.o文件链接即可生成可执行文件
具体操作如下
上面的操作虽然省略了源文件的重复预处理、编译、汇编的过程,但是这样写还是太费劲了,如果有很多的.o文件,我们手敲也很容易出错,所以我们可以将头文件和它们对应的.o文件打包起来,方便我们操作。也就是形成库。
二、如何生成静态库?
生成静态库
[root@localhost linux]# ar -rc libXXX.a xxx.o xxx.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libXXX.a
t:列出静态库中的文件
v:verbose 详细信息
所以静态库本质就是将库中的源代码直接翻译成为.o目标二进制文件,然后打包
注意:静态库文件有前缀lib和后缀.a,静态库的文件名是XXX的部分。
演示如下
现在我们只要有头文件,静态库和.c文件就能生成可执行文件,具体操作如下
有人可能对我们用的C语言的库不需要指明路径和名字感到奇怪,具体原因是因为我们写的叫第三方库,gcc不认识,本质是gcc的默认搜索路径中没有我们的库
我们用ldd命令去查看a.out这个文件依赖的动态库时,会发现没有libmymath.a这个库,为什么?因为libmymath.a是静态库,静态库中的内容会被拷贝到可执行文件中,所以可执行文件不需要找到静态库
这里讲一下gcc编译加不加-static选项的区别,加-static表示依赖的库都需要是静态库,否则报错,所以我们这里不加static选项,gcc默认用动态库,如果没有动态库就会选择用静态库,即遵循动态库优先的原则
当然,我们一般还会将头文件和库文件放到目录中,在需要用的时候就直接找这个目录即可
而我们正常所说的配置环境等,就是将该文件的压缩包下载,然后解压,将文件放到相应的系统目录下(比如头文件放到/usr/include,库文件放到/lib64等)然后我们就能正常使用了
当然我们这个写的是测试样例就不将它放到系统路径下了。那么如果不放到系统中,我们该怎么编译生成可执行文件呢?(我们现在要处理的文件如下)
如果我们直接编译就会报错,gcc找不到我们包含的头文件,因为头文件不在当前目录下(注意当前目录仅仅只有我们看到的文件,头文件在mymath_lib目录中,不属于当前目录!!!)
方法一:可以在包含头文件时,可以直接加上头文件文件所在的目录
方法二:把头文件所在路径告诉给编译器,让gcc也去我们给的路径下去找头文件
但是还是编译不通过,因为gcc找不到库文件,我们也需要把链接的库文件也告诉gcc
(如果我们将头文件和静态库安装到系统中,我们就不需要新增头文件和库的搜索路径了)
三、如何生成动态库?
生成动态库
shared:表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
示例:[root@localhost linux]# gcc -fPIC -c sub.c add.c
[root@localhost linux]# gcc -shared -o libmymath.so *.o
通过指令,我们就能将头文件和动态库放到一个目录下
现在我们要处理的文件如下
看着和用静态库进行链接时一样,但是操作上会有所差异
生成可执行文件的语句和静态库一样,但是当我们运行a.out时,程序报错说找不到库,这就很奇怪了,明明在生成可执行文件的时候已经告诉gcc库文件在哪里了,为什么这里说找不到呢?
在回答这个问题之前,我们来回忆一下,动态库和静态库的区别:
- 用静态库链接生成可执行文件,本质是将静态库拷贝到可执行文件中
- 用动态库链接生成可执行文件,本质是让可执行文件在执行时去找动态库,从而调用库函数,也就是说动态库和可执行文件要同时被加载到内存
- 总的还说,无论是动态库,还是静态库,都需要被加载到内存,程序才能执行,只不过动态库需要单独加载,并且当有多个程序运行时,动态库只需要加载一份,而静态库则是被包含在程序中一起被加载了多份
现在我们再回过头去回答一下上面的问题:我们在运行程序时,需要将链接的动态库也加载到内存,而我们刚刚只是告诉 gcc 动态库的路径,但是现在是shell命令行在运行程序,这两个是独立的进程,也就是说 gcc知道路径 != 命令行知道路径 ,所以我们还需要将动态库的路径告诉命令行。下面的结果也能说明这一点
这里提供四种处理方法:
方法一:将头文件和库文件直接拷贝到系统(推荐使用,但不推荐你自己写的库这么用)
(如果要自己测试,记得把头文件和库从系统中删除)
方法二:通过使用软连接,查找动态库
(在运行程序时,系统会在当前目录下找库,所以直接把动态库/软连接放在当前目录也可以)
方法三:使用往环境变量 LD_LIBRARY_PATH 添加路径的方式,让系统找到动态库,(系统除了会在默认路径找库,还会去LD_LIBRARY_PATH这个环境变量包含的路径中去查找)
(当然这只是内存级的修改,当我们重启Linux时,该环境变量就会恢复原样)
方法四:在/etc/ld.so.conf.d/目录下添加配置文件,向配置文件中写入动态库的路径即可
四、动态库加载原理
1、动态链接的程序在运行时,可执行程序和动态库都要被加载到内存
2、程序没有被加载之前,程序内部有地址吗?有的。当程序被编译成二进制目标文件时,程序中的函数名,变量名就会被换成二进制的地址,因为计算机只认识二进制,程序中的变量名,函数名是方便给人看的,一旦要交给机器也就没必要存在了。
=> 编译时,需要对代码进行编址,如何编址?基本遵循虚拟地址空间的规则,注意:虚拟地址空间,不仅仅是OS中的概念,编译器编译的时候,也要按照这样的规则编译程序,这样才能在加载时,形成从磁盘到内存的对应关系,即编址方式和内存管理进程地址的方式相同,方便映射。
编译时的虚拟地址,又称为逻辑地址(采用基地址+偏移量的方式) 这里基地址为0
3、编址有两种:绝对编址,相对编址,动态库采用相对编址。(我们写的可执行程序是绝对编址,因为在虚拟地址空间中代码区的位置是固定的)