0%

静态链接库与动态链接库

背景

我写了一个矩阵的基本运算库,头文件matrix.h中是class matrix成员变量和方法的声明,方法的具体实现放在matrix.cpp中。在main.cpp#include <matrix.h>调用库文件。

以往要编译这种文件的时候,往往都是g++ matrix.cpp main.cpp -o main直接生成一个main可执行文件。如果项目还有还多其他的依赖呢?继续把全部代码打包成一个很大的可执行文件吗?如果日后很多程序都复用这个模块,是否会存储和加载冗余的代码?

静态链接与动态链接

首先我们从C++的编译过程入手,一般一个C++程序被编译的过程分几步:预编译,编译,汇编,链接。

g++ -c matrix.cpp -o matrix.o通过制定-c参数,告诉编译器只编译matrix.cpp,但不要链接,而生成的matrix.o文件称为对象文件(object file)(如果不指定-o输出文件,默认输出cpp文件同名的*.o文件)。这条命令完成了链接之前的全部过程:编译和汇编。接下来可以将汇编生成的二进制对象文件matrix.o和库一起链接,打包成可执行文件main

msvc下对象文件为*.obj

g++ main.cpp matrix.o -o main

或者:

g++ -c main.cpp -o main.o
g++ main.o matrix.o -o main

在不指定-c参数的时候,g++会自动识别参数中的文件类型并可执行文件。

这种过程生成和直接g++ main.cpp matrix.cpp -o main的构建过程等价。在编译期链接阶段生成的对象文件(.o)需要和相关函数库链接,形成一个可执行文件。程序在执行时,直接从该文件加载相关库,这种链接方式叫做*静态链接

如果把一些库函数的链接过程推迟到运行时进行,这就是动态链接,参与链接的库被成为动态链接库(dynamic link library),在Windows下动态链接库文件表现为*.dll的形式,在Linux下只是*.so

静态链接库

静态连接过程中使用的相关的函数库被称作是静态链接库(static library),在Windows下常常以*.lib文件出现,在Linux下是*.a文件。

静态链接库同是经过汇编后的二进制文件,内容上和对象文件相似,可以看成是一组目标文件(.o / .obj文件)的压缩打包后的集合。

同上述对象文件连接过程一样,静态链接库对函数库的链接是在编译期完成的。编译完成后,生成的程序便于静态链接库没有关系,因此移植方便。

Linux静态链接库

Linux静态链接库的文件命名必须是lib[libname].a,Linux中创建一个静态链接库过程如下:

  1. 将编译代码文件编译成对象文件:g++ -c matrix.cpp -o matrix.o
  2. 用ar工具打包成静态链接库matrix.aar -crv libmatrix.a matrix.o

使用静态链接库编译,需要保证libmatrix.amatrix.h头文件在当前目录下:

g++ main.cpp -L . -lmatrix -o main

生成的main可执行文件已经打包了libmatrix.a数据和程序,可以被移植后单独运行。

Windows静态链接库

Windows下基于VS的开发模式下,要创建一个静态链接库只需要新建一个Static Library项目,然后在头文件#include "stdafx.h",他用于生成一个预编译头文件project.pch和预编译类型文件stdafx.obj。在编译完成后,就会生成对应的*.lib文件了。

使用静态链接库的时候,在头部加上:#pragma comment(lib,"matrix.lib")

动态链接库

在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件和一个DLL (.dll) 文件。虽然引入库的后缀名也是“lib”,但是,动态库的引入库文件和静态库文件有着本质上的区别,对一个DLL来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。这时,在发布产品时,除了发布可执行文件以外,同时还要发布该程序将要调用的动态链接库。

Windows API中所有的函数都包含在DLL中,其中有3个最重要的DLL:

  • Kernel32.dll:它包含那些用于管理内存、进程和线程的函数,例如CreateThread函数;
  • User32.dll:它包含那些用于执行用户界面任务(如窗口的创建和消息的传送)的函数,例如 CreateWindow 函数;
  • GDI32.dll:它包含那些用于画图和显示文本的函数。

使用动态链接库的优点:

  1. 可以用多种程序编写动态链接库:

动态链接库文件是系统相关的,可以用熟悉的语言开发动态链接库,然后在不同的语言开发的应用程序种调用它。于是可以充分发挥各种语言的特性,针对特定模块功能选用不同的语言。

  1. 节约磁盘和内存空间

如果多个程序需要使用同样的功能,可以将共同的功能模块以动态链接库的形式提供,所以同样的程序在磁盘上只需要存在一份,节约了磁盘空间。另外,如果多个进程使用同一份动态连接库,操作系统将保证该份动态链接库文件只需要放入内存一次,他的内存空间可以被多个应用程序共享,也节约了内存空间。

  1. 实现资源共享

