静态链接库和动态链接库

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文件:

LIB文件

接着介绍lib文件的作用。lib文件通常被称为静态链接库(static library)。纯粹的静态链接库包含了函数符号和函数实现代码。在编译的链接阶段,静态链接库中的内容被包括到了编译得到的可执行文件中。因此,在只链接了静态链接库的情况下,可以认为所得到的编译结果不依赖于外部动态链接库。

我个人认为“静态链接库”这个称呼不够贴切,可以从上面dll文件的作用中看出,编译的链接过程中,动态链接库也会使用到lib。在这种情况下,我们把lib文件称为静态链接库,从字面上给人的感觉就是和动态链接库是互斥的。所以,我认为将lib文件称为引入库文件(import library)更加合适。不过,从另外一方面来说,引入库文件在链接时将所要调用的函数在dll文件中的入口链接到了对应的生成文件中,也可以认为是一种静态链接。

DLL和LIB的总结

静态链接在代码编译的时候,进行静态加载;而动态链接在程序运行的时候,进行动态加载。lib在编译阶段需要用到,而dll文件在程序运行时需要用到。如果只需要完成源代码编译,那么在编译阶段添加lib文件就够了;如果要让编译得到的程序运行起来,那么就分为2种情况:

如果一个函数库使用动态链接库的形式进行发布(release),当然这个函数库是用来给其他开发者调用的,那么这个函数库需要包含lib文件和dll文件。lib文件包含了函数符号,也就是告诉程序到哪里找引用函数的实现代码,而dll文件包含了代码的实现。因此,对于开发者来说lib文件和dll文件二者都必不可少。编译得到的程序中只包含了所调用函数的函数符号,但是并未包含所调用函数的实现代码,因此程序运行的时候仍然需要附带对应的dll文件。

如果一个函数库采用了静态链接的方式发布,那么只需要提供lib文件就可以了。因为lib文件中已经包含了函数符号和函数实现,也就是说已经包含了程序所有所需要的内容。在程序编译阶段,lib文件的内容已经被链接到了程序中,这样程序在运行的时候不需要再额外提供lib文件了。

另外,在设计动态链接的函数库的时候,需要将提供给其他开发者使用的库函数进行导出(export)。被导出的函数的函数符号将会出现在lib文件中,可以被外部程序调用。而未导出的函数不会出现在lib文件中,只能被dll文件中的内部函数所调用。静态链接库中的函数则不需要进行导出,函数可以直接被外部程序所调用(除了加static关键词的函数)。

静态链接和动态链接的优缺点

静态链接是将程序运行所需要的代码都整合到程序中,因此对外部环境的依赖较小,不需要添加额外的依赖文件(例如dll文件)。这是静态链接最大的优点,也是静态链接的问题所在。静态链接将用到的代码都添加到程序中,就会导致编译得到的程序,无论是在内存上还是在硬盘上,占用空间比较庞大。

而动态链接的引入,主要用于解决静态链接的问题,其优点如下(同时也是静态链接的缺点):

Linux下的编程环境

在上一节Windows编程环境中,已经介绍完了静态链接库和动态链接库的基本概念。这一节将在上一节的基础上,对照Windows下的编程环境进一步介绍Linux下的编程环境。首先,介绍Linux下目标文件、静态链接库和动态链接库与Windows上的对应关系:

由于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)。

  1. 预处理

     $ gcc -E test.c -o test.i 或 gcc -E test.c
    

    ​生成的test.i文件存放着test.c经过预处理之后的代码,也就是将stdio.h文件中的内容插入到了test.c文件中了。

  2. 编译为汇编代码

     $ gcc -S test.i -o test.s.
    

    ​预处理之后,直接对生成的test.i文件进行编译,生成汇编代码test.s。

  3. 汇编

     $ gcc -c test.s -o test.o
    

    ​将汇编代码test.s进行编译生成目标文件test.o。

  4. 链接

     $ gcc test.o -o test
    

    将程序的目标文件和所有附加的目标文件链接到一起,生成最终的可执行文件test。其中附加的目标文件包括静态链接库和动态链接库。

函数库链接

下面以MySQL Connectors的C库为例,介绍使用gcc链接库函数的过程。函数库包括一个include文件,路径为/usr/dev/mysql/include;一个lib文件,路径为/usr/dev/mysql/lib,里面包含二进制so文件libmysqlclient.so。

  1. 编译得到目标文件

     $ gcc –c –I /usr/dev/mysql/include test.c –o test.o
    
  2. 链接生成可执行文件

     $ gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
    
  3. 也可以强制链接时使用静态链接库

     $ gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
    

默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。

在/usr/dev/mysql/lib目录下有链接时所需要的库文件libmysqlclient.so和libmysqlclient.a,为了让gcc在链接时只用到静态链接库,可以使用以上命令。

函数库文件路径

静态库链接时搜索路径顺序:

  1. ld会去找gcc命令中的参数-L;
  2. 再找gcc的环境变量LIBRARY_PATH;
  3. 再找内定目录 /lib、/usr/lib和/usr/local/lib,这是当初compile gcc时写在程序内的。

动态链接时、执行时搜索路径顺序:

  1. 编译目标代码时指定的动态库搜索路径
  2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
  4. 默认的动态库搜索路径/lib
  5. 默认的动态库搜索路径/usr/lib

相关环境变量

Table of Contents