静态链接库和动态链接库
Windows下的编程环境
由于我对windows的编程环境比较熟悉,所以首先对Windows下的编程环境进行一个简单的介绍。在Windows中进行C++编程时,我们会经常调用一些函数库,因此不可避免地会经常接触到obj、lib和dll这些文件,当然也会接触到各种include目录。
OBJ文件
obj文件由编译器(compiler)编译生成,其内容为本地代码(naive code,对应于特定CPU的机器代码),其中包括外部符号,也就是对其他代码函数(或者库函数)的引用(所以如果出现每个函数未出现在其他代码中或者引用库函数中,在链接阶段就会出现“未解析的外部符号”的错误)。
在windows中,cpp文件是源代码文件,lib、dll和exe文件属于最终的生成文件。obj文件是一种介于源代码和最终生成文件的中间代码。通常一个程序(例如exe文件)由多个源代码组成,或者依赖其他函数库(lib、dll等文件),单个obj文件不具有整个程序运行所需要的所有内容。形成一个完整的可执行文件,需要通过链接器(linker),将不同源代码生成的obj文件和所调用的库函数链接到一起。因此,obj文件是一种中间代码而非最终的可执行文件。
DLL文件
首先介绍dll文件的作用。dll文件通常称为动态链接库(dynamic link library),它的格式和exe文件格式相同,属于可执行文件。在以下情况中,需要使用到dll文件:
-
在程序运行的时候,windows系统会从当前目录(./)、系统目录(C:/Windows/System32、C:/Windows/System)、Windows目录(C:/Windows)或者环境变量PATH中申明的路径中寻找对应的dll文件。
-
在程序编译的链接过程中,只有动态链接库dll文件是不够的,还需要使用对应的.lib文件。lib文件中包含了所生成可执行文件所依赖的库函数地址,链接器(linker)需要在lib文件中找到对应的函数符号(symbol)。这样编译生成的可执行文件就可以在运行时找到对应库函数实现代码在内存中的地址,从而调用对应的模块。
LIB文件
接着介绍lib文件的作用。lib文件通常被称为静态链接库(static library)。纯粹的静态链接库包含了函数符号和函数实现代码。在编译的链接阶段,静态链接库中的内容被包括到了编译得到的可执行文件中。因此,在只链接了静态链接库的情况下,可以认为所得到的编译结果不依赖于外部动态链接库。
我个人认为“静态链接库”这个称呼不够贴切,可以从上面dll文件的作用中看出,编译的链接过程中,动态链接库也会使用到lib。在这种情况下,我们把lib文件称为静态链接库,从字面上给人的感觉就是和动态链接库是互斥的。所以,我认为将lib文件称为引入库文件(import library)更加合适。不过,从另外一方面来说,引入库文件在链接时将所要调用的函数在dll文件中的入口链接到了对应的生成文件中,也可以认为是一种静态链接。
DLL和LIB的总结
静态链接在代码编译的时候,进行静态加载;而动态链接在程序运行的时候,进行动态加载。lib在编译阶段需要用到,而dll文件在程序运行时需要用到。如果只需要完成源代码编译,那么在编译阶段添加lib文件就够了;如果要让编译得到的程序运行起来,那么就分为2种情况:
- 如果编译时使用的lib文件是静态链接库,也就是包含了函数符号和函数实现。在这种情况下,编译得到的程序可以直接运行;
- 如果编译时使用的lib文件是引入库,即只包含函数符号,不包含函数实现。在这种情况下,编译得到的程序要运行起来,就需要使用到dll文件,因为dll文件包含了程序运行所要使用到的函数实现代码。
如果一个函数库使用动态链接库的形式进行发布(release),当然这个函数库是用来给其他开发者调用的,那么这个函数库需要包含lib文件和dll文件。lib文件包含了函数符号,也就是告诉程序到哪里找引用函数的实现代码,而dll文件包含了代码的实现。因此,对于开发者来说lib文件和dll文件二者都必不可少。编译得到的程序中只包含了所调用函数的函数符号,但是并未包含所调用函数的实现代码,因此程序运行的时候仍然需要附带对应的dll文件。
如果一个函数库采用了静态链接的方式发布,那么只需要提供lib文件就可以了。因为lib文件中已经包含了函数符号和函数实现,也就是说已经包含了程序所有所需要的内容。在程序编译阶段,lib文件的内容已经被链接到了程序中,这样程序在运行的时候不需要再额外提供lib文件了。
另外,在设计动态链接的函数库的时候,需要将提供给其他开发者使用的库函数进行导出(export)。被导出的函数的函数符号将会出现在lib文件中,可以被外部程序调用。而未导出的函数不会出现在lib文件中,只能被dll文件中的内部函数所调用。静态链接库中的函数则不需要进行导出,函数可以直接被外部程序所调用(除了加static关键词的函数)。
静态链接和动态链接的优缺点
静态链接是将程序运行所需要的代码都整合到程序中,因此对外部环境的依赖较小,不需要添加额外的依赖文件(例如dll文件)。这是静态链接最大的优点,也是静态链接的问题所在。静态链接将用到的代码都添加到程序中,就会导致编译得到的程序,无论是在内存上还是在硬盘上,占用空间比较庞大。
而动态链接的引入,主要用于解决静态链接的问题,其优点如下(同时也是静态链接的缺点):
-
优化存储。动态链接可以将被多个程序使用到的代码分离出来,内存中只保存了一份代码,可以同时被几个程序调用,这样有效节约了内存资源;
-
方便程序的维护升级。对于程序的某一部分进行升级的时候,只需要在保持接口不变的情况下,更新对应部分的dll文件中的函数实现,而不需要对整个程序工程进行重新编译。这种管理方式,在大型软件工程中有利于功能模块化和编码的分工,极大地提高了工程效率。 动态链接的缺点在于:由于依赖大量dll文件,使得软件的配置变得复杂,不方便在不同的机器上进行运行,时常会遇到“缺少某个dll文件”的错误,而使得程序无法正常运行。
Linux下的编程环境
在上一节Windows编程环境中,已经介绍完了静态链接库和动态链接库的基本概念。这一节将在上一节的基础上,对照Windows下的编程环境进一步介绍Linux下的编程环境。首先,介绍Linux下目标文件、静态链接库和动态链接库与Windows上的对应关系:
- o文件:称为目标文件(object),相当于Windows下的obj文件,由对应的源文件编译生成;
- so文件:称为动态链接库(dynamic linke library, shared object),相当于Windows下的dll文件;
- a文件:成为静态链接库(static library, archive),相当于Windows下的lib文件。 编译过程
由于Windows下,开发者通常采用Visual Studio等集成开发环境(IDE)。对于开发者来说,程序的编译链接通常是一步完成的。因此,不容易接触到,IDE在编译时每一步都进行了什么操作。而Linux下使用gcc(GNU Compiler Collection)编译能够很好的体会编译的过程,下面通过gcc编译的例子来介绍Linux中程序的编译的具体过程。
对于如下C语言程序,使用gcc进行编译。
//test.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
以上程序可以通过gcc一步到位完成编译,命令如下:
$ gcc test.c -o test
实质上,以上编译过程可以细分为如下步骤:预处理(也称为预编译,preprocessing)、编译为汇编代码(compilation)、汇编(assembly)和链接(linking)。
-
预处理
$ gcc -E test.c -o test.i 或 gcc -E test.c
生成的test.i文件存放着test.c经过预处理之后的代码,也就是将stdio.h文件中的内容插入到了test.c文件中了。
-
编译为汇编代码
$ gcc -S test.i -o test.s.
预处理之后,直接对生成的test.i文件进行编译,生成汇编代码test.s。
-
汇编
$ gcc -c test.s -o test.o
将汇编代码test.s进行编译生成目标文件test.o。
-
链接
$ gcc test.o -o test
将程序的目标文件和所有附加的目标文件链接到一起,生成最终的可执行文件test。其中附加的目标文件包括静态链接库和动态链接库。
函数库链接
下面以MySQL Connectors的C库为例,介绍使用gcc链接库函数的过程。函数库包括一个include文件,路径为/usr/dev/mysql/include;一个lib文件,路径为/usr/dev/mysql/lib,里面包含二进制so文件libmysqlclient.so。
-
编译得到目标文件
$ gcc –c –I /usr/dev/mysql/include test.c –o test.o
-
链接生成可执行文件
$ gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
-
也可以强制链接时使用静态链接库
$ gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。
在/usr/dev/mysql/lib目录下有链接时所需要的库文件libmysqlclient.so和libmysqlclient.a,为了让gcc在链接时只用到静态链接库,可以使用以上命令。
函数库文件路径
静态库链接时搜索路径顺序:
- ld会去找gcc命令中的参数-L;
- 再找gcc的环境变量LIBRARY_PATH;
- 再找内定目录 /lib、/usr/lib和/usr/local/lib,这是当初compile gcc时写在程序内的。
动态链接时、执行时搜索路径顺序:
- 编译目标代码时指定的动态库搜索路径
- 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
- 配置文件/etc/ld.so.conf中指定的动态库搜索路径
- 默认的动态库搜索路径/lib
- 默认的动态库搜索路径/usr/lib
相关环境变量
-
LIBRARY_PATH环境变量:指定程序静态链接库文件搜索路径
-
LD_LIBRARY_PATH环境变量:指定程序动态链接库文件搜索路径