我们在学习C语言阶段其实就已经知道一个可执行程序的形成过程分为预处理、编译、汇编、链接这四个阶段,而且也知道我们程序中使用的各种库其实是在链接的阶段加载的。
可我们那时候并不知道库是怎么被加载的,或者库是怎么形成的,所以今天我们就要好好的来聊一下,库的形成。
我们知道链接阶段其实是将形成可执行程序的各种.o目标文件连接起来形成可执行程序,但是对于一个库来说,可是如果一个可执行程序使用的很多函数分散在不同的.o文件中,那么这样一个一个的链接就显得很麻烦。
就例如我们现在有这样的一个场景:
如上图,我们有这样一堆头文件和对应的方法实现,我们想要模拟编译器的行为先形成.o文件,再将它们连接形成可执行程序。我们当然可以一个一个的将它们编译形成.o文件,然后再连接:
但这样实在太麻烦,特别是后面链接形成可执行的操作,如果今天我们的可执行程序依赖了100或200个.o文件,那这样不累死人?
所以为了方便,我们可以将一些有关联的.o文件再次打包,形成一个库文件,那这样我们在连接的时候就只需要找到这个库文件就行了。
所以库的封装其实就是将一批.o文件打包。
我们打包静态库使用的指令是ar -rc指令,例如我们想要将上面的这些方法打包成一个名为mymath的静态库,对应的指令是这样的:
注意:上面所写的库名称为libmymath.a,但是该库的真实名称并不是libmymath,而是mymath,这是因为库文件必须要一个前缀lib,而.a是静态库文件的后缀名。
但是这样使用起来会有点麻烦,这是因为gcc在编译形成可执行程序的时候默认值知道系统默认的库的名称和路径,我们现在这个是我们自己新建的库,所以gcc不认识。所以我们在编译的时候还需要特别指定库名称和库路径。
指定库名称使用的是-l选项,执行库路径使用的是-L选项:
这样我们才能成功的生成可执行程序:
但我们今天还是想要做得更规范一些,一般我们的库文件和头文件是被放到一个目录结构中的,这样就更像一个整体。
我们编写一个makefile来完成再次打包的工作:
这样执行了makefile之后,我们就得到了一个更规范的库文件了:
然后使用的时候就需要注意了,因为我们上面的演示中头文件是和test.c处在同一个路径下的,所以编译的时候不需要指定头文件的路径编译器也能直接找到,而我们今天已经形成了一个库文件,也就是所我们以后其他的源代码想要用这个库中的方法,就只需要有这个库文件即可,而其他的源代码的路径下就不会有对应的头文件了,所以我们在使用的时候还需要多加一个-I的选项表示指定头文件的路径:
动态库的封装就和静态库有点不一样了,首先动态库的封装是直接使用gcc的,这说明gcc是可以直接形成动态库的。这个后面我也会聊一些,这是因为动态库比起静态库来说更重要,优先级也更高。
第二个不同的地方也比较特殊了,我们在形成对应的.o文件的时候还需要额外的加上一个“与位置无关码”fPIC的选项,这个后面我们简单的聊一些,其实我也不是很懂,记住就行了。
此外还需要加上一个-shared的指令,表示生成的是“共享库”,这个也是到后面聊:
其他的地方就没什么不同了,因为库的本质动态库和静态库是一样的。
所以我们就只需要修改一下makefile即可,也就是像上面生成静态库一样,将二次打包也做好:
执行makfile之后我们就形成了动态库了:
然后使用动态库生成可执行程序的时候也是个静态库一样:
如果直接运行的话,会提示找不到动态库,这时候大家可能就会有疑问了,我在形成可执行程序的时候不是已经告诉了编译器库的路径了吗?怎么还会找不到呢?
这是因为静态库和动态库的加载是不一样的。
严格来说,静态库是不需要加载的,因为静态库是在链接的时候将对应的方法一代码的形式“拷贝”到我们可执行程序的代码部分,所以自然的,运行的时候就不要再找静态库了。
而动态库不同,动态库并不是以代码的形式将拷贝到可执行程序的代码部分,它是在运行的时候才加载到“内存”,并不是加载到可执行程序中,然后由可执行程序在内存中去寻找。
所以我们也就知道了,我们编译形成可执行程序的时候只是将库的路径告诉了编译器,但运行是操作系统的事,操作系统当然不知道我们的库在哪里了。
如果想要解决动态库运行时候找不到库的问题,可以有一下四种解决方案。
方法1:直接将动态库安装到系统中默认路径下
我们以前在编译可执行程序的时候,之所以不需要指定库路径和头文件的路径,是因为我们以前使用的都是系统默认支持的库,而这些库的头文件都会存在于系统中的两个默认的路径:/lib64和/usr/include中。
所以我们今天就可以直接将我们的头文件和库拷贝到这两个路径中:
这样我们再次运行就可以正常运行了:
并且如果我们今天将可执行程序删掉在重新编译形成,也不再需要指定头文件路径和库路径了,只需要指定库名称即可:
因为我们的头文件和库已经存在于系统的默认路径下了,所以在编译的时候就像我们以前使用系统库一样再默认路径下寻找了,而之所以还需要指定库名称是因为我们实现的mymath本质上还是属于第三方库,而第三方库gcc是不认识的。
而且我们在使用ldd指令来查询可执行程序所依赖的库的时候也是可以查到的,并且显示的路径就是/lib64:
而如果我们将刚才加入的头文件和库都删掉,在用ldd查询的时候就会显示找不到对应的库了:
同时将库直接安装到系统的默认路径下也是我们以后使用第三方库最推荐的一种做法,没有之一。
方法2:在当前路径下创建连接到库的软连接:
其实这也是软连接的一个应用场景,当然啦我们也可以将软连接创建到/lib64目录下,我当前就不做了。
方法3:更改环境变量
在我们的系统中有许许多多的环境变量,而其中有一个是和我们今天动态库加载有关的环境变量——LD_LIBRARY_PATH,这个环境变量就是用来存储我们需要加载的动态库的绝对路径。
但是有的朋友可能这个环境变量是空的或者不存在,这也是很正常的,因为有可能你的系统比较新或者你从来没有设置过这个环境变量。
设置了之后我们就可以,正常的运行我们的可执行程序了:
那我们最后再来做一个实验,就是如果同一套方法,我们机提供了静态库也提供了动态库,编译器会选择哪种库呢?
我们可以先将两个库拷贝到同一个目录下:
然后我们在重新生成可执行程序,再来查看一下它所依赖的库:
从结果中我们可以看到,默认依赖的是动态库,虽然这里找不到,但是也能显示出来我们所依赖的是哪一个库(找不到是因为我们现在库的路径变了,而且我们也把原来的动态库给删除了)。