动态链接库可以包含对话框模板、字符串、图标和位图等多种资源,多个应用程序可以使用动态链接库来共享这些资源。在实际工作中,可以编写一个纯资源的动态链接库,供其他应用程序访问。因此动态链接库也被称为共享库

  1. 有助于实现应用程序的本地化

如果产品需要提供多语言版本,那么就可以使用DLL来支持多语言。可以为每种语言创建一个只支持这种语言的动态链接库。

  1. 提供二次开发的平台
    在销售产品的同时,可以采用DLL的形式提供一个二次开发的平台,让用户可以利用该DLL调用其中实现的功能,编写符合自己业务需要的产品,从而实现二次开发。

  2. 增强产品的功能

在发布产品时,可以发布产品功能实现的动态链接库规范,让其他公司或个人遵照这个规范开发自己的DLL,以取代产品原有的DLL,让产品调用新的DLL,从而实现功能 的增强。在实际工作中,我们看到许多产品都提供了界面插件功能,允许用户动态地更换程序的界面,这就可以通过更换界面DLL来实现。

  1. 简化项目管理

在一个大型项目开发中,通常都是由多个项目小组同时开发,如果采用串行开发,则效率是非常低的。我们可以将项目细分,将不同功能交由各项目小组以多个DLL的方式实现,这样,各个项目小组就可以同时进行开发了。

  1. 方便程序更新

如果项目依赖的静态链接库更新了,则主程序需要整个重新更新。

  1. 支持手动载入程序

一般的静态库加载代码段都是操作系统自行完成的,无法人为控制。如果一些项目特别大,就需要有选择的载入一些库,而动态链接库允许开发者在运行时显示的加载,调用和释放某一部分代码。

Windows动态链接库

Windows下可以用VS创建一个Dynamic Link Library项目来构建一个动态链接库。在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字,表明函数将输出为动态链接库。编译后生成*.lib*.dll等文件。

在使用动态库时,往往提供两个文件:一个导入库(*.lib,非必须) 和一个(*.dll)文件。

导入库和静态库本质上的区别:静态库本身就包含了实际执行代码和地址符号表等数据。而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。

在调用动态链接库文件*.dll的时候,需要引入库的头文件(.h),导入库文件(.lib),并加上#pragma comment(lib,"matrix.lib"),还需要把*.dll文件放在可执行文件的生成目录。

Linux动态链接库

Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便:
g++ -shared -fPIC main.cpp -o libmatrix.so通过指定-shared参数,编译matrix.cpp生成libmatrix.so动态链接库文件。其中-fPIC参数是为了生成启示地址无关的动态库。详细原理见这篇文章:关于 gcc/g++编译选项: -fPIC 功能的解释

动态链接库的隐式调用和显式调用

动态链接库调用分为隐式调用和显式调用。隐式调用是运行时执行到某个函数,由操作系统去寻找并链接库;显式调用是开发者自己声明库文件的位置和函数名,手动加载代码段,控制代码执行和代码块的释放。

由于对Windows平台C++的开发没有经验,Windows相关的DLL使用之后再整理

隐式调用(Linux)

我们先就开篇的背景问题,用动态链接库写个完整的例子:

g++ -shared -fPIC -o libmatrix.so matrix.cpp 通过指定-shared参数,编译matrix.cpp生成libmatrix.so动态链接库文件,接着用matrix g++ -o main main.cpp -L . -lmatrix生成可执行文件main,其中-L .指定动态链接库目录为当前目录,-lmatrix指明需要动态链接的库,编译器会自动去寻找libmatrix.so

编译链接成功后,但执行可执行程序main报错:

./main: error while loading shared libraries: libmatrix.so: cannot open shared object file: No such file or directory

提示链接器找不到libmatrix.so,用ldd main检查main种动态链接库的完整性:
$ ldd main
linux-vdso.so.1 (0x00007fffd9366000)
libmatrix.so => not found
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f611ccc0000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f611cca0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f611caa0000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f611c951000)
/lib64/ld-linux-x86-64.so.2 (0x00007f611ceca000)

果然是运行时找不到libmatrix.so,这里有几种方式解决动态链接库的路径问题:

  • 设置LD_LIBRARY_PATH环境变量:export LD_LIBRARY_PATH=$(pwd)
  • /etc/ld.so.conf中添加一行路径,然后执行sudo ldconfig
  • libmatrix.so拷贝到默认动态链接库目录/usr/local/lib

修复完路径后,再次执行main就可以正常运行了。上述这样的一个过程就是隐式调用,隐式调用的优点是方便,实现简单。缺点是灵活性不足,如果一个程序模块特别多,启动时库全部加载,会严重拖慢启动时间。而且一些方法之后很少时间会用到,他们大部分时间可以被主动释放掉,从而减少内存占用。

显式调用(linux)

详见:linux下动态链接库(.so)的显式调用和隐式调用

参考资料

Disqus评论区没有正常加载,请使用科学上网