动态链接(Dynamic Link)

摘要:动态链接(Dynamic Linking)是一种在程序构建和运行过程中的链接方式。在编译系统中,它与静态链接相对应。静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法,例如在Windows下以.lib为后缀(在Linux下以.a为后缀)的静态链接库会

一、动态链接的定义

动态链接(Dynamic Linking)是一种在程序构建和运行过程中的链接方式。在编译系统中,它与静态链接相对应。静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法,例如在Windows下以.lib为后缀(在Linux下以.a为后缀)的静态链接库会被直接整合进可执行文件。而动态链接则把链接这个过程推迟到了运行时再进行。在可执行文件装载时或运行时,由操作系统的装载程序加载库,这里的库指的是动态链接库,在Windows下以.dll为后缀,Linux下以.so为后缀。需要注意的是,在Windows下的动态链接也可以用到.lib为后缀的文件,但这里的.lib文件叫做导入库,是由.dll文件生成的。

从程序开发和运行的整体流程来看,源程序在经过编译生成目标代码后需要进行链接才能生成可执行文件。在动态链接过程中,编译系统在链接阶段并不把目标文件和函数库文件直接链接在一起,而是等到程序在运行过程中需要的时候才进行链接操作。这就好比一个复杂的机器,各个零部件(函数库等)在机器开始运转(程序运行)时才被组装(链接)到一起,而不是在制造(编译)的时候就固定死。例如,一个大型的软件系统可能包含众多功能模块,这些功能模块对应的函数库如果采用动态链接的方式,就可以在程序运行时根据具体需求动态加载,而不是在程序编译时就将所有可能用到的函数库都整合进可执行文件中,这样可以使程序更加灵活和高效地利用系统资源。

二、动态链接的工作原理

(一)符号重定位与编译单元

c/c++程序的编译是以文件为单位进行的,每个c/cpp文件被称为一个编译单元(translation unit)。源文件先是被编译成一个个目标文件,再由链接器把这些目标文件组合成一个可执行文件或库。链接的过程,其核心工作是解决模块间各种符号(变量,函数)相互引用的问题,对符号的引用本质是对其在内存中具体地址的引用,这就是所谓的符号重定位。在编译以源文件为单位进行时,编译器没有全局视野,对于编译单元内的符号无力确定其最终地址。对于可执行文件来说,在现代操作系统上,程序加载运行的地址是固定或可以预期的,因此在链接时,链接器可以直接计算分配该文件内各种段的绝对或相对地址,符号重定位在链接时完成(如果可执行文件引用了动态库里的函数,则情况稍有不同)。但对于动态链接库来说,因为动态库的加载是在运行时,且加载的地址不固定,因此没法事先确定该模块的起始地址,所以对动态库的符号重定位,只能推迟。

(二)不同类型的地址引用与处理

模块内部引用

对于模块内部的函数调用、调转等(第一种类型),由于被调函数与调用函数都在函数内部,在编译链接时它们的相对位置是确定的。这是相对地址调用,或者是基于寄存器的相对调用,所以这种调用是不需要重定位的。例如在汇编层面,对于32位来说,call指令地址中第一个字节是指令的地址码,后边的四个字节是目的地址相对于当前指令的下一条指令的偏移。但是在获取当前指令的地址时,在汇编层面会调用一个_i686.get_pc_thunk.cx 的函数,作用是把返回地址的值放在ecx寄存器(就是把call的下一条指令地址放到ecx寄存器中),通过这种方法可以确定模块内部的变量以及函数的入口。

模块外部引用

模块间的数据访问(第三种类型):这种情况要更加复杂一些。因为是在多个模块中调用函数、变量,所以需要地址无关的属性。ELF在数据段里面建立一个指向这些变量的指针数组(*),也被称之为全局偏移表(Global Offset Table,GOT),当代码需要引用全局变量的时候通过GOT中对应的项间接引用。每个变量对应一个地址(4字节),至于对应的顺序是由编译器决定的。模块间的函数调转与调用(第四种类型):和模块间的数据访问类似,GOT段同样保存着各个函数的地址。同样运用另一种方法首先找到PC地址,然后加上一个偏移得到函数地址在GOT中的偏移,然后得到一个间接的调用。不过,“ - fPIC”产生的代码要大,而“ - fpic”产生的代码相对较小,并且较快。但是fPIC在某些硬件平台上会有一些限制,比如全局符号的数量或者代码长度等,而“ - fPIC”则没有这种限制,所以为了方便都使用“ - fPIC”参数来产生地址无关代码。如果有输出那就不是PIC的,否则就是PIC的。

(三)动态链接库的加载与函数调用

