从APP的启动提及

iOS里面APP的启动,过程有些复杂,今天咱们来抽丝剥茧,一步步探讨一下APP的启动会经历哪些过程。html

首先,用户点击iPhone里面的某个APP的icon,Kernel内核会开始初始化空间并建立进程, 在调用exec_active_image后,开始加载Mach-O文件。macos

这里咱们简要说一下Mach-O文件。数组

Mach-Osass

Mach-O是iPhone下的可执行文件格式,咱们的APP对应的ipa文件,解压缩之后就会看到这个Mach-O文件,咱们能够用MachOView这个软件来查看一下,如图:架构

(注:这里使用的是x86架构下的mach-o文件,也就是模拟器生成的,若是是arm架构的话会有一些区别,不过区别不大,总体结构差很少)app

 咱们拿其中几个比较重要的来说解一下。ide

Mach64 Header:描述了Mach-O的CPU架构、文件类型以及加载命令等信息。函数

Load Commands:一系列的加载的命令集合,在Mach-O文件加载的时候用于给kernel和dyld调用,如图:学习

LC_SEGMENT_64(__PAGEZERO):映射虚拟内存的第一页地址和大小,通常是4G(0x1000000)大小。spa

LC_SEGMENT_64(__TEXT):代码段的Header,里面记录了__TEXT的各类类型的偏移地址,如图:

代表了__stubs的偏移地址以及一些相关的头信息,其余的Header也相似。

LC_SEGMENT_64(__DATA):数据段,里面记录的信息也是偏移地址和一些相关头信息。

LC_SEGMENT_64(__LINKEDIT):记录的是动态连接相关的偏移地址和头信息(主要是dyld),动态连接十分重要,咱们在后面会说到。

LC_DYLD_INFO_ONLY:记录了动态连接的rebase,binding,lazy binding等的头信息和偏移地址。

LC_SYMTAB:符号表的信息,记录符号表的位置,偏移量,数据个数等。一般跟Symbol Table还有String Table一块儿来查找符号地址,以下图:

在__Text代码段找到代码-[XFCorrelationNewsJSExport onload]的符号地址:0x1000014E0,经过LC_SYMTAB中的Symbol Table Offset找到地址 0x0012C218,而后根据此地址找到Symbols -[XFCorrelationNewsJSExport onload] 的偏移地址 0x00006D70 与 String Table的起始地址相加后计算出符号地址为:0x0017DB7C,而后就能够找到咱们符号对应的字符串,若是要收集crash,也就能够拿到符号地址对应的符号的名字了。

LC_LOAD_DYLINKER:该Mach-O使用的连接器信息,记录了具体使用哪一个连接器接管内核后续的加载工做,以及连接器的位置信息。

LC_LOAD_DYLIB:依赖库信息,dyld会经过这个段去加载动态库。列出了全部依赖的动态库。

Mach-O文件就暂时介绍到这里,后续提到动态连接器(dyld),动态库(dylib),动态库的延迟绑定问题时,还会继续介绍Mach-O相关的Section。

这里分享一点关于Mach-O的小感悟,一开始我在看Mach-O文件的各个section和segment的时候,以为这么多的section,这么多的segment,我怎么可能搞清楚每个都是干什么的,就算搞清楚了,时间长了也会忘记。后来我仔细想了一下,以为Mach-O只是一种操做系统认识的可执行文件格式,因此他的各个section或者segment都是为了在不一样的时候和不一样的阶段提供不一样的信息给操做系统使用的,因此,我我的认为,只须要了解他的大体结构(MachHeader)和比较核心的几个点(Load Commands,动态库和动态连接相关)就能够了。

在加载了Mach-O后,会开始载入动态连接器。

咱们来简要说一下动态连接器。

动态连接器

在介绍动态连接器以前,咱们有必要先介绍一下什么是连接,什么是动态连接。

连接

连接就是经过连接器将执行文件中引用的其余符号(变量和方法)作地址重定位的过程。连接分为:静态连接和动态连接。

静态连接

