8、iOS强化 --- 动态链接(详解)
我们在5、iOS强化 --- 链接与符号(补充内容)里面提到了动态链接,在这里我们再详细的探讨一下,动态链接到底是怎么链接的。
- 动态链接的基本思想就是:把程序的模块分割开来,不是通过静态链接在一起,而且推迟到程序运行的时候链接在一起。
还记的我们之前举的例子吗?
比如:我们开发中经常会用到的Foundation框架(Apple提供的),如果采用静态链接的方式,那么市面上所有用到它的APP都要在自己的Mach-O文件中集成它。试想一下,一旦该库出现问题,那所有用到它的APP都要从新集成,从新上架。这样做不仅让APP的体积变大,而且还极不方便。
接下来我们来看一下动态链接是怎么实现的。
动态链接的基本实现
首先我们来了解两个名词:
名字 | 解释 |
---|---|
dyld |
the dynamic link editor。动态链接器 |
dylib |
动态链接库,也叫做共享对象 |
-
静态链接和动态链接都是把程序分割成一个个独立的模块,但是不同的是:
image.png
1、静态链接
是运行前就用ld
链接器链接成一个完整的程序。
2、动态链接
是程序主模块被加载的时候,对应的Mach-O
文件里面有dyld
加载命令,通过这个dyld
去寻找依赖的dylib
(⚠️Mach-O
有动态链接加载命令),把dylib
加载到内存(如果对应的dylib
不在内存),然后将程序中所有未决议的符号绑定到相应的dylib
中,并进行重定位工作。
dyld
&dylib
加载命令如下:
-
dylinker_command
// dyld 加载命令
struct dylinker_command {
uint32_t cmd; /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
LC_DYLD_ENVIRONMENT */
uint32_t cmdsize; /* includes pathname string */
union lc_str name; /* dynamic linker's path name */
};
-
lc_str
dyld
加载命令中
1、offset
为sizeof(cmd)+sizeof(cmdsize)+sizeof(offset)=12
2、ptr
表示dyld
的路径
则上面表示偏移12位置是dyld
的路径。
在加载命令中,假如有字符串,那就都用lc_str
表示,lc_str
仅仅告诉去相对于加载命令头部(cmd
)多少的偏移位置去取字符串,这个字符串都是放在加载命令结构体最后。
/*
* A variable length string in a load command is represented by an lc_str
* union. The strings are stored just after the load command structure and
* the offset is from the start of the load command structure. The size
* of the string is reflected in the cmdsize field of the load command.
* Once again any padded bytes to bring the cmdsize field to a multiple
* of 4 bytes must be zero.
*/
union lc_str {
uint32_t offset; /* offset to the string */
#ifndef __LP64__
char *ptr; /* pointer to the string */
#endif
};
-
dylib
加载命令
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};
想必这里大家都主要到了,dyld
&dylib
加载命令的最后都是lc_str
,大家仔细阅读lc_str
的注释部分。
大致意思就是说:name
放在加载命令的结尾,name
放到加载命令结构体时候,偏移量是从加载命令结构体的开头开始的。并且加载命令是4字节的倍数,不够的话用0填充(必须是0)。
动态链接实例
-
a.c
文件
extern char *global_var;
void print(char *str);
int main(int argc, const char * argv[]) {
print(global_var);
return 0;
}
-
b.c
文件
extern char *global_var;
void print(char *str);
int main(int argc, const char * argv[]) {
print(global_var);
return 0;
}
我们将b.c
文件包装成dylib
动态库,然后a
文件与动态库合并成main
可执行文件:
1、编译a.c
,生成a.o
xcrun -sdk iphoneos clang -c a.c -o a.o -target arm64-apple-ios12.2
2、编译b.c
生成libPrint.dylib
xcrun -sdk iphoneos clang -fPIC -shared b.c -o libPrint.dylib -target arm64-apple-ios12.2
3、链接main.o
&libPrint.dylib
,生成main
可执行文件
xcrun -sdk iphoneos clang a.o -o main -L . -l Print -target arm64-apple-ios12.2
// -target arm64-apple-ios12.2 ==> 运行的目标版本号iOS12.2
// -l Print ==> 链接libPrint.dylib
// -L . ==> libPrint.dylib在当前路径寻找(.代表当前路径)
image.png
⚠️ 上面说过
动态链接
与 静态链接
的区别就是链接的时机推迟到程序被加载的时候。但是上面第3步将目标文件main.o
链接成可执行文件的时候,还是用到了动态库libPrint.dylib
。我们来看一下
main
可执行文件:image.png
我们在7、iOS强化 --- 静态链接(详解)讲过,再经过重定位之后,静态链接的话,此时已经。但是通过上图可以看到,动态链接的话,此时是。、
动态链接的情况下,在链接的时候让链接器(dyld
)知道这两个符号来自dylib
,只需要给这两个符号做标记就可以了,而不是此刻进行绑定和重定位。生成的main
可执行文件知道这两个符号来自dylib
,做了标记。等到main
被加载的时候,再把这两个符号绑定到dylib
中,并进行重定位。
PIC(position-independent code 地址无关代码)
-
产生地址无关代码的原因
dylib
在编译的时候,是不知道自己在进程中的虚拟内存地址的。因为dylib
可以被多个进程共享。
例子:现在有一个dylib
作为共享对象(标记为Z),同时被两个进程(A 和 B) 共享。假如A进程可以在空闲地址0x1000-0x2000
存放共享对象Z,但是B进程的0x1000-0x2000
已经被主模块占用了,只有空闲地址0x3000-0x4000
可以存放共享对象Z。
那么Z对象里面有一个函数,此时在A进程中的虚拟内存地址就是0x10f4
,在B进程中的虚拟内存地址就是0x30f4
。这样的话机器指令就不能包含绝对地址了(动态库代码段所有进程共享;可修改的数据段,每个进程有一个副本,私有的)。 -
PIC原理
PIC
就是为了解决dylib
的代码段能被共享的问题。
PIC
:把指令中那些需要被修改的部分剥离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分是每个进程都有一个副本。
dylib
需要被修改的部分(对地址的引用),按照是否跨模块分为两类;按照引用方式又可以分为两类:函数调用 & 数据访问。这样总共可以分为4类:
dylib 需要被修改的部分,分类情况 |
---|
1、模块内部的函数调用、跳转等。 |
2、模块内部的数据访问,比如模块中定义的全局变量,静态变量等。 |
3、模块外部的数据访问、跳转等。(如:a.dylib 调用 b.dylib 中的函数) |
4、模块外部的数据访问。(访问其他模块模块中定义的全局变量) |
延迟绑定
- 延迟绑定的基本思想跟iOS的
objc_msgSend
基本一样的,都是第一调用函数的时候,去查找函数的地址。而不是程序启动的时候,先把所有的地址查找好。
模块外部的函数
和数据
的放完,都是通过got
来间接寻址的。程序被加载的时候,动态链接器要进行一次链接工作,比如加载依赖的模块,修改got
里面的地址(符号查找、地址重定位)等工作,减慢了程序的启动速度。
举个例子:我们在开发中引入Foundation
动态库,但是并不是说库里面的所有函数我们都会用到。如果我们在程序启动的时候就去绑定所有的符号地址,显然是不合理的。
这时候就用到了延迟绑定
的技术。
在Mach-O
中,因为模块间的数据访问很少(模块间如果提供了很多全局变量给其他模块使用,那耦合度太大了,所以这种情况很少见),所以外部数据地址都放到got
(也叫做Non-Lazy Symbol Pointers)数据段,非惰性的,动态链接阶段就寻找好所有数据符号的地址。
而模块间函数调用就很频繁,这里就用延迟绑定
的技术,将外部函数地址放到la_symbol_ptr(Lazy Symbol Pointers)
数据段,惰性的,程序第一次调用这个函数的时候,才去寻找函数地址,然后将地址写入到这个数据段。
image.png
-
这里我们再引入一个加载命令
dysymtab_command
间接符号表
间接符号表我们在4、iOS强化 --- 链接与符号(Symbol)里面讲过,它里面保存着我们当前可执行文件使用的其他动态库里面的导出符号。我们来看一下间接符号表:
image.png
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */
...
uint32_t indirectsymoff; /* file offset to the indirect symbol table */ 到间接符号表的偏移量,可以确定位置
uint32_t nindirectsyms; /* number of indirect symbol table entries */ 间接符号表里面符号的个数
...
}
dysymtab_command
可看做指向一个数组,里面的元素是整型数字。
例如:dysymtab[0] == 2
,代表间接符号表第0项对应的符号,在符号表中的第2项。
- 还有一个结构体需要说明一下
section_64
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
大家可以看到结构体中有一个reserved1
,在got
数据段的section_64
里面,reserved1
代表got
里面的符号在间接符号表(IndirectSymbolTable
)的起始Index
。
结合上面的间接符号表的含义,我们可以得出这样一个逻辑:
1、假设A
代表间接符号表里面的元素,则 A = IndirectSymbolTable[got.section_64.reserved1]
2、拿到A
之后,我们就可以去符号表里面找到对应的符号。
- 下面我们按照上面的逻辑来寻找一下
_global_var
image.png - 下面我们再来寻找一下
_print
1、我们在la_sumbol_ptr
数据段里面找到_print
:
image.png
可以看到_print
对应了一个地址7FAC
。
我们在讲3、iOS强化 --- Mach-O 文件有提到过下面一句话:
image.png
那么接下来我们去__stub_helper
找一下对应的元素
2、查看__stub_helper
image.png
这里面是一些汇编指令,可能有的同学看起来会有点吃力,不过没关系。
首先我们要明确一点,__la_symbol_ptr
表里面的指针,就是指向这里,这一点是没有问题的。
接着我们会发现一行注释:literal pool symbol address:dyld_stub_binder
。那么我们就去找一下dyld_stub_binder
:
image.png
_print
的寻址过程,总结一下就是:
1、先从la_symbol_ptr
找到对应指针;
2、然后去__stub_helper
找到相应的地址(建议大家去理解一下这里面的汇编代码);
3、跳到相应的地址,此时到了got
数据段。
⚠️ 我们在上面提到过,got
里面存放的是外部数据符号。但是在动态链接的时候,会重定位dyld
的dyld_stub_binder
函数地址,放在这里。
dyld_stub_binder
是一个寻址外部函数地址的函数,所以必须提前重定位好。
第一次调用print
函数的时候,会调用dyld_stub_binder
函数去寻找地址,找到之后把print
的地址写入到la_symbol_ptr
数据段,替换到对应的指针(这里面是结尾是7FAC
的地址),然后调用print
函数。
之后再调用print
函数的时候,就不用去寻址了,直接就可以调用,因为函数地址已经被写入了la_symbol_ptr
数据段。