以Windows系统为例,对于常规的函数库,链接器从中拷贝它需要的所有库函数,并把确切的函数地址传送给调用这些函数的程序。而对于动态链接库(DLLs),函数储存在一个独立的动态链接库文件中。在创建Windows程序时,链接过程并不把DLLs文件链接到程序上。直到程序运行并调用一个DLLs中的函数时,该程序才要求这个函数的地址。此时Windows才在DLLs中寻找被调用函数,并把它的地址传送给调用程序。并且,动态链接库的另一个方便之处是对动态链接库中函数的修改可以自动传播到所有调用它的程序中,而不必对程序作任何改动或处理。DLLs不仅提供了函数重用的机制,而且提供了数据共享的机制。任何应用程序都可以共享由装入内存的DLLs管理的内存资源块。只包含共享数据的DLLs称为资源文件,如Windows的字体文件等。

三、动态链接的应用场景

(一)代码共享与节省内存

多进程共享DLL

在多进程环境下,动态链接的一个重要应用场景就是多个进程可以共用一个DLL。这是非常节省内存的做法,因为如果每个进程都使用静态链接,将函数库的副本整合进自己的可执行文件,那么内存中就会存在大量相同函数库的副本,造成内存资源的浪费。例如,在一个操作系统中,有多个应用程序都需要使用某个图像处理函数库,如果采用动态链接,这些应用程序可以共享这个DLL文件,内存中只需要存在一份该DLL的副本就可以满足所有应用程序的需求,从而减少文件的交换,提高内存的利用率。

大型程序的模块化开发

在开发大型程序时,动态链接可以将程序按照模块拆分成各个相对独立部分。不同的模块可以分别开发、编译成动态链接库,然后在程序运行时才将它们链接在一起形成一个完整的程序。这样可以让开发团队中的不同成员或不同开发小组独立开发各个模块,提高开发效率。例如,一个大型的办公软件,文字处理、表格处理、图形绘制等功能模块可以分别开发成动态链接库,然后在软件运行时进行链接组装。

(二)便于软件维护与升级

局部代码修改与升级

只要函数的接口参数(输入和输出)不变,则修改函数及其DLL时,无需对可执行文件重新编译或链接。这对于软件的维护和升级非常方便。例如,一个软件中的某个功能模块存在漏洞需要修复或者需要进行功能升级,如果这个功能模块是采用动态链接的方式构建成一个DLL,那么开发人员只需要修改这个DLL文件,而不需要重新编译整个软件的可执行文件。这样可以大大缩短软件升级的周期,减少对用户的影响。

适应不同运行环境

调用不同的DLL,可以适应多种使用环境并提供不同的功能。例如,不同的显示卡只需厂商为其提供特定的DLL,而操作系统和应用程序则不必修改。在游戏开发中,对于不同的图形处理需求,可以通过加载不同的图形处理相关的DLL来适应不同的硬件环境,而不需要为每种硬件环境重新编译整个游戏程序。这使得软件具有更好的兼容性和可扩展性,能够在不同的硬件和软件平台上运行,只需要根据平台的特点加载相应的DLL即可。

(三)跨语言调用

由于动态链接库是与语言无关的,因此可以创建一个DLL,被C++、VB或任何支持动态链接库的语言调用。这在软件开发中非常有用,当一种语言存在不足时,可以通过访问另一种语言创建的DLL来弥补。例如,在一个项目中,部分功能用C++开发效率更高,而另一部分功能用Python开发更方便,那么可以将C++开发的功能封装成DLL,然后在Python程序中调用这个DLL,实现不同语言之间的协同工作,充分发挥不同编程语言的优势。

四、动态链接的优势和劣势

(一)优势

内存和磁盘空间利用高效

生成的可执行文件较静态链接生成的可执行文件小。因为动态链接库在多个应用程序之间可以共享,不需要像静态链接那样每个可执行文件都包含一份函数库的副本。例如,在一个操作系统中有多个应用程序都需要使用某个数学计算函数库,如果采用动态链接,只需要在磁盘上存储一份该函数库的动态链接库文件(.dll或.so),而每个应用程序的可执行文件体积也会因为不需要包含这个函数库而变小。同时,在内存中多个进程共享同一个DLL副本,节省了内存空间,减少了物理页面的换进换出,也可增加CPU缓存的命中率,因为不同进程间的数据和指令访问都集中在了同一个共享模块上。

开发灵活性高

适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。在大型项目中,不同的模块可以由不同的团队或个人独立开发成动态链接库,这些模块之间的接口可以通过定义好的函数接口来进行交互。这样的开发模式可以提高开发效率,降低开发过程中的耦合度,每个模块可以独立进行测试和优化,不会因为一个模块的修改而影响到整个项目的其他部分(只要接口不变)。例如,在一个大型的企业级应用开发中,数据库访问模块、业务逻辑模块、用户界面模块等可以分别开发成动态链接库,各自独立进行开发和测试,最后再进行集成。

便于软件维护和升级

