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

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

这里我们简要说一下Mach-O文件。

Mach-O

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

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

我们拿其中几个比较重要的来讲解一下。

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

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

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

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) 技术:

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

例子:

 //动态库代码 Person.h
extern const NSString * _Nonnull str; extern int add(int a, int b); NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject - (void)printStr:(NSString *)str; @end //动态库代码 Person.m
const NSString * _Nonnull str = @"abc"; int add(int a, int b) {
return a + b;
} @implementation Person - (void)printStr:(NSString *)str { NSLog(@"sss:%@", str);
} @end //另一个项目引入动态库后调用的代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Person *person = [[Person alloc] init];
[person printStr:@"ttt"]; NSLog(@"%@", str); NSLog(@"%d", add(, ));
}

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

数据引用:

编译器在代码段和数据段之间创建了一个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。

综上所述,我们可以简单罗列一下静态链接库和动态链接库的区别:

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

2、静态链接库会打包进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的启动过程十分复杂,还有很多细节和知识点需要我们仔细深入研究和学习。

从APP的启动说起的更多相关文章

  1. Android性能优化之App应用启动分析与优化

    前言: 昨晚新版本终于发布了,但是还是记得有测试反馈app启动好长时间也没进入app主页,所以今天准备加个班总结一下App启动那些事! app的启动方式: 1.)冷启动      当启动应用时,后台没 ...

  2. App的启动过程

    App的启动过程 所有的app都是通过launcher去启动的 launcher自己也是一个app,一个系统级别的app,放在asystem/app/下,使用系统签名. 对代码进行分析

  3. iOS app 程序启动原理

    iOS app 程序启动原理 Info.plist: 常见设置     建立一个工程后,会在Supporting files文件夹下看到一个"工程名-Info.plist"的文件, ...

  4. iOS 9 failed for URL: "XXX://@" - error: "This app is not allowed to query for scheme XXX" iOS 从APP里启动另一APP

    iOS 从C APP里启动 D APP 首先在D APP里设置 URL Schemes 在info.plist 文件里添加URL Schemes URL Types -->item0 --> ...

  5. 由App的启动说起(转)

    The two most important days in your life are the day you are born and the day you find out why. -- M ...

  6. 如何设置App的启动图

    如何设置App的启动图,也就是Launch Image? Step1 1.点击Image.xcassets 进入图片管理,然后右击,弹出"New Launch Image" 2.如 ...

  7. 【转载】Android App应用启动分析与优化

    前言: 昨晚新版本终于发布了,但是还是记得有测试反馈app启动好长时间也没进入app主页,所以今天准备加个班总结一下App启动那些事! app的启动方式: 1.)冷启动  当启动应用时,后台没有该应用 ...

  8. 怎样做一个iOS App的启动分层引导动画?

    一. 为什么要写这篇文章? 这是一个很古老的话题,从两年前新浪微博开始使用多层动画制作iOS App的启动引导页让人眼前一亮(当然,微博是不是历史第一个这个问题值得商榷)之后,各种类型的引导页层出不穷 ...

  9. iOS App的启动过程

    一.mach-O Executable 可执行文件 Dylib 动态库 Bundle 无法被连接的动态库,只能通过 dlopen() 加载 Image 指的是 Executable,Dylib 或者 ...

随机推荐

  1. C++程序员容易走入性能优化误区!对此你怎么看呢?

    有些C++ 程序员,特别是只写C++ 没有写过 Python/PHP 等慢语言的程序员,容易对性能有心智负担,就像着了魔一样,每写3 行代码必有一行代码因为性能考虑而优化使得代码变形(复杂而晦涩). ...

  2. CF453A Little Pony and Expected Maximum 期望dp

    LINK:Little Pony and Expected Maximum 容易设出状态f[i][j]表示前i次最大值为j的概率. 转移很显然 不过复杂度很高. 考虑优化.考虑直接求出最大值为j的概率 ...

  3. NOI On Line 提高组题解

    (话说其实我想填的是去年CSP的坑...但是貌似有一道题我还不会写咕咕咕... 先写一下这一次的题解吧. T1:序列.题意省略. 两种操作.这种题要先分析部分分 给出了全部都是2操作的子任务. 发现A ...

  4. Android JNI之数据类型

    JNI中数据类型的意义在于桥接Java数据类型与C数据类型. 简单数据类型: Java Type Native Type Description boolean jboolean unsigned 8 ...

  5. __STL_VOLATILE

    _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n); _Obj* * __my_free_list ...

  6. 并发|WEB服务器并发

    面试中容易被问到你们服务器的并发是多少?但是这个问题我问过许多人,没有得到一个准确的答案!我总结了一些不错的回答,分享给大家! 面试题: 你们公司的服务器并发是多少? 我的回答: 1.并发这个词,许多 ...

  7. SeaweedFS在.net core下的实践方案(续一)

    前言 我们之前已经完成了SeaweedFS在.net core下的使用了,但是说实话,还是不够,于是,我的目光盯住了IApplicationBuilder的扩展方法UseStaticFiles 这个可 ...

  8. 区间DP 学习笔记

    前言:本人是个DP蒟蒻,一直以来都特别害怕DP,终于鼓起勇气做了几道DP题,发现也没想象中的那么难?(又要被DP大神吊打了呜呜呜. ----------------------- 首先,区间DP是什么 ...

  9. MySQL面试题!新鲜出炉~

    01.Mysql 的存储引擎,myisam和innodb的区别? 答:1.MyISAM 是非事务的存储引擎,适合用于频繁查询的应用.表锁,不会出现死锁,适合小数据,小并发. 2.innodb是支持事务 ...

  10. 006_go语言中的互斥锁的作用练习与思考

    在go语言基本知识点中,我练习了一下互斥锁,感觉还是有点懵逼状,接下来为了弄懂,我再次进行了一些尝试,以下就是经过我的尝试后得出的互斥锁的作用. 首先还是奉上我改造后的代码: package main ...