如今假设文件A,里面有方法 a(),方法a()里面引用了文件B里面的方法b(),那么在编译器编译的时候,会将方法a里面调用的方法b的地址以0x0,0x2等这些来暂时代替,而后输出可执行文件C,等到调用静态连接器的时候,由静态连接器来将真实的方法b的地址(这里的真实地址实际上是指的虚拟地址)修改到C对应的位置上。

这里有个问题就是静态连接器如何知道哪些符号的地址须要重定位呢?

由于在编译A的时候,会生成一个重定位表,里面记录了哪些符号须要被重定位。

动态连接

动态连接区别于静态连接在于连接的时机不一样,静态连接是编译的时候作连接,而动态连接是在APP启动时作连接,并且对于动态库而言,里面的方法并不会作连接操做,只有当第一次运行到这个方法时,才会去作连接操做,从而获得真正的地址,这也叫:延迟绑定。

动态连接主要是针对动态库(dylib,或者也能够叫共享库)的连接操做,在系统的/usr/lib目录下,存放了大量供系统与应用程序调用的动态库文件。动态库不能直接运行,而是须要经过系统的动态连接器(dyld)进行加载到内存后执行,当dyld加载完动态库之后,不一样的APP可使用一样的动态库(跨进程共享代码和部分数据)。可是须要注意的是,对于各进程共享的部分,只包括代码和不须要修改的数据部分,对于会变更的数据部分,是会被分离出来,每一个进程一个副本。

这里有一个问题,就是如何才能在各个进程间共享能够共享的动态库的代码和无需修改的数据呢?

由于各进程调用动态库的地址都是各个进程的虚拟地址,彼此独立,因此你没办法修正动态库的代码的地址来适应全部进程调用,因而有人想到了用绝对地址,虽然能够知足这一要求的,可是会带来新的问题,即:

- 程序每引入一个共享库或者共享库更新后占用空间更大,就须要预留更大的虚拟空间(可是事实上并非每一个函数都会被调用到),可执行文件或许就要从新编译。
- 共享对象更新时,内部的符号地址可能变化,可执行文件又得从新编译。

因此用到了地址无关代码 (PIC, Position-independent Code) 技术:

不管目标模块(包括共享目标模块)被加载到内存中的什么位置,数据段老是紧跟着地址段的。所以,代码段中的任意指令与数据段中的任意变量之间的距离在运行时都是一个常量,而与代码和数据加载的绝对内存位置无关。

例子:

 1 //动态库代码 Person.h
 2 extern const NSString * _Nonnull str;
 3 
 4 extern int add(int a, int b);
 5 
 6 NS_ASSUME_NONNULL_BEGIN
 7 
 8 @interface Person : NSObject
 9 
10 - (void)printStr:(NSString *)str;
11 
12 @end
13 
14 //动态库代码 Person.m
15 const NSString * _Nonnull str  = @"abc";
16 
17 int add(int a, int b) {
18     return a + b;
19 }
20 
21 @implementation Person
22 
23 - (void)printStr:(NSString *)str {
24     
25     NSLog(@"sss:%@", str);
26 }
27 
28 @end
29 
30 //另外一个项目引入动态库后调用的代码
31 - (void)viewDidLoad {
32     [super viewDidLoad];
33     // Do any additional setup after loading the view.
34     Person *person = [[Person alloc] init];
35     [person printStr:@"ttt"];
36     
37     NSLog(@"%@", str);
38     
39     NSLog(@"%d", add(3, 5));
40 }

动态连接对于数据引用和方法引用,处理的方式有些区别。

数据引用:

编译器在代码段和数据段之间建立了一个GOT(Global Offset Table,全局偏移表),里面存储的是目标模块引用的动态库中的变量,如图:

 

初始状态下,这些GOT中的地址都是0x0,到了app启动的时候,在Binding阶段(后面会讲到)动态连接器会将GOT中的数据地址都作一次修正。由于GOT是一个数组,因此修正的方式比较简单,即:GOT[n] = 代码段的地址 + 代码段与数据段的固定偏移 + GOT数据大小

