静态链接与动态链接
大多数高级语言都支持分别编译,程序员可以显式地把程序划分为独立的模块或文件,然后每个独立部分分别编译。在编译之后,由链接器把这些独立的片段(称为编译单元)“粘接到一起”。(想想这样做有什么好处?)
在C/C++中,这些独立的编译单元包括obj文件(一般的源程序编译而成)、lib文件(静态链接的函数库)、dll文件(动态链接的函数库)等。
静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件)。
动态链接方式:在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝。
静态链接库与动态链接库
先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终的EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。
采用动态链接库的优点:(1)更加节省内存;(2)DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。
认识动态链接库
动态链接是相对于静态链接而言的。所谓静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。换句话说,函数和过程的代码就在程序的exe文件中,该文件包含了运行时所需的全部代码。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源。而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。一般情况下,如果一个应用程序使用了动态链接库,Win32系统保证内存中只有DLL的一份复制品
动态链接库的两种链接方法:
(1) 装载时动态链接(Load-time Dynamic Linking):这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,调用函数的时候利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)
(2) 运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存),并标识内存地址,其他程序也可以使用该程序,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。(dll在内存中只存在一份,处在运行阶段)
上述的区别主要在于阶段不同,编译器是否知道进程要调用的dll函数。动态加载在编译时知道所调用的函数,而在运行态时则必须不知道。
Android中的动态链接
在Android中,当System.loadLibrary()
用Javadlopen()
执行或以本机代码执行时,将调用动态链接器。对于Java代码,Dalvik / Android运行时dlopen()
最终会调用动态链接器。
从Lollipop MR1(我们的讨论基于Marshmallow)开始,Android动态链接分为两个阶段:库加载和库重定位。如图3所示,左半部分正在加载,而右半部分正在链接。
在库加载过程中,动态链接程序将重建库依赖关系,并将其所有库加载到内存中。库重定位过程链接了依赖性。我们首先讨论Android动态链接器的重要数据结构。
数据结构
持久数据结构
Android的动态链接器在应用程序/程序的生命周期中具有两个持久的数据结构-LSPath(库搜索路径)和ALList(已加载的库列表)。
LSPath 是存储库的目录。动态链接器遍历这些路径以寻找库。这些路径对于库的定位至关重要,并按优先级排序。
ALList 是以 soinfo 为元素的列表,其用于保持装载的库(例如数据ELF和存储器布局)的元数据。动态链接器在不同的库加载和链接过程中从ALList获取数据。分别在加载和卸载库时,ALList会增长和缩短。
持久性数据结构位于图3的顶部。图3中的“存储”在某种程度上意味着LSPath。
临时数据结构
自然地,在加载库期间会使用许多临时数据结构。其中,最重要的两个是load_tasks和local_group。他们两个都表现出对库的依赖。
load_tasks是一个队列,其中包含要加载的库-库相关性的子库,这些库尚未加载到内存中。当链接程序开始搜索库并使刚解析的相关库入库时(从DT_NEEDED
ELF格式库的表中),* load_tasks*出队。
加载所有树后(此时load_tasks为空),便构造了local_group并将其用于重定位。local_group是soinfo的队列,并按BFS顺序表示库依赖关系。(将在“特殊功能”部分中讨论另一个称为global_group的类似数据结构。)
临时数据结构在上图的底部列出。
Library装载程序
在开始的时候,Library被操作系统请求-Root-添加到load_tasks,如上图所示。在库加载的程序,动态链接程序不断在load_tasks加载的所有Library 并更新它,如图的左半部分, 完成此过程后,将加载library dependency中的所有库。
Library定位
动态链接器从load_tasks中提取一个名称/路径,如果它是绝对路径,则直接打开,否则将遍历LSPath来寻找该库。
找到并打开该库后,它可能是系统库或应用程序库。系统库是从系统库路径如/system/lib
; 应用程序库是从应用程序库路径(如)加载的/data/data/com.example.app/lib
。
在从Zygote派生应用程序之前,动态链接程序仅在系统库路径下搜索库。在应用程序派生并设置了库路径之后,动态链接器首先在应用程序路径下搜索,然后在系统库路径下搜索。
Library加载
从存储中打开库之后,并且在将库加载到内存中之前,链接器要验证刚刚打开的文件是否为有效的共享库。它根据ELF数据执行检查:magic number,32/64位,小/大字节序,机器类型等。如果有任何错误,该库和库依赖项将被卸载。
如果验证通过,则动态链接器将读取库头并将所有可装入的段加载到内存中。它通过检查PT_LOAD
程序头表来计算所需的库内存大小。内存分配只是通过mmap
。(在Jelly Bean之前和之前,库内存由伙伴内存分配系统系统管理)
Library预链接
“预链接”旨在通过读取库的依赖关系(动态DT_NEEDED
部分)来建立一个更高级别的库依赖关系。DT_NEEDED
表中记录的所有库名称均添加到load_tasks中并进行加载。
可以很容易地看到,在加载库时,相同的库(名称)可能会多次添加到load_tasks中。动态链接器会在打开该库之前和之后遍历ALList,以检查该库是否已通过名称和索引节点(i-node)加载到内存中。如果找到,动态链接器将删除该load_tasks节点,然后进行下一步获取。因此,ALList中没有重复加载的库。
在Android的整个开发过程中,库的读取依赖项的发生时间已更改。在Lollipop-MR1之前,库链接是DFS,它以递归方式加载和链接库依赖关系。从Lollipop-MR1开始,库将更改链接到BFS。此更改使库链接一个两阶段的过程,库相关性中的所有库都在重定位之前已加载到内存中。
Library Relocation程序
库加载过程完成后,库的依赖关系记录在soinfo中。动态链接器读取以root开头的soinfo来建造local_group。重定位在local_group上执行。重定位主循环将库从local_group 出队并查找。local_group是根据BFS构建的,因此重定位也是BFS。
当解析一个库的符号时,动态链接器会遍历Relocation Section,这是共享库中需要重新放置(DT_REL
或DT_RELA
ELF)所有内容的表。对于每个重定位条目,链接器都会读取符号索引并将其转换为符号名称。使用该名称,链接器在依赖关系树中搜索它的定义-从库本身开始,然后是global_group(请参见“动态链接器的扩展”)和local_group。在库中搜索符号定义时,动态链接程序将检查其符号表(DT_SYMTAB
ELF)。有一种用于表查找的加速方法,DT_HASH
ELF是一个散列表,其中包含库的所有“已导出”或“已导入”符号。
库重定位过程很直观。完成后,动态链接器将调用依赖项中的所有库构造函数。构造函数完成后,将加载库,动态链接器会将此库的处理程序返回给用户。
扩展动态链接器
动态链接具有一些扩展以支持各种情况,而Android则针对特定目的扩展了动态链接功能。
通用动态链接
Global Library
当一个库被声明为“全局库”并加载了标志时RTLD_GLOBAL
,该库的符号定义对于之后加载的所有库具有最高优先级。
每次加载库时,Android动态链接器都会在每次开始时构建global_group。重定位符号时,首先查找global_group- “全局库”可以覆盖之后要加载的库的符号定义。
Preload库
当二进制文件带有标志执行时LD_PRELOAD
。这些库将在二进制文件真正执行之前加载。Android动态链接器在初始化时会预加载这些库。这些库将带有该标志RTLD_GLOBAL
。之后,“预加载库”就像“全局库”。LD_PRELOAD
仅对纯native程序有效。
Android扩展动态链接
Android系统扩展了动态链接,以改善从Java和本机加载库时的体验。该API是android_dlopen_ext()
。直到M,此扩展程序的功能如下所示,其中大多数功能都很容易理解。只需从源文件中复制…。
该扩展的数据结构如下:
typedef struct {
uint64_t flags;
void* reserved_addr;
size_t reserved_size;
int relro_fd;
int library_fd;
off64_t library_fd_offset;
} android_dlextinfo;
与库内存相关
ANDROID_DLEXT_RESERVED_ADDRESS
:设置后,reserved_addr
andreserved_size
字段必须指向地址空间的一个已经保留的区域,如果合适,该区域将用于加载库。如果保留区域不够大,则加载将失败。
ANDROID_DLEXT_RESERVED_ADDRESS_HINT
:作为DLEXT_RESERVED_ADDRESS,但是如果保留区域不够大,则链接器将改为选择可用地址。
Library打开相关
ANDROID_DLEXT_USE_LIBRARY_FD
:指示dlopen使用library_fd
而不是按名称打开文件。filename参数仍用于标识库。
ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET
:如果使用打开图书馆,library_fd
请从开始阅读library_fd_offset
。该标志仅在ANDROID_DLEXT_USE_LIBRARY_FD
设置时有效。
ANDROID_DLEXT_FORCE_LOAD
:设置后,请勿检查文件stat(2)s是否已加载该库。如果由于某些原因多个ELF文件共享相同的文件名(例如,由于已经加载并删除了已加载的库),则此标志允许强制加载该库。请注意,如果该库具有与旧库相同的dt_soname,而其他库在DT_NEEDED
列表中具有该soname ,则第一个库将用于解析任何依赖项。
Library relocation相关
ANDROID_DLEXT_WRITE_RELRO
:设置后,请relro_fd
在执行重定位后将映射库的GNU RELRO部分写入,以允许另一进程在相同地址加载相同库时重用它。这意味着ANDROID_DLEXT_USE_RELRO
。
ANDROID_DLEXT_USE_RELRO
:设置后,relro_fd
在执行重定位后,将映射库的GNU RELRO部分与进行比较,并替换与从文件映射的版本相同的所有重定位页面。
动态链接器的引导程序
动态链接器旨在“链接”所有可重定位的二进制文件,并且必须使其自身看起来像libdl.so
可重定位的libdl.so
二进制文件-二进制文件只是一个使ld
编译器工具链满意的虚拟库。动态链接器在编译时是静态链接的,除了系统调用外不依赖任何其他资源。自我定位和伪造的libdl.so
是Bootstrap。
Android动态链接器的引导分为两个步骤:
- 初始化:硬编码以重新定位链接器本身。
- 初始化后:准备“链接器运行时”以加载库。
初始化
在此阶段,所有执行的代码都将静态重定位。没有外部变量,外部函数或GOT访问。从调用begin.S
,之后将调用Post-initialize函数。主要操作是重定位链接器本身并创建虚拟libdl.so
soinfo。
重定位链接器本身是一个悲伤的故事,每件事都是手工获得的。在对soinfo进行正确设置(主要与内存相关)之后,便会进行真正的重定位。然后调用链接器的构造函数以初始化链接器的全局变量。
主要设置创建虚拟libdl.so
soinfo的过程,并将soinfo的引用更新为硬编码数组,例如符号表。的soinfo节点libdl.so
始终是ALList的第一个节点。
完成这些工作后,链接器将重新定位。
后初始化
在自我重定位后,动态链接程序将重定位somain(主进程)Zygote。
在重定位Zygote之前,链接器会从系统(如LD_LIBRARY_PATH
和)中请求运行时变量LD_PRELOAD
。然后,它重新安置了Zygote。Zygote重定位后,加载在中声明的所有库LD_PRELOAD
。完成所有操作后,链接器完成Bootstrap并跳转到Zygote。
Library依赖
如开头所述,动态链接器的一项任务是重新构建库依赖项。在某些特殊情况下,重建过程对运行时环境敏感。
考虑到有两组库-set1和set2。这两个集合中的某些库共享相同的名称,但具有不同的定义。首先,只能加载set1,然后可以加载set1和set2。诀窍是,无论如何依赖,在阶段1中加载的库只能依赖set1中的库,如上图所示。这是因为每当需要set1中的库时,动态链接程序都将简单地重用它的soinfo。
LD_PRELOAD
在传统的Linux中,在Android的Zygote分支之前加载的库就是这种情况。对于大多数开发人员来说,这很好,但是可能会影响某些仿真系统。
总结
动态链接器重新构建可执行文件的依赖关系,查找,加载和链接它。它是现代操作系统的基本基础结构,并且对运行环境敏感。动态链接通常是在高端平台上定制的,并且需要* bootstrap*。
Android N包括名称空间更改,以防止加载非公共API。此功能严重影响了Android的生态系统。从理论上讲,名称空间可以在动态链接中实现“虚拟化”。我们在本文档中的“内部流程”中讨论了动态链接,而名称空间可以构建多个虚拟空间-名称空间-在一个流程中进行动态链接,从而使动态链接成为“内部名称空间”。将来我们将引用命名空间。(请参阅基于命名空间的动态链接)