启动流程如图
总结来说,大体分为如下步骤:
(1) 系统为程序启动做好准备
(2) 系统将控制权交给 Dyld,Dyld 会负责后续的工作
(3) Dyld 加载程序所需的动态库
(3) Dyld 对程序进行 rebase 以及 bind 操作
(4) Objc SetUp
(5) 运行初始化函数
(6) 执行程序的 main 函数
加载动态库
dyld会首先读取mach-o文件的Header和load commands。 接着就知道了这个可执行文件依赖的动态库。
- dyld3
dyld3是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:
Rebase 和 Bind
有两种主要的技术来保证应用的安全:ASLR和Code Sign。
- ASLR ASLR的全称是Address space layout randomization。App启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址 偏移量找到函数的地址。
- Code Sign Code Sign相信大多数开发者都知晓,这里要提一点的是,在进行Code sign的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证。
mach-o中有很多符号,有指向当前mach-o的,也有指向其他dylib的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?
mach-o中采用了PIC技术,全称是Position Independ code。当你的程序要调用printf的时候,会先在__DATA段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分
- Rebase 修正内部(指向当前mach-o文件)的指针指向
- Bind 修正外部指针指向
之所以需要Rebase,是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中。
可以通过MachOView查看:Dynamic Loader Info -> Rebase Info
代码语言:javascript复制192:Desktop Leo$ xcrun dyldinfo -bind demo
bind information:
segment section address type addend dylib symbol
__DATA __got 0x10003C038 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC4LeftC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C040 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC5RightC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C048 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6FooterC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C050 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6HeaderC7spinnerSo23UIActivityIndicatorViewCvWvd
//...
Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。
同样,也可以通过xcrun dyldinfo来查看Bind的信息,比如我们查看bind信息中,包含UITableView的部分:
代码语言:javascript复制192:Desktop Leo$ xcrun dyldinfo -bind demo | grep UITableView
__DATA __objc_classrefs 0x100041940 pointer 0 UIKit _OBJC_CLASS_$_UITableView
__DATA __objc_classrefs 0x1000418B0 pointer 0 UIKit _OBJC_CLASS_$_UITableViewCell
__DATA __objc_data 0x100041AC0 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __objc_data 0x100041BE8 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __objc_data 0x100042348 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __objc_data 0x100042718 pointer 0 UIKit _OBJC_CLASS_$_UITableViewController
__DATA __data 0x100042998 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
__DATA __data 0x100042A28 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
__DATA __data 0x100042F10 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
__DATA __data 0x1000431A8 pointer 0 UIKit _OBJC_METACLASS_$_UITableViewController
Objc
C 会为静态创建的对象生成初始化器,与静态语言不同,OC基于Runtime机制可以用类的名字来实例化一个类的对象。Runtime 维护了一张映射类名与类的全局表,当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中。ObjC 在加载时可以通过 fix-up 在动态类中改变实例变量的偏移量,利用这个技术可以在不改变dylib的情况下添加另一个 dylib 中类的方法,而非常见的通过定义类别(Category)的方式改变一个类的方法。
Dyld 在 bind 操作结束之后,会发出 dyld_image_state_bound 通知,然后与之绑定的回调函数 map_2_images 就会被调用,它主要做以下几件事来完成 Objc Setup:
Initializers
Objc SetUp 结束后,Dyld 便开始运行程序的初始化函数,该任务由 initializeMainExecutable 函数执行。整个初始化过程是一个递归的过程,顺序是先将依赖的动态库初始化,然后在对自己初始化。初始化需要做的事情包括:
- 调用 Objc 类的 load 函数
- 调用 C 中带有 constructor 标记的函数
- 非基本类型的 C 静态全局变量的创建
所谓执行监控启动crash的思路都是在这里构建的。下面是一些方法的执行顺序,initialize的顺序可能在更早,但总是会在load和launch之间。
程序启动逻辑
最后 dyld 会调用 main() 函数。main() 会调用 UIApplicationMain(),程序启动。
main.m文件,此处就是应用的入口了。程序启动时,先执行main函数,main函数是ios程序的入口点,内部会调用UIApplicationMain函数,UIApplicationMain里会创建一个UIApplication对象 ,然后创建UIApplication的delegate对象 —–(您的)AppDelegate ,开启一个消息循环(main runloop),每当监听到对应的系统事件时,就会通知AppDelegate。
代码语言:javascript复制int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}