方法引用(延迟绑定):

编译器在编译的时候会在__TEXT,__stubs里面将动态库的add方法生成一个占位,这个占位主要用来指向__DATA,____la_symbol_ptr里面对应的项,如图:

当运行到上面的代码第39行,目标函数调用动态库中的add方法,对应汇编如图:

bl是汇编指令,跳转到子程序的意思,使用Hopper Disassembler查看一下汇编,如图:

ldr:将内存中的值存入到寄存器x16中,此时0x10000c018正好对应__DATA,____la_symbol_ptr中的项,

br:x16  跳转到x16指向的地址,如图:

第一次调用add方法的时候,__DATA,____la_symbol_ptr里面还没有记录add的地址,而是指向__TEXT,__stub_helper里面相关的内容(0x0000001000065E4),如图:

w16:寄存器x16的低32位

.long 0x0000003f 找寻Dynamic Loader Info 中Lazy Binding Info的偏移3f的符号

上述代码的意思就是:跳转到__TEXT,__stub_helper头部(65CC),而后调用 dyld_stub_binder(动态连接器的入口) 进行符号绑定,最后会将 add 的地址放到 __la_symbol_ptr 处,下次再调用就能够直接取add的地址调用了。

绕了这么大一圈终于完成了方法的绑定,简化一下:

生产stub占位 -> 运行时调用 -> 指向la_symbol_ptr -> 若是有地址则返回地址,若是没有地址则指向stub_helper -> 调用dyld_stub_binder来绑定方法地址并修正la_symbol_ptr的地址。

这里会产生一个问题,为何须要la_symbol_ptr,直接在stub里面修改地址不就完了吗?

由于stub是代码段,而代码段是只读的,动态库的指导思想就是共享代码段,分离出可变数据段,因此须要la_symbol_ptr。

综上所述,咱们能够简单罗列一下静态连接库和动态连接库的区别:

一、静态连接库在编译后,库里的方法及变量地址就肯定了(虚拟地址),动态连接库则是在运行时才能肯定,而动态库中的方法则须要到调用到的时候才能肯定。

二、静态连接库会打包进APP中,而动态连接库则在系统的/usr/lib目录下,若是是本身制做的动态库,也会随着APP一块儿打包进去。

动态连接器(dyld)
苹果操做系统的重要组成部分,负责连接和装载动态库,当xnu内核(开源的系统底层代码,下载地址)加载了动态连接器之后,APP将从内核态过分到用户态。

dyld自己也是mach-o格式的文件,可是dyld中不会再引用其余动态库的东西,因此就不存在动态绑定这个过程了,拿MachOView看看如图:

动态连接器也是开源的,下载地址

接下来App的启动就进入Rebase,Binding阶段了。

这几个阶段都是由dyld来控制的,咱们来简单分析一下他的这几个过程

Rebasing

在过去,会把 dylib 加载到指定地址,全部指针和数据对于代码来讲都是对的,dyld 就无需作任何 fix-up 了。现在用了 ASLR 后会将 dylib 加载到新的随机地址(actual_address),这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有误差,dyld 须要修正这个误差(slide),作法就是将 dylib 内部的指针地址都加上这个偏移量,偏移量的计算方法以下:

Slide = actual_address - preferred_address

Binding

主要是针对那些外部符号作的绑定操做,好比咱们上面说的GOT中的内容。

剩余启动事件

App启动到这里接下来就是进入到Runtime环节,会初始化Runtime环境并初始化,处理category和调用+load()方法。

initializers 调用全部动态库的initializer方法,初始化动态库。

调用App的main函数,正式进入App的生命周期。

小结

App的启动咱们来回顾一下,主要分为:加载Mach-O、加载dyld、rebase、binding、加载dylib,Runtime、Initializer、main这几个过程,咱们主要讲解了一下Mach-O的文件结构,动态连接的GOT和动态绑定过程,还简单介绍了rebase和binding。

能够看出来,App的启动过程十分复杂,还有不少细节和知识点须要咱们仔细深刻研究和学习。

相关文章
相关标签/搜索