author
type
status
date
slug
summary
tags
category
icon
password
7.1 为什么要动态链接
1 静态链接的缺点
1.1 内存和磁盘空间占用
每个程序都将包含的库函数直接打包使用,使得占用内存和磁盘大。如下图1所示,其中Program1和Program2分别包含Program1.o和Program2.o两程序开发和发个模块,并且它们还共用Lib.o两个模块。静态链接的做法是,将它们存两份副本,所以造成资源浪费。
图1 静态链接时文件在内存中的副本
1.2 程序开发和发布
一个工程项目包含多个模块,在静态链接的做法下,如果有一个模块需要更新,那么就需要将所有的模块重新获取一次才能运行。因为,一旦程序任何一个位置的一个小的改动,都会导致整个程序需要重新下载。
2 动态链接
2.1 思想
将程序的模块互相分割开,不让它们静态地链接在一起;即不对那些组成程序的目标文件进行链接,等到程序运行时再链接。有如Program1和Program2两个程序,并假定保留了Program1.o、Program2.o和Lib.o三个文件。当我们需要运行Program1这个程序时,系统首先加载Program1.o,而系统发现Program1.o中用到了Lib.o,即Program1.o依赖于Lib.o,此时系统加载Lib.o,并将其加载至内存。所有需要的目标文件加载完毕后,若依赖关系满足,即所有的依赖文件都在磁盘中,此时系统开始进行链接工作(与静态链接类似,符号解析、地址重定位等);完成这些工作,系统将控制权交给Program1.o的程序入口,程序开始运行;若此时我们想运行Program2.o,则不需要再重新加载Lib.o——因为前面在内存中已经存有一份Lib,o的副本,如图2所示,系统只需要将Program2.o与Lib.o链接起来即可。
图2 动态链接时文件在内存中的副本
2.2 动态链接基本实现
静态链接是将所有的程序模块都连接成一个单独的可执行文件;动态链接则是将程序拆分成各个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序。动态链接涉及运行时的链接和多个文件的装载,动态链接下进程的虚拟地址空间分布复杂,另外,涉及到的存储管理、内存共享、进程线程等机制在动态链接下也有不同。在Linux中,ELF动态链接文件被称为动态共享对象,简称共享对象,是以.so为扩展名的文件;而在Windows中,动态链接文件则被称为动态链接库,即.dll扩展名的文件。
程序被装载时,系统的动态链接器会将程序所需的所有动态链接库(最基本的是libc.so)装载到进程的地址空间,并将程序中所有未决议的符号绑定到相应动态链接库中,并进行重定位工作。
程序与libc.so之间的链接由动态链接器完成,而非静态链接器ld。即动态链接将连接过程由本来的程序装载前推迟到装载时——程序每次装载时进行链接,这样会不会很慢;是的,所以后续对动态链接的连接过程进行优化,如后将介绍的延迟绑定(Lazy Binding)。
7.2 简单的动态链接例子
1 两个程序调用一个函数
代码如下所示,Program1.c与Program2.c分别调用了Lib.c的foobar()函数,传入一个数字,foobar()函数的作用就是打印这个数字。
程序清单1 SimpleDynamicalLinking
使用gcc对Lib.c进行编译,得到一个共享对象文件:
此时得到Lib.so文件,是一个包含了Lib.c的foobar()函数的共享对象文件,然后分别编译链接Program1.c和Program2.c:
这样得到两个程序Program1和Program2,它们都用到了Lib.so里的foobar()函数。从Program1的角度来看,编译和连接过程如图3所示:1、Lib.c被编译成Lib.so共享对象文件,Program1.c被编译成Program.o后,链接成为可执行程序Program1;2、其中Program1.o被链接成可执行文件的过程中,在静态链接的情况下,会把Program1.o和Lib.o链接到一起,并产生输出可执行文件Program1;3、但是在动态链接中,Lib.o没有被链接进来,链接的输入目标文件只有Program1.o(还有C语言库),并且这里Lib.so也参与了链接过程——这是为何?
图3 动态链接过程
在动态链接中,Program1.c被编译成Program1.o时,编译器不知道foobar()函数的地址。当连接器将Program1.o链接成可执行文件时,此时必须确定Program1.o中所引用的foobar()函数的性质——若foobar()是一个定义于其他静态目标模块中的函数,那么连接器将会按照静态链接的规则,将Program1.o的foobar地址引用重定位;若foobar()是一个定义在动态共享对象中的函数,那么链接器会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,并将此过程留到装载时进行。
那么,链接器如何判断foobar的引用是静态符号还是动态符号?——Lib.so保留了完整的符号信息(运行时使用动态链接还需使用符号信息),将Lib.so也作为链接的输入文件之一。
2 动态链接程序运行时地址空间分布
相对于静态链接的可执行文件只需要对可执行文件进行映射而言,动态链接还需要对其所依赖的共享目标文件的地址空间分布进行分析,如图4所示:
图4 共享目标文件的地址空间分布
其中,包含了Lib.so、Program1和ld-2.6.so(Linux下的动态链接器)。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行Program1之前,首先将控制权交给动态链接器,由它完成所有动态链接工作后,再将控制权交给Program1,之后开始执行。
通过readelf工具查看Lib.so的装载属性,如图5所示。发现,动态链接模块装在地址是从地址0x00000000开始的,我们知道此地址是无效的,并且Lib.so的最终装在地址并不是0x00000000,而是0xb7efc000。由此猜想,共享对象的最终装在地址在编译时是不确定的,而是在装载时,装载器根据当前地址的空闲情况,动态分配虚拟地址空间给相应的共享对象。——(为什么不将每个共享对象在进程中的地址固定?)
图5 Lib.so装载属性
7.3 地址无关代码
1 固定装载地址的问题
——"共享对象在被装载时,如何确定它在进程虚拟地址空间中的位置?"
动态链接首先会遇到共享对象对象地址的冲突问题。而程序模块的指令和数据中会包含一些绝对地址的引用,在链接产生输出文件时,需要假设模块被装载的目标地址。在动态链接的情况下,若不同模块的目标装载地址都一样是不行的。而对于单个程序来说,手工指定各个模块的地址也是不行的,因为多个模块的程序下,容易导致地址冲突。
为解决模块装载地址固定的问题,设想可以让共享对象在任意地址加载——即共享对象在编译时,不能假设自己在进程虚拟地址空间中的位置。
2 装载时重定位——动态链接重定位方案一
要使得共享对象能够在任意地址装载,首先想到静态链接的重定位。其思路就是,在链接时,对所有的绝对地址的引用不做重定位,而将重定位推迟到装载时完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。示例如图6:
图6 装载时重定位示例
之前的做法下,系统根据当下内存的空闲情况,动态分配内存给程序,所以程序被装载的地址是不确定的。那么,系统在装载程序时,需要对程序的质量和数据中对地址的引用进行重定位(由于整个程序一起加载,所以程序中质量和数据的相对位置是不会改变的)。——先确定装载的目标地址,再根据相对地址进行重定位。
装载时重定位的方法并不适用于解决共享对象中所存在的问题。动态链接模块被装载映射至虚拟空间后,指令部分在多个进程之间共享,由于装载时重定位方法需要修改指令,因此无法实现同一份指令被多个进程共享,因为指令被重定位后对于每个进程来讲是不同的;数据则可以又多个备份,可以实现多个进程的共享。
3 地址无关代码——动态链接重定位方案二
3.1 重定位的缺陷以及地址无关代码思路
上述的装载时重定位的方式存在缺点——指令无法在多个进程间共享,这样就失去了动态链接节省内存的一大优势。所以希望有更好的方法解决共享对象指令中对绝对地址重定位的问题,暨希望共享指令部分在装载时不因装载地址的改变而发生变化,那么思路就是,将指令中需要被修改的部分分离出来跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。——地址无关代码(PIC, Position-independent Code)技术。
共享对象模块中的地址引用,按是否跨模块分为:模块内部引用、模块外部引用,按不同的引用方式分为:指令引用、数据访问。四种情况如图7所示:
图7 四种寻址模式
3.2 不同类型的地址无关代码
类型一 模块内部调用或跳转
模块内部的调用者与被调函数都处于同一个模块,所以相对位置固定,因此可以使用相对地址调用,或基于寄存器的相对调用,而无需重定位。(指令地址无关,暨无论模块被加载到哪个位置,这条指令都是有效的)——类型一看似容易解决,但是存在共享对象全局符号介入(Global System Interposition)问题。
类型二 模块内部数据访问
指令中不能直接包含数据的绝对地址,所以只能使用相对寻址。一般来说,一个模块前面是若干个页的代码,后面紧跟着若干个页的数据,这些页之间相对位置固定,暨任意一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么相对于当前指令加上偏移量就可以访问模块内的数据了。现代的体系结构中,数据的相对寻址往往没有相对于当前指令地址(PC)的寻址方式,所以ELF使用了一个方法得到当前PC值,然后加上一个偏移量就可以达到访问相应变量的目的。
类型三 模块间数据访问
要做到代码地址无关,基本思想就是把跟地址相关的部分放到数据段里面,而其他模块的全局变量的地址和模块装载地址有关。ELF做法为,在数据段建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table, GOT),当代吗需要引用该全局变量时,可通过GOT中相对应的项间接引用。如图8所示,当指令要访问数据b,程序先找到GOT,然后根据GOT中变量所对应的项找到对应变量目标地址;链接器在装载模块的时候会查找每个变量的地址,然后填充GOT,以确保每个指针所指向的地址正确。由于GOT本身放在数据段,所以它可以在模块装载时被修改,并且每个进程可以有独立的副本,互不影响。
图8 模块间数据访问
GOT如何做到指令的地址无关性?模块在编译时可确定模块内部变量与当前指令的偏移,同理我们可以在编译时确定GOT相对于当前指令的偏移。确定GOT的位置跟上面的访问变量a的方法基本一样,通过得到的PC值然后加上偏移量,就可以得到GOT的位置,然后根据变量地址在GOT中的偏移就可以得到变量的地址。
类型四 模块间调用、跳转
可采用类型三的方法进行,但有所不同的是,GOT中相应项保存的是目标函数的地址,当模块需要调用目标函数时,可以通过GOT中的项进行间接跳转,基本原理如图9所示,调用ext()函数的方法与上面调用数据b的方法类似,先得到当前指令地址PC,然后加上一个偏移得到函数地址在GOT的偏移,然后进行间接调用:
图9 模块间调用、跳转
地址无关代码引用方式小结如表1所示:
表1 各种地址引用方式
3.3 共享模块的全局变量问题
模块内部的静态全局变量可以直接使用相对寻址进行访问,而模块内部的非静态全局变量却不能,因为编译器无法确定对全局变量的引用是不同模块间的还是模块内部的。所以无论是模块内部还是外部的全局变量,都只能以GOT的方式来访问。可执行文件在生成代码的过程中,在链接时就会确定地址,这时链接器将在.bss段创建一个该全局变量的副本,在之后的动态链接过程中,其他模块的GOT都会指向该副本,不会导致地址上的冲突。
3.4 数据段地址无关性
前文我们讨论了指令的代码地址无关性,其实数据段也存在绝对地址的问题,如下代码所示,指针p的地址就是一个绝对地址,它指向变量a,而变量a的地址会随着共享对象的装载地址改变而变化,那么就可以使用重定位表的方式解决这个问题,重定位表包含a的重定位信息,在进行动态装载时会进行重定位。
3.5 装载时重定位与地址无关代码对比
装载时重定位:优点——运行速度快;缺点——对象不能被共享,浪费内存;地址无关代码:优点——可进行对象共享,节省内存;缺点——多出计算当前地址以及间接寻址的工作,运行速度慢;
4 延迟绑定
4.1 延迟绑定思想
影响动态链接性能的两大因素:1、复杂的GOT定位;2、动态链接的链接工作在运行时完成(即开始开始执行时,动态链接器都会进行一次链接工作,寻找并装载所需要的共享对象,然后进行符号查找地址重定位等工作)。
在一个程序中,可能有很多的函数,例如错误处理、或者一些用户极少用到的功能模块,在程序运行完都并不会使用到,如果在程序运行时都要将这些函数进行链接,那么就是对性能的浪费,ELF延迟绑定(Lazy Binding)的想法就出现了,其做法是当函数第一次使用时才进行绑定(符号查找、重定位等操作),若没有用到则不进行绑定。
4.2 延迟绑定做法
在ELF使用PLT(Procedure Linkage Table)的方法,具体表现为,在ELF文件中包含.plt的段;而具体做法就是在指令与GOT层之间加入一层对跳转的控制,即PLT层;其实现原理是,在第一次调用共享对象模块函数时,调用一个“绑定函数”完成地址绑定过程(对GOT表中的项进行填充),每个外部函数在PLT中都有一个对应的项。动态链接的绑定过程具体调用的函数是_dl_runtime_resolve(),功能相当于lookup(module, function)——(动态链接器中的某个函数,用于完成地址绑定工作),它需要知道以下信息:1、module:地址绑定发生在哪个模块;2、function:地址绑定发生在哪个函数。
4.2.1 PLT基本原理
示例如下:
liba.c调用libc.so中的bar()函数,具体表现如下:
对于上述代码,各个指令的含义如下:
1、第1行指令,jmp *(bar@GOT) :通过GOT间接跳转的指令。其中,bar@GOT表示GOT中保存bar()这个函数相应的项,如果链接器在初始化阶段已经初始化该项,并将bar()的地址填入该项,那么这个跳转指令直接跳转至bar(),实现函数的正确调用;但是为了实现延迟绑定,链接器在初始阶段没有将bar()的地址填入到该项,而是将第2条指令“push n”的地址填入bar@GOT中;所以第1条指令的效果是跳转到第2条指令。
2、第2行指令,push n:讲一个数字n压入堆栈中,这个数字是bar这个符号引用在重定位表“.rel.plt”中的下标。
3、第3条指令,push moduleID:将模块的ID压入堆栈。
4、第4条指令,jump _dl_runtime_resolve:跳转到指令_dl_runtime_resolve。其实这个指令就是相当于lookup(module, function)函数的调用——先将所需要决议符号的下标压入堆栈,再将模块ID压入堆栈,然后调用动态链接器的_dl_runtime_resolve()函数来完成符号解析和重定位工作;_dl_runtime_resolve在进行一系列的工作以后将bar()的真正地址填入bar@GOT中。
一旦bar()函数解析完成,当再次调用bar@plt时,第一条jmp指令就跳转到真正的bar()函数,而bar()函数返回的时候会根据堆栈里面保存的EIP直接返回到调用者,而不会继续执行bar@plt中第二条指令开始的那段代码(那段代码只会在符号未被解析时执行一次)。
4.2.2 PLT实现方式
ELF将GOT分成了两个表——“.got”和“.got.plt”,“.got”用来保存全局变量引用的地址,“.got.plt”用来保存函数引用的地址。“.got.plt”前三项内容及含义如下:
1、第1项,“.dynamic”段的地址:它描述了本模块动态链接相关信息;
2、第2项,本模块的ID。
3、第3项,_dl_runtime_resolve()的地址。
动态链接在装载共享模块的时候会将第二项和第三项初始化,“.got.plt”的其余项分别对应每个外部函数的引用。为减少代码重复,ELF把上面例子中最后两条指令放到PLT的第一项,代码如下:
实际的PLT基本结构如图10所示:
图10 GOT中的PLT数据结构
5 动态链接相关结构
在动态链接情况下,可执行文件的装载与静态链接情况基本一样。首先,操作系统读取可执行文件头部,检查文件的合法性;然后,从头部中的“Program Header”中读取每个“Segment”的虚拟地址、文件地址和属性,并将它们映射到进程虚拟空间的相应位置——这些都与静态链接的装载相同。而在此之后,静态链接的情况下,就会直接将控制权转交给可执行文件的入口地址,然后程序开始执行。
但是在动态链接情况下,由于可执行文件依赖于很多共享对象,此时的可执行文件中对于很多外部符号的引用还处于无效地址的状态,即没有和相应的共享对象中的实际位置链接起来。所以在映射完可执行文件之后,操作系统会先启动一个动态链接器(Dynamic Linker)。
动态链接器ld.so是一个共享对象,操作系统以映射的方式将它加载到进程的地址空间;在加载完动态链接器之后,操作系统就将控制权交给动态链接器的入口地址(与可执行文件一样,共享对象也有入口地址)。当动态链接工作完成之后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。
5.1 ”.interp“段
动态链接器的位置——在动态链接的ELF可执行文件中,有一个段叫做”.interp“段,其中,使用objdump工具进行查看,”.interp“内容如下图11所示,它的内容是一个字符串,含义就是可执行文件所需要的动态链接器的路径。
图11 “.interp”的内容
5.2 “.dynamic”段
要了解动态链接器如何完成链接过程,很有必要了解ELF文件中和动态链接相关的结构。其中的“.dynamic”段是动态链接ELF中最重要的结构,这个段保存了动态链接器所需要的基本信息,如:依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。它是一个结构体数组,定义在elf.h中,具体如代码所示。
其中的d_tag代表了DT_SYMTAB类型,而d_un的值表示了动态链接符号表的地址。“.dynamic”段中保存的信息类似于ELF文件头,所以“.dynamic”段可以看成是动态链接下ELF文件的“文件头”。使用readelf工具指令$ readelf -d Lib.so可查看“.dynamic”段的内容。另外,Linux还提供了一个命令查看一个程序主模块或一个共享库依赖于哪些共享库,如图12所示。
图12 查看共享库的依赖关系
5.3 动态符号表
动态链接中,若模块A引用了模块B的函数fun,那么称fun为A的导入函数(Import Function)、B的导出函数(Export Function),将这种导入导出关系放到静态链接的情形下,就可以视作普通的函数定义和引用。
ELF中的动态符号表(Dynamic Symbol Table)段保存了动态链接中这些模块的导入导出关系的信息,其段名为“.dynsym”(Dynamic Symbol),它只保留了于动态链接相关的符号。由于在动态链接下,需要在程序运行时查找符号,为了加快符号查找,一般还会有辅助的符号哈希表(“.hash”)。
我们可以使用readelf工具指令$ readelf -sD Lib.so查看ELF文件的动态符号表及其哈希表。另外,动态符号表结构于静态链接的符号表几乎一样,所以可以简单地将导入函数看作是对其他目标文件中函数的引用,而将导出函数看作是在本目标文件定义的函数。
5.4 动态链接重定位表
由于导入符号(可执行文件或共享对象依赖于其他共享对象)的存在,它的代码或数据中就会有对于导入符号的引用,而在编译时这些导入符号的地址是未知的。在静态链接中,这些未知的地址引用在最终链接时被修正;但是在动态链接中,导入符号的地址在运行时才确定,所以在运行时需要对这些导入符号的引用进行修正,即需要对共享对象进行重定位。
如果一个共享对象不是以PIC模式编译的,那么它在装载时需要被重定位;而若一个共享对象是以PIC模式编译的,它仍需进行重定位。对于使用PIC技术的可执行文件或共享对象来说,它们的代码段不需要重定位(因为地址无关代码),但是数据段还包含了绝对地址的引用——因为代码段中绝对地址相关的部分,被分离了出来,变成了GOT,而GOT实际上是数据段的一部分。除GOT外,数据段还可能包含绝对地址引用。
动态链接重定位相关结构
共享对象的重定位类似于“静态链接”中目标文件的重定位,区别在于,目标文件的重定位在静态链接时完成,而共享对象重定位在装载时完成。在静态链接中,目标文件中包含专门用于表示重定位信息的重定位表,如“.rel.text”表示代码段的重定位表,“.rel.data”是数据段的重定位表。
动态链接的文件中的“rel.dyn”和“.rel.plt”分别类似于“.rel.text”和“.rel.data”,其中“rel.dyn”是对数据引用的修正,它所修正的位置位于“.got”以及数据段;而“rel.plt”是对函数引用的修正,它所修正的位置位于“.got.plt”。可以使用readelf工具指令$ readelf -r Lib.so查看动态链接文件的重定位表。
5.5 动态链接时进程堆栈初始化信息
在操作系统将控制权交给动态链接器后,它开始进行链接工作,那么它至少需要知道关于可执行文件和本进程的一些信息,有如可执行文件有几个段(“Segment”)、每个段的属性、程序的入口地址(因为动态链接器后续需要将控制权交给可执行文件)等,堆栈中保存了动态链接器所需要的一些辅助信息数组(Auxiliary Vector),它的格式是一个结构数组,被定义在“elf.h”中。事实上,这个数组结构位于环境变量指针的后面。
6 动态链接的步骤及实现
动态链接3大步骤:启动动态链接器本身 → 装载所有需要的共享对象 → 重定位和初始化。
6.1 动态链接器自举
普通共享对象文件的重定位工作由动态链接器来完成,而动态链接本身是一个共享对象,那么动态链接器又是由谁来进行重定位呢?这是一个“鸡生蛋,蛋生鸡”的问题,为了解决这种循环似的依赖问题,动态链接器必须有一定的特殊性,其特殊性包含以下两点:1、动态链接器本身不可以依赖于其他任何共享对象;2、动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。
以上两点条件的解决方式如下所示,这种含限制条件的启动代码也被称为自举(Bootstrap)。
1、动态链接器的编写,不使能用任何系统库、运行库;
2、动态链接器的编写,不能使用全局变量和静态变量。
动态链接器的入口地址就是自举代码的入口,当OS将进程控制权交给动态链接器时,动态链接器的自举代码即开始执行。首先,自举代码会找到它自己的GOT,GOT的第一个入口保存的就是“.dynamic”段的偏移地址,由此找到动态链接器本身的“.dynamic”段。通过“.dynamic”,自举代码可获得动态链接本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位。从这一步开始,动态链接器才可以使用自己的全局变量和静态变量。
在动态链接器的自举代码中,除不可使用全局变量和静态变量外,还不能调用函数,即动态链接器本身的函数也不能调用。原因就在于,其实PIC模式编译的共享对象,对于模块内部的函数调用也是采用和模块外部函数调用一样的GOT/PLT方式。所以在GOT/PLT没有被重定位之前,自举代码不可使用全局变量,也不可调用函数。
6.2 装载共享对象
完成基本自举后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表中,称之为全局符号表(Global Symbol Table),将共享对象的依赖关系看做是一个图数据结构,那么链接器一般使用深度优先或广度优先或其他顺序来遍历整个图,通常使用广度优先算法。当一个新的共享对象被装载进来,它的符号表会被合并到全局符号表中,所以当所有的共享对象都被装载进来时,全局符号表里面,将包含进程中所有的动态链接所需的符号。
另外,还会出现符号命名重复的问题,对于一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象又被称为共享对象全局符号介入(Global Symbol Interpose),在Linux下它遵从以下规则——当一个符号需要被加入全局符号表时,若相同的符号名已经存在,则后被加入的符号忽略。
6.3 重定位和初始化
在这个阶段下,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT的每个需要重定位的位置进行修正。
重定位完成后,若共享对象有“.init”段,那么动态链接器将会执行“.init”段中的代码,以实现共享对象特有的初始化过程;而若进程的可执行文件也有“.init”段,那么动态链接不会执行它。
6.4 Linux动态链接器实现
动态链接器是共享对象,同时也是一个可执行文件。以下几个问题值得注意:
1、动态链接器本身是静态链接的,它不能依赖于其他共享对象;
2、动态链接器往往使用PIC的模式,方便代码段的共享;
3、动态链接器ld.so的装在地址与一般的共享对象没有区别,一般是0x00000000,是一个无效地址,内核在装载时会为它选择一个合适的装载地址。
7 显式运行时链接
以上介绍的是动态链接情况下动态链接器的自动装载方法,还有一种更加灵活的手动装载方式——运行时加载,就是让程序自己在运行时控制加载指定的模块,并可在不需要该模块时将其卸载。具体就是在代码中显式调用相关API来加载共享对象——动态装载库(Dynamic Loading Library)。
程序可以通过以下4个由动态链接器提供的API对动态库进行操作:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)和关闭动态库(dlclose),它们