首先,对于库这个东西,我们是既熟悉又陌生。熟悉是因为我们每天基本都在用,C语言标准库,C++的STL类库等等的库。但是陌生的是,我们仅仅只是懂得使用这些库,而对库的整个原理我们是一无所知的!所以接下来,我们就先来感性认识一下库。
首先,来看一段最基本的C语言代码
#include
int main()
{
printf("hello world\n");
return 0;
}
这个C语言代码我们在熟悉不过了。那么这个代码最终如何变成最终的可执行程序的流程我们也很清楚。
预处理,编译,汇编,链接,而今天我们的研究的重点就是这个链接。
首先,我们要知道,首先我们要包
简单来说:库就是函数实现的二进制代码的集合
感性认识了库以后。我们接下来具体讲一讲库的分类。
库一般有两种:一种叫做静态库,另外一种叫做动态库。 所谓的静态库,就是在链接这一步的时候,把对应库里面的代码拷贝到自己的代码里面去。那么静态库的优点就是在运行的时候不会依赖于对应的库,但是缺点就是会使得代码变得庞大。
而与之相反的则就是动态库,动态库就是在链接的时候去对应的库里面寻找对应的函数的实现代码,这样做的好处是节约空间,能够让多份代码共享同一个库。但不好的地方就是对库的依赖性强!
而在Linux下,动态库是以.so为后缀,静态库则是以.a作为后缀名
在windows下,动态库的后缀是.dll,静态库的后缀是.lib
那么接下来我们就从库的设计者和库的使用者两个角度来看库。首先我们先以库的设计者的角度切入,看看在Linux下如何制作动静态库
我们规范一点,我们按照C语言给我们制作库的方式。制作对应的头文件,还有制作对应的库文件。我们先以制作静态库为例
/*
先提供库函数的实现
*/
#include "getMax.h"
int getMax(int* a,int n)
{
assert(a);
int i=1;
int max=a[0];
for( i=1;i<n;++i)
{
if(max < a[i])
max=a[i];
}
return max;
}
而对应的,我们还需要头文件,这里也简单提供一下头文件
//提供库函数的声明
#pragma once
#include
extern int getMax(int* a,int n);
完成,这一简单的动作以后,接下来我们就要把函数文件进行编译了。使用的是gcc编译器的-c选项生成二进制目标文件
gcc -c getMax.c #默认就是形成同名的二进制目标文件
首先,接下来我们创建一个简单的使用这个函数的文件
#include "getMax.h"
#include
int main()
{
int a[]={11,22,33,44};
int max=getMax(a,4);
printf("max=%d\n",max);
return 0;
}
在正式弄出静态库之前,我们不妨先来做个小实验。假如我就是把编译后的二进制文件直接交给对方进行链接,能否可以运行成功呢?说干就干
gcc -c test.o getMax.o
我们看到,程序不仅编译通过。而且运行的结果是正确的,也就是说直接把二进制文件交付给对方也是可以的! 但是为什么要有库的存在呢?因为未来的开发中,实际有的二进制文件可能有几百个甚至是上千个,而一股脑把这么庞大的二进制文件数量交给对方实在是不妥。所以,在实际的开发中。我们都会把二进制文件打包成库 接下来我们就来看怎么打包库。首先我们先从打包静态库开始。首先一样,我们要先把对应的文件编译成二进制文件。为了能够更好演示,所以我们这里多增加一个getMin函数
#include "getMin.h"
int getMin(int* a,int n)
{
assert(a);
int i=1;
int min=a[0];
for(i=1;i<n;++i)
{
if(min>a[i])
min=a[i];
}
return min;
}
对应的getmin.h的内容如下:
#pragma once
#include
extern int getMin(int* a,int n);
接下来我们就来看一看如何打包成静态库。首先第一步就是要把对应的头文件创建好,然后就是把对应函数的实现编译成二进制文件即可。
接下来使用的就是ar归档命令
#归档命令
ar -rc lib[libname].a objfile1 [...]
而接下来我们使用makefile来完成这些工作:
libgetMax.a:getMax.o getMin.o
ar -rc libgetMax.a getMax.o getMin.o
getMax.o:getMax.c
gcc -c getMax.c
getMin.o:getMin.c
gcc -c getMin.c
.PHONY:static
static:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
.PHONY:clean
clean:
rm -rf *.a *.o lib-static
运行以后,对应的当前目录就会多一个lib-static的目录文件,里面包括了对应的头文件和库文件。
我们查看一下lib-static目录的内容:
这就是生成静态库。接下来我们来看一看如何制作动态库。
动态库的制作于静态库的制作方式十分相似。相对而言只不过是使用的一些gcc的选项不同罢了! 第一步就需要先把对应的原文件编译成二进制目标文件
#生成动态库的二进制文件需要使用选项-fPIC
gcc -fpic -c getMax.c
gcc -fpic -c getMin.c
这里的fpic选项的专业属于是与位置无关码,简单来讲就是生成的二进制代码的装入方式是和程序位置无关的!也就是说是相对地址的方式装入。不过这些都不是我们今天的关注的重点。 要生成动态库,就必须带上-fpic选项来对对应的源文件进行编译!
而接下来,我们就要进行动态库的生成使用如下的命令:
#生成对应的动态库
gcc -shared -o libgetMax.so getMax.o getMin.o
接下来,我们就来规范整个过程,makefile文件内容如下:
libgetMax.so:getMax.o getMin.o
libgetMax.so:
gcc -shared -o libgetMax.so getMax.o getMin.o
getMax.o:getMax.c
gcc -fpic -c getMax.c
getMin.o:getMin.c
gcc -fpic -c getMin.c
.PHONY:dynamic
dynamic:
mkdir -p lib-dynamic/lib
mkdir -p lib-dynamic/include
cp *.so lib-dynamic/lib
cp *.h lib-dynamic/include
.PHONY:clean
clean:
rm -rf *.so *.o lib-dynamic
运行以后:
我们再来看一看对应的lib-dynamic的内容是什么:
到这里,动态库的打包形成也做好了。接下来,我们更改makefile,一次把静态库和动态库都生成了。
.PHONY:all
all:libgetMax.so libgetMax.a
libgetMax.so:getMax.o getMin.o
gcc -shared -o libgetMax.so getMax.o getMin.o
getMax.o:getMax.c
gcc -fpic -c getMax.c -o getMax.o
getMin.o:getMin.c
gcc -fpic -c getMin.c -o getMin.o
libgetMax.a:getMax_s.o getMin_s.o
ar -rc libgetMax.a getMax_s.o getMin_s.o
getMax_s.o:getMax.c
gcc -c getMax.c -o getMax_s.o
getMin_s.o:getMin.c
gcc -c getMin.c -o getMin_s.o
.PHONY:lib
lib:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
mkdir -p lib-dynamic/lib
mkdir -p lib-dynamic/include
cp *.so lib-dynamic/lib
cp *.h lib-dynamic/include
.PHONY:clean
clean:
rm -rf *.a *.so *.o lib-static lib-dynamic
可以看到,这里一次性生成了动态库和静态库。对于如何制作动静态库至此就已经告一段落了! 后面我们就要从库的使用者角度来看库了。
接下来我们从使用者的角度来看待库。前面我们打包了动静态库,而动静态库的最主要的目的就是使用的。所以接下来我们来看一看怎么使用动静态库。
首先我们先来看静态库如何使用,我们写下如下的测试代码:
#include "getMax.h"
#include
int main()
{
int a[]={11,22,33,44};
int max=getMax(a,4);
printf("max=%d\n",max);
return 0;
}
使用gcc编译,发现编译失败,失败信息如下:
发现gcc并不认识这个头文件!这就和头文件的搜索策略有关了!""的方式的搜索策略优先从当前路径寻找头文件,然后就是从系统的路径寻找头文件。然而实际的情况是getMax头文件既不在当前路径,也不在系统的路径里面,所以就会报错! 在这种情况下,我们就要手动告诉gcc头文件的搜索路径。使用的就是-I选项。
#使用-I选项指定gcc的头文件搜索路径
gcc -I ../lib-static/include
结果如下:
虽然依旧是报错,但是可以明显发现:此时的报错已经和头文件没有关系了,而是对于getMax未定义的引用。这个报错是一个链接错误。说明gcc没办法找到这个函数的实现!
我们知道,形成可执行程序需要链接。而由于我们先前使用的都是C/C++的官方库,而这些库gcc/g++默认都认识!而我们今天使用的是自己的库,所以需要告诉gcc库的搜索路径还有需要链接的是哪一个库!所以需要带上下面的链接命令:
#-I跟的是头文件的搜索路径,-L跟的是库文件的搜索路径,-l表示的需要链接的库的名字
gcc test.c -I ../lib-static/include -L ../lib-static/lib/ -lgetMax -o test
可以看到,不仅生成了可执行程序,而且运行出了正确的结果!这就是使用静态库的方式! 下面我们来看一看如何使用动态库
使用动态库的方式和使用静态库的方式是一摸一样的,我们重复前面的步骤:
gcc test.c -I ../lib-static/include -L ../lib-static/lib/ -lgetMax -o test
但是运行起来的时候,发现报错!具体的报错原因如下:
什么?无法打开共享库文件?不存在这个文件!我们使用ldd命令来查看这个程序的动态库依赖:
为什么getMax库会找不到呢?明明已经把路径都指明了,为什么还找不到呢?问题是我们前面做的一切都是给gcc做的,gcc确实知道了库的路径,也帮我们生成了可执行程序。但是这些都是告诉gcc的!可执行程序跑起来变成进程以后,依旧要自己去寻找库的路径! 而如下有四种方式来帮助可执行程序寻找动态库:
首先,系统的头文件在下面的路径下:
/usr/include ---->系统头文件路径
而对应的库文件的路径是:
/lib64 ---->系统的库文件路径
而所谓的安装和卸载库就是把库拷贝到/lib64路径下,由于这种做法不够好,会污染系统的环境,所以这种方式不推荐。感兴趣的读者可以自己实现
实际上,系统每一次加载动态库都会从一个环境变量中读取,这个环境变量
叫做:LD_LIBRARY_PATH 我们只要讲我们对应的库的路径导入到这个变量里面就可以了,即:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH : (lib_path)
执行这条命令以后,查看对应的程序的动态库信息:
但是,首先这个环境变量的名字很难记住,另外这种命令行式的导入下次重新启动就失效了!所以这种方式其实也是相对比较吃力不讨好 接下来我们介绍第三种方式
在系统的etc目录下存在 ld.so.conf.d 这个文件夹,这个文件夹下存储的是一系列的配置文件:
其实里面的存储的内容都特别简单:存储的就是对应库文件的绝对路径!接下来我们也可以把我们的getMax函数库的路径写到这个文件夹里面的配置文件里,后面我们的动态库就可以查找到这个文件了
首先,创建一个ld.so.conf.d路径下创建一个配置文件:
接下来,往配置文件里面写入对应的动态库的路径信息:建议使用绝对路径!
这里我已经写入了。
保存退出以后,使用ldconfig命令是配置立即生效:
此时我们使用ldd查看对应的test程序的动态库依赖关系:
可以看到这个时候,能够找到对应的getMax库了!而且这种方式的修改是永久的!即使退出本shell,下次登陆也是有效的!
最后一种方式,也是官方库里面用的相对较多的一种方式:虽然直接在系统路径下直接拷贝库文件不太好,但是可以在系统的路径下建议软链接来代替直接拷贝库!这里我们也选择建立一个软链接指向我们的库
sudo ln -s [绝对路径] /lib64/软链接名
这个时候我们在使用ldd命令观察对应的test程序的动态库依赖:
可以看到,此时我们的程序依旧是正确找到了对应的对应的动态库信息。所以在系统库路径直接建立软连接的方式也是可以的!
接下来,我们从原理层面来看动态库是如何加载的。
首先,我们还是把进程,进程地址空间,物理内存,页表,还有磁盘全部画出来:
而另外一个进程来,对应做的就是已经加载进物理内存的库的物理内存地址和自己进程地址空间的虚拟地址之间的映射关系使用页表维护好,这样物理内存里面就会只有一份库文件!达到了节约资源的目的
但是,还有一个问题:那么就是要加载库的前提是要先找到这个库,虽然操作系统每次都会从默认的路径去寻找,但是如果默认路径找不到,最终操作系统也就会找不到!所以动态库需要告诉系统对应的库在哪里,以方便操作系统把库文件载入内存!这就是为什么动态库在运行的时候常常找不到的原因!静态库就因为是把代码拷贝进自己的进程PCB中去,所以没有这些问题!
这就是本文的主要内容,如有不足之处,还望指出。希望大家共同进步。