在我们之前提到过一个网吧的例子
库可以分为静态库和动态库
libXXX.a ----静态库,与之相对的就是静态链接
libYYY.so ----动态库,与之相对的就是动态链接
静态链接就是将对应的代码直接拷贝到可执行程序中,从此以后就不在依赖于静态库了
动态库就是将对应的地址添加进去就可以了。
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
如下所示,当我们写了这样一个代码以后
我们需要把我们的方法给别人去用,这里我们可以直接把源文件直接给别人去用,可是我们有时候不想把代码让别人看到。所以我们可以把我们的源代码想办法打包成库,即库+.h文件
关于静态库他是比较简单的,如下所示,其实我们一开始编译链接生成可执行程序的时候本身就需要将其的.o文件给生成出来,然后再进行链接,这里我们也可以将我们本来已有的四个代码的.o文件自己生成出来,然后这四个就可以与main.o链接后形成可执行程序,静态库就是嫌一个一个写太麻烦了,于是将这四个.o文件给打包起来,随后再形成可执行程序
在如下这里中,我们生成.o文件的时候,我们可以不用加上-o mymath.o,因为这里它会将我们这个文件直接形成同名的.o文件
然后我们就需要用一个ar命令进行打包这个.o文件,ar就是一个生成静态库的命令,说白了就是将所有的.o打包形成一个.a
这里的-rc就是replace和create的意思,它的意思是后面的这些.o文件放到libmymath.a这个文件中,如果已存在那就替换,如果不存在那就替换
然后我们可以去运行一下
我们可以看到已经形成了一个libmymath.a了,至此我们就形成了一个静态库了
接下来我们可以直接发布
这样的话,就可以一键式的直接发布了,也将库和头文件进行分开了
所以未来一旦有人想要用我们的库,我们直接将lib文件夹给他就可以了
那么现在,我们就来使用一下这个库,我们先写出如下的代码
注意,这里我们需要加上绝对或者相对路径,因为它只会在系统中的库里面和我们当前目录里面去寻找头文件,所以这里必须加上路径
如果我们非要直接用mymath.h,那么我们需要加上-I选项,这个I代表的就是include,即不仅在当前目录中找,如果找不到,就去指定目录下找
gcc main.c -I ./lib/include/
不过这里我们已经解决了路径的问题了,但是我们会发现还有错误,即找不到add的方法
这个报错其实就是属于链接错误
因为我们如果直接去main.o是可以的
这说明编译的前三个阶段都没错,所以智能出在链接错误
如下所示,我们可以注意到,其实我们这里并没有办法找到静态库
这个问题其实还是前面那个问题,gcc只能去系统中,如/lib64/libc.so,/lib64/libc.a这些以及当前目录去寻找。而我们当前这个是找不到的
如下所示,我们可以在继续加上-L选项,这里的L就是lib的首字母,然后指定对应的路径。而我们在以前不用的原因是因为都在系统已经安装好了
gcc main.c -I ./lib/include/ -L ./lib/mymathlib/
然而,我们会发现,还是会报错
其实这里是需要链接具体的库,那么为什么我们在-I的时候不需要指定头文件呢?这是因为我们的代码里面已经有头文件了,所以只需要告诉路径即可。但是库不行,因为可能有很多个库,到底链接哪个我们不清楚,需要指定。
所以这里我们就还需要一个-l选项,注意这里是库的真实名字,需要去掉lib前缀和.a后缀,且一般这个l紧跟库名称
gcc main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath
像我们以前的C/C++代码是不需要这么麻烦的,因为它是可以找到对应的静态库和头文件的,并且gcc是可以认识C/C++的动态库或静态库的。如果我们使用第三方库的话,就必须这样做了
那么我们怎么样可以做到像C/C++里面的库那么方便呢?
首先我们可以直接将这个头文件拷贝到系统对应的include中,静态库拷贝对应的lib64中,这样的话,我们就只需要指明-l就可以编译过去
或者我们也可以建立软链接,将软链接放到库中,这样也可以不用带这么多选项了,只需要一个-l选项即可
上面是一个加法的案例,如果我们使用除法的时候,我们知道,在我们的代码中,当除数为0的时候,那么结果一定为-1,可是这个-1是本来计算正常,但是结果就是-1,还是说因为发生除零异常导致的-1呢,所以我们就需要添加一个全局变量。
不过在这里我们会发现一个问题,这里的myerrno应该是1,但是结果为什么是0呢?
其实这是因为c语言的传参是从右向左传递的,所以,当myerrno为0的时候,已经被传递过去了,随后才调用的除零,才去导致最终的修改了myerrno了。因此这里我们修改一下代码即可
我们之前说过,使用ldd命令可以查看动态链接或者静态链接
可是这里我们并没有看到mymath这个库,而且这里并没有静态库
这是因为gcc链接时是默认动态链接的。结果也确实证明是动态链接的
现在我们只提供了一个静态库,并且程序是可以运行的,说明程序里面肯定有这个静态库
所以说,gcc默认是动态链接的,但是我们没有动态库只有静态库的时候,那么gcc才会对对应的库进行静态链接。所以我们才看不到对应的mymath库。
- 第三方库,往后使用的时候,必定要使用gcc -l
- 深刻理解errno的本质
- 如果系统中只提供静态库,那么就按照静态链接,否则默认动态链接。也就是说一个可执行程序可能是静态和动态混合链接的(下面解释)
- 如果系统中需要链接多个库,则gcc可以链接多个库(也就是说可以带多个-lXXX)
我们前面提到过,如果我们不想在命令行写-I和-L选项,那么我们可以将我们的库直接拷贝到系统中对应的目录下
sudo cp lib/include/mymath.h /usr/include/
sudo cp lib/mymathlib/libmymath.a /lib64/
现在我们就将这两个库安置到了系统当中
如下所示,我们就可以直接运行了,我们也可以注意到,当我们不指定静态库的时候,会显示找不到myerrno
像我们前面所做的cp操作,其实就是所谓的安装操作
当然,我们一般也不建议这样直接将库拷贝到对应的系统中,因为我们自己写的库会污染系统当中别人写的头文件
我们也可以使用软链接的方式
这样我们是为添加了一个头文件目录
所以我们写代码时候,需要这样写了
我们在对静态库也使用一下软链接
如下所示,最终我们就可以正常运行了
所以说我们未来也可以直接在系统库中建立软链接,不过这种做法我们不太推荐
如下所示,我们先创建以下文件,我们让mymath仍然为静态库,但是让mylog和myprint为动态库
我们先将这些文件的代码写上去
当我们想要形成动态库的时候,与静态库是十分类似的。我们都需要将这些源文件形成.o文件
我们首先先用命令行的方式去实现一下,然后在使用Makefile的方式
对于下面的代码,我们仍然是先生成一个.o文件,不过我们需要加上一个-fPIC选项,这个我们后序进行解释
gcc -fPIC -c mylog.c
gcc -fPIC -c myprint.c
接下来我们将这两个.o文件打包形成库,ar命令是专门用来打包静态库的,而我们现在需要打包动态库,需要以下操作
我们还是用gcc,因为gcc默认就是动态链接,我们需要加上一个shared共享的选项,代表告诉gcc,我们不要生成可执行程序,而是生成一个库,其他的和直接生成可执行程序的步骤基本是一样的
gcc -shared -o libmymethod.so *.o
我们可以注意到,这个动态库是具有可执行权限的,但是它是无法被执行的,毕竟连main函数都没有。而静态库是没有可执行权限的
那么这个可执行权限是什么意思呢?
对于静态库,它的作用就是直接将静态库中的内容拷贝过去。从此以后这个程序的死活就与这个静态库没有任何关系了。更重要的是他是不会被加载到内存当中的。
当我们在运行程序的时候,当要用到动态库的时候,就会跳转到动态库中,从而使得动态库加载到内存中。而要加载就要可执行权限。
所以这个可执行权限就是我们这个文件是否会以可执行程序的方式加载到内存中。库虽然没有main函数,但是它有方法,它以后也要被调用,它也是可执行程序的一种,具有可执行程序的特征的。它不是不能执行,而是不能单独执行,需要别人来用它。
我们现在将他改为Makefile的,如下所示
运行结果如下
至此我们就形成了我们动态库
如下所示,我们先将库拷贝到我们对应的代码中
然后如下所示的代码中,如果我们直接使用gcc,且不考虑动静态库,只考虑动态库,那么找不到头文件
如果我们使用静态库的方式去弄既有静态库又有动态库的,那么也不行
如果我们只考虑动态库的话,那么这样就可以了,就如同静态库的那样
如果还包含静态库,那么后面只需要多加上一个-lmymath即可
以上是已经生成了可执行程序了
我们现在只考虑动态库的,当我们运行的时候,我们会发现出问题了
当我们使用ldd命令的时候,我们可以看到就是动态链接的
使用file命令也可以看到确实是使用动态链接的
关键问题是我们使用ldd时候我们会注意到这个动态库是没有被发现的
但是我们不是已经告诉了这个动态库在哪里吗?为什么在运行的时候还是找不到呢?
这里需要注意这里我们的告诉动态库在哪,告诉的是编译器,一旦当程序形成以后,就和编译器没关系了。这里是加载的时候没找到,所以说,动态库在哪里,也得告诉加载器
那么为什么这里找不到呢?更关键的是,我们以前的C语言中它为什么能找到呢?
所以其实加载的时候也要有对应的路径,而之前的C语言中是因为在系统中有一个默认的搜索路径去搜索动态库,而我们现在写的动态库,是找不到的
为了让他可以找到,有四种方法
最简单的办法就是直接将这个动态库拷贝到/lib64或者/usr/lib64路径下
建立软链接在/lib64或者/usr/lib64
将自己的库所在的路径,添加到系统的环境变量:LD_LIBRARY_PATH中
这个环境我们的云服务器上大概率是没有的,如果有也大概率是因为以前配置vim的时候已经弄上去了
这个环境变量是专门用来给用户提供用户的自定义库的路径的,他是专门用来搜索动态库的路径的
我们可以将我们这个动态库所处的路径给放到这个环境变量中
此时我们就会发现我们这个可执行程序瞬间就有了对应的动态链接
此时在运行就可以跑了
在/etc/ld.so.conf.d下建立自己的动态库路径的配置文件,然后重新ldconfig即可
我们知道,环境变量每次重新启动的时候都会消失,除非我们写在对应的文件中
所以我们还有一种更简单的办法
ls /etc/ld.so.conf.d/
我们可以注意到这里有很多的对应的配置文件
这些配置文件里面放的全是路径,只要我们这里建立一个conf文件,然后把我们的动态库路径放上去,系统自然而然就找到了
然后我们使用这个命令
ldconfig
此时我们就可以发现,我们的代码可以运行了
并且这种方式即便我们将Xshell给关闭了,它依旧是有效的
不过实际情况下,我们用的库都是别人的成熟的库,都采用直接安装到系统的方式,即第一种方式!
最后我们再来将动静态结合的使用一下
我们也可以注意到使用ldd时它是没有静态的部分的。这是因为静态的方法已经拷贝到可执行程序里面了,所以也就跟他没有关系了
即便我们以后将这个mymath静态库给删除掉了,这个程序照样可以运行
而一旦我们将自己的动态库给删掉了,那么程序就跑不了了
我们知道了以下的几点
所以,动态库在系统中加载之后,会被所有进程共享
先看我们以前的这个图,当磁盘中的可执行程序被加载到内存中的时候,那么就会创建task_struct然后里面会有一个进程地址空间,通过页表与物理内存建立映射。在磁盘的这个文件被加载到内存的过程中,会找到对应的inode从而可以读取到文件的属性,同时也会去找到对应的文件的内容,按照一页一页的方式加载到内存当中
当未来在开启一个进程的时候也是一样的道理。
所以此时第一个进程挂掉了,并不会影响第二个进程
我们也知道动态库其实也是一个文件,当我们第一个程序运行的时候,需要执行printf了,需要用到动态库里面的函数了,所以就会将动态库给加载到内存当中。然后在页表上建立映射,这个是在共享区上的
当运行的时候,就会从代码区跳转到共享区,执行完以后,在跳转回去
所以这样的话:建立映射了以后,从此我们执行的任何代码,都是在我们的进程地址空间中进行执行的!!!
所以我们还知道一个事实:系统在运行中,一定会存在多个动态库,OS管理起来,先描述,在组织,系统中,所有库的加载情况,OS非常清楚
所以当未来第二个进程,还需要用到这个同样的共享库的时候,就不会再去加载了,而是直接使用了
所以这个库叫做动态库,也叫做共享库,他是通过地址空间加上页表进行完成的,就可以实现对所有进程的共享。
那么我们这里还有一个问题,那就是如果这个动态库里面有一个全局变量,比如errno,那么当一个进程中对这个变量进行了修改以后,那么岂不是所有的都要进行修改?
其实是不会出现这种情况的,因为这个共享区就在堆栈之间,这里也是处于用户空间的,在写入的过程会发生写时拷贝