只要函数的接口参数(输入和输出)不变,则修改函数及其DLL时,无需对可执行文件重新编译或链接。这对于软件的长期维护和升级非常有利。例如,在一个软件产品的生命周期内,如果发现某个功能模块存在性能问题或者需要添加新功能,只需要对相应的动态链接库进行修改,而不需要重新编译整个可执行文件。这可以大大减少软件升级的工作量和时间成本,提高软件的可维护性和可扩展性。同时,不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数,这也为软件的集成和扩展提供了更多的可能性。

增强软件兼容性

一个程序在不同平台运行时可以动态地链接到由操作系统提供的动态链接库。例如,一个Java程序在不同的操作系统(如Windows、Linux、Mac)上运行时,可以根据操作系统的特点动态链接到相应的本地动态链接库,以实现与操作系统的交互功能,如文件系统操作、图形界面显示等。这种方式可以提高软件的兼容性,使软件能够在不同的平台上运行,而不需要为每个平台单独开发一个版本的软件。

(二)劣势

程序启动和运行速度较慢

动态链接比静态链接慢。在程序启动时,如果采用载入时动态链接,程序需要在启动过程中加载所需的动态链接库,如果动态链接库不存在,系统将终止程序并给出错误信息;即使动态链接库存在,加载和链接动态库的过程也会增加程序启动的时间。在运行过程中,动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT(全局偏移表),然后再进行间接跳转,这些操作都会增加程序运行的开销。据估算,动态链接与静态链接相比,性能损失大约在5%以下,但在对性能要求极高的场景下,这一损失也可能会产生影响。

管理复杂度增加

由于程序由多个文件(可执行文件和动态链接库文件)组成,因此增加了管理复杂度。在软件的部署、分发和维护过程中,需要确保动态链接库的正确安装、版本匹配和路径设置等问题。例如,在一个企业内部使用的软件系统中,如果软件依赖多个动态链接库,在软件升级或者系统迁移时,需要同时考虑这些动态链接库的兼容性和可用性,确保它们能够正确地被可执行文件加载,否则可能会导致软件运行失败。

五、如何实现动态链接

(一)编译时保留链接信息(Load - time Dynamic Linking)

这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码。当程序执行时,利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中,其主要目的是便于代码共享。例如在C/C++语言中,在编译时可以通过一些特定的编译选项或者配置文件来指定要链接的动态链接库以及相关的函数信息,当程序运行时,操作系统根据这些预先保留的链接信息加载相应的动态链接库到内存中,并进行链接操作,使得程序能够正常调用动态链接库中的函数。

(二)运行时动态获取函数地址(Run - time Dynamic Linking)

这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,并用LoadLibrary和GetProcAddress(在Windows系统下)动态获得DLL函数的入口地址。以Windows平台为例,程序在运行时可以使用LoadLibrary函数加载动态链接库,然后使用GetProcAddress函数获取动态链接库中特定函数的地址,最后通过这个地址来调用函数。在Linux系统下,也有类似的函数和机制来实现运行时动态链接,例如dlopen、dlsym等函数。例如,一个插件式的软件架构中,软件在启动时并不知道用户会加载哪些插件(每个插件可以看作是一个动态链接库),当用户选择加载某个插件时,软件通过运行时动态链接的方式加载插件对应的动态链接库,并获取其中函数的地址,从而实现插件的功能调用。

在Java中,动态链接(Dynamic Linking)是指在运行时解析类和方法引用的过程。这个过程发生在类加载器的链接阶段,它包括验证、准备和解析三个子阶段。解析阶段是链接阶段的一部分,它的主要工作是在类被初始化(Initialization)之前,将符号引用转换成直接引用。在Java中实现动态链接的技术包括反射,通过Class.forName 或ClassLoader.loadClass 动态加载类,并通过反射API访问类的方法和字段。这也是Java实现动态链接的一种方式,使得Java应用程序在运行时可以根据需要动态加载和使用类,增强了Java应用程序的灵活性和适应性,尤其是在大型系统和微服务架构中,可以方便地进行模块化和动态扩展。

在一些编译环境中,如通过gcc编译命令指定 - l+库名来链接动态库。在makefile中将函数封装成动态库后,在其他程序中使用时,需要在程序内声明相关函数,同时在MakeFile中进行链接操作。还有一种方式是通过dlopen来使用动态库,该方法使用动态库不需要加入链接动态库 - ltest的指令,程序在运行的时候也会自动加载动态库,有点像fopen打开文件,当需要这个动态库的时候,用dlopen打开它就好了。在使用dlopen函数时,需要注意动态链接时、执行时搜索路径顺序:1.编译目标代码 - L时指定的动态库搜索路径;2.环境变量LD_LIBRARY_PATH指定的动态库搜索路径;3.配置文件/etc/ld.so.conf 中指定的动态库搜索路径;4.默认的动态库搜索路径/lib;5.默认的动态库搜索路径/usr/lib。

来源:小唐看科技

相关推荐