动静态库简述

动静态库简述
在本篇我打算从三个角度为大家讲解动静态库的知识这三个角度分别是怎么制作库怎么使用库以及动态库是怎么加载的。在具体讲解之前我们要知道在Linux中静态库文件一般是libxxx.a动态库文件是libxxx.so(xxx是具体的库名称)。在编译的过程中如果链接方式是静态链接那么使用的就是静态库。如果链接方式是动态链接使用的就是动态库。静态库制作静态库为了更好地讲解制作静态库的方法我们将亲手制作一个简单的静态库例如数学库(mymath)库中的方法如下// mymath.h #pragma once extern int myerrno; int add(int x, int y); int sub(int x, int y); int mul(int x, int y); int div(int x, int y);// mymath.c #include mymath.h int myerrno 0; int add(int x, int y) { return x y; } int sub(int x, int y) { return x - y; } int mul(int x, int y) { return x * y; } int div(int x, int y) { if (0 y) { myerrno -1; return -1; } return x / y; }现在我们已经完成了库中的方法再讲如何制作静态库之前我有一个问题要问大家我们怎们才能让其他人使用我们自己的方法呢首先我们可以将我们的方法源码发给其他人(.c和.h)这样他们就能够自己使用。但这样就存在一个问题其他人可能会借鉴或者抄袭代码更过分的是声称那是他们创造的代码这种情况下我们的利益就会受损。所以直接将源码交给他人不太现实那我们要怎么办呢我们先回想一下程序变为可执行文件的过程源代码经过预处理编译和汇编生成目标文件(.o文件)后多个目标文件链接才会生成可执行程序。那么如果我们将自己的方法实现后再编译成目标文件其他人能否使用我们的方法呢答案是肯定的。我们这么做允许其他人使用我们编写的方法的同时他们也无法轻易查看方法实现的细节。如果我们要分享多个源程序我们可以这多个源程序编译成目标文件后将其打包而这便是静态库。简单地说静态库本质上是多个目标文件的集合。原理已经说完了接下来就要讲具体怎么制作静态库了。首先我们需要将所有的源文件编译成目标文件。需要注意的是我们希望将自己所写的方法分享给其他人也就是说生成静态库的源代码中不包含main函数所以在生成静态库阶段不需要将main所在源代码编译成目标文件。我们只需要使用gcc即可即gcc -c mymath.c接着我们要使用ar工具这是gnu归档工具具体命令如下。如果有多个目标文件生成一个静态库我们可以在命令末尾继续列举。ar -rc libmymath.a mymath.o // 多个目标文件 ar -rc libmymath.a add.o sub.o我们可以使用ar -tv查看静态库中的文件。其中-t代表列出静态库的文件-v表示详细信息例如我们可以看到我制造的静态库mymath中只有一个文件那就是mymath.o。同时为了更加规范我将库文件 libmymath.a 放到了 ./test/lib 目录中头文件 mymath.h 放到了 ./test/include 目录中。具体如下// test.c #include mymath.h int main() { printf(5 7 %d\n, add(5, 7)); return 0; }静态库制作好后接下来我们该用一用静态库了。使用静态库测试代码就是刚刚提到的 test.c现在我们要生成可执行程序了。使用指令 gcc结果报错错误内容是无法找到头文件很明显这是编译时报错但是为什么会出现这个错误呢这和 gcc 编译时头文件的搜索规则有关。头文件搜索使用gcc编译源文件时包含头文件的方式不同其头文件的搜索规则不同。如果是使用尖括号包含头文件那么在编译时会跳过当前目录直接去系统标准路径搜索。如果是使用双引号包含头文件编译器会优先在源文件所在目录中搜索(不会搜索该目录下的子目录)。但如果没有找到就会去系统标准规则搜索。所以要解决头文件找不到的问题有简单的办法例如将头文件和源文件放在同一个目录下或者将头文件直接放在系统标准目录中像/usr/include等等。我们可以通过下面的命令输出gcc内置的所有头文件搜索路径在我的机器中运行的部分结果如下gcc -v -E -x c /dev/null前两个方法可以解决问题不过如果你注重自己文件的结构性的话将源文件和头文件放在同一目录下就不太适合。同时如果是我们自己写的库还是不要直接复制在标准路径中了因为可能会污染系统的头文件目录。当然最终决定权在你自己。除了直接复制外我们还能为自己的头文件建立软链接这会减少我们复制的麻烦但仍会污染系统的目录。那么有没有其他方法让我们直接能够找到我们的头文件呢方法一编译选项-I(大写i)我们可以使用这个选项手动指定搜索目录当手动指定目录后被指定的目录搜索的优先级仅次于使用双引号引用头文件时的“当前源文件目录”如果在指定目录中没找到的话则按照之前的规则搜索。例如通过指定目录编译器能够编译源代码了最终可以生成目标文件。我们可以看到在第一次尝试生成可执行程序时失败了而失败原因是无法找到add函数。这就是链接时错误了具体的之后再谈。方法二通过环境变量指定路径gcc在预处理时它会读取专门的环境变量补充头文件搜索路径。这样的环境变量有两个一是C_INCLUDE_PATH它仅仅针对C语言头文件。另一个是CPATH它对C/C头文件都有效。在该环境变量中通过冒号(:)分割不同的路径。下图是具体演示图中我们使用的环境变量是C_INCLUDE_PATH环境变量CPATH我们就不演示了。当然这样设置的是临时环境变量等我们下次重新登陆时就会消失。如果想要设置成永久的就将设置环境变量的命令写入文件~/.bashrc或~/.profile然后执行source ~/.bashrc(具体看你想写哪个文件)。在之前我们提到过gcc编译时头文件的搜索路径优先级问题现在我们就总结一下头文件包含方式完整搜索优先级(由高到低)#include xxx.h1. 当前源文件所在的目录(不进行子目录递归搜索)2.-I选项指定的目录(如果有多个-I指定按顺序)3. 环境变量C_INCLUDE_PATH/CPATH4. 系统默认路径#include xxx.h1.-I选项指定2. 环境变量C_INCLUDE_PATH/CPATH3. 系统默认路径现在已经解决了头文件的搜索问题我们该解决一下链接时具体实现找不到的问题了。我们已经知道了gcc将源文件编译成目标文件(.o)时它会搜索引用的头文件然后将其展开。那么在多个目标文件链接时是否需要搜索函数的具体实现呢(搜索对应的库文件)答案是肯定的。既然链接时要搜索文件那么肯定会有搜索规则甚至是搜索路径链接时的搜索又是什么样的呢库文件搜索在讲具体的库文件搜索规则之前我们需要知道链接库文件时的相关知识gcc在链接库时只认识固定命名规则的库文件就是之前说的静态库名字是libxxx.a动态库名字是libxxx.so。而且库文件的搜索规则是按优先级顺序查找直到找到符合命名的库就会停止查找。库文件的核心搜索优先级如下1. 手动通过-L指定库目录。这是优先级最高的库搜索方式同时我们可以使用多个-L参数指定多个目录此时按先后顺序进行搜索。2. 通过环境变量指定库目录(对应头文件搜索时的C_INCLUDE_PATH/CPATH)。这时我们要使用的环境变量是LIBRARY_PATH使用冒号分隔多个路径即可。也许你的机器上就没有这个环境变量这很正常我也没有。3. gcc内置的系统默认库目录。对于这个不同的Linux发行版路径略有差异但也能找到具体描述。(接下来都是猜测)不知道你们是否记得之前为了找头文件搜索的系统默认路径使用的命令gcc -v -E -x c /dev/null这个命令的运行结果很多行其中有行是LIBRARY_PATH。我觉得这里出现的和上文说的环境变量不是一回事这里的LIBRARY_PATH指的就是gcc内置的系统默认库目录。之后我们可以验证一下。现在我们已经明白了库文件搜索的具体规则我们就能够解决库文件找不到问题了。我们先尝试一下环境变量我们发现虽然我们将路径加入到环境变量里了但是仍然是这个错误这是为什么呢这个问题我们先放置不答我们先试试其他的办法我们在系统默认库目录中建立对应的软链接试一试即使在gcc的系统默认库目录中这个错误仍然没有解决。至于-L指定它也无法解决这个问题为什么呢gcc找不到头文件我们指定头文件。gcc找不到库文件我们将它能找到库文件可是为什么它还是不能将源程序编译成目标文件呢虽然我们将库文件放到了gcc的系统默认库目录中或者使用环境变量或-L告知编译器库所在的路径但是一个目录中有很多文件编译器又怎么知道它需要什么库呢所以除了上面的步骤之外我们还需要指明需要链接的具体库名称。指明库名称我们需要使用选项-l(小写)同时需要注意的是这里的库名称指的是libxxx.a中的xxx而非库文件的全称。当我们再使用-l指定库名称之后我们就能够将源代码编译成可执行文件了。具体如图到目前为止我们已经讲了静态库的制作以及使用。我们自己制作的静态库使用时确实比较麻烦不过编译时涉及到的一些选项是可以进行简化的例如使用环境变量将头文件或者库文件放到系统默认路径下。无论我们是否进行简化使用时都不要忘记-l指定库名称。但是我们在编译C/C程序时我们没指定过C/C标准库文件也能够将源程序编译成可执行程序这是为什么因为我们使用的编译器是默认自动链接标准库文件的不需要我们自己指定标准库文件。动态库制作动态库到现在我们虽然谈不上非常了解静态库但是我们已经可以自己制作并使用自己的静态库了。有了这个基础对于动态库的制作也就比较简单了。实际上无论是动态库还是静态库它们都能够被看做已经编译好的目标文件的集合。因此制作动态库第一步便是将库源文件编译成目标文件。不过和静态库不同的是在编译时我们需要添加编译选项-fPIC这个选项的意思是产生位置无关码。具体的问题之后再讲我们先将动态库制作出来然后用起来再说。具体示例如图生成目标文件后我们就需要将目标文件编成库。我们需要使用的指令仍然是gcc这次也需要加上其他选项即-shared它的意思是生成共享库格式。示例如图我们制作了自己的动态库可以看到生成的动态库默认就具有可执行权限但实际上它并不能运行。为什么gcc在生成动态库时要给予它可执行权限呢我们知道程序在运行时会创建对应的进程同时会将磁盘上的内容数据加载到内存上。进程在执行库函数时会跳到对应的函数位置执行相关的代码这说明动态库也需要将自己的内容数据加载到内存上。因为使用动态库时编译器的链接方式是动态链接因此在程序运行时(程序的代码已经加载好)才会加载动态库的代码所以动态库需要具有将内容数据加载到内存的能力所以它需要有可执行权限。使用动态库动态库已经制作完毕了接下来就是使用动态库了我写了一个简单的测试程序main.c同时我将目录的文件结构调整了一下具体如下#include myerror.h #include myprint.h int main() { errlog(hello, 动态库\n); print(); return 0; }使用动态库和使用静态库几乎一模一样在生成可执行文件时我们同样需要注意头文件搜索和库文件搜索的规则具体的之前已经讲过了。示例如下我们生成了可执行文件但运行不了。报错原因是加载共享库(动态库)时无法打开相应库文件并且不是权限的问题而是根本找不到对应的库文件。对于这个错误我们也可以使用指令ldd它会告诉我们程序所依赖的库例如可以看到库libmyio.so的地址是未知的。明明我们在编译时已经告诉了编译器路径为什么运行时找不到对应的库文件呢很简单编译器的工作是生成可执行程序程序加载和它就没有关系了。所以我们告诉了编译器没有告诉加载器加载程序时就找不到文件了。那么怎么才能将库文件路径告诉加载器呢我们要知道程序加载时也有一套动态库的搜索规则它的搜索优先级如下1. 环境变量LD_LIBRARY_PATH指定的目录(注意和LIBRARY_PATH进行区分)。示例如图2. 可执行文件的rpath路径。这个路径可以在编译时指定我们可以使用-Wl,-rpath./lib添加路径运行时加载器会自动搜索这个路径。示例如下3. 系统配置文件下的目录。在程序加载时加载器还会搜索系统配置文件/etc/ld.so.conf或者/etc/ld.so.conf.d/记录的路径。我们可以将动态库所在路径加入/etc/ld.so.conf文件中或者在/etc/ld.so.conf.d/目录下新建一个conf文件然后使用sudo ldconfig更新缓存即可示例如下4. 默认系统动态库目录。在没有找到动态库时加载器最终都会在系统的默认目录中搜索对应的库文件所以我们只要将自己的库文件放在对应目录下就可以了。系统默认目录有很多例如/lib,/usr/lib,/usr/local/lib等等。示例如下讲到这里我们已经能够自己制作并使用动静态库了。我们一直说的安装一个库简单地说就是将我们下载的成品库的路径进行设置(例如复制创建软链接或者配置环境变量)从而然我们编译和运行时能够简单一点但在使用第三方库进行编译时指明需要的库名称是必不可少的。本篇对于动静态库的讲解到此结束本文为个人观点如有看法欢迎在评论区讨论。