对于ios
来说,main函数
是ios
应用的入口函数。我们可以在main
函数打上断点,如下图所示:
通过bt
打印当前调用栈可以发现,main
函数的调用来自于libdyld.dylib
中的start
方法。今天我们就来探索一下这个dyld
到底有什么作用。
libobjc:781
libdyld:750.6
dyld
dyld(the dynamic link editor)
是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld
负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。
dyld流程
_dyld_start
通过下图我们可以了解到关于dyld
更多的内容。
可以发现调用栈的起点是_dyld_start
,我们在dyld
源码中搜索一下_dyld_start
。
_dyld_start
是由汇编编写的,根据注释以及上文中的截图对应可以发现_dyld_start
调用了dyldbootstrp::start
方法。
dyldbootstrp::start
注释表示这是启动dyld
的代码。一般来说程序的启动工作是由dyld和crt
来做的。但是在dyld
中,需要我们手动启动,因为此时dyld
还没有启动。
上图中启动代码主要干了三件事情:
rebaseDyld
在磁盘上,dyld
的DATA segment
的所有指针是链在一起的,需要被修正已执行正确的指针。
当前修正链上的所有镜像的基地址都是0
,因此偏移量slide
就是加载的地址。
__guard_setup
设置栈溢出保护
dyld::_main
_main
是dyld
的main
函数,并不是我们系统的main
函数,代码比较多,核心代码如下:
先看一下注释:dyld
的入口。内核加载dyld
并且跳转到__dyld_start
,__dyld_start
方法设置了一些注册回调并且调用了_main
函数。
设置上下文context
,保存了当前的一些状态。
- 配置环境变量,可以通过在环境变量中设置DYLD_PRINT_ENV来打印出来当前的环境变量,如下图所示:
打印如下:
其中可以看到插入的动态库有三个,分别是:
libBacktraceRecording.dylib
、libMainThreadChecker.dylib
和libViewDebuggerSupport.dylib
。 - 加载共享缓存库
checkShareRegionDisable
函数实现如下: 其中最下面的注释为:iOS
不能脱离了共享区域运行,就是说iOS必须使用共享缓存库。mapShareCache
函数的核心调用为loadDyldCache
,代码如下: 由此可见,共享缓存库确实做到了共享这一特点。
- 接着回到
_main
方法,实例化主程序,就是读取macho
中各项内容保存起来。 - 加载插入的动态库
- 链接主程序 其中会递归的加载依赖库。
- 链接插入的动态库,在链接主程序之后执行以确保插入的动态库依赖的其他动态库不会在程序使用的动态库之前。
- 递归绑定主程序以及主程序依赖的其他库
- 递归绑定插入的镜像
- 弱符号绑定
- 执行所有的初始化
代码如下:
先执行插入的动态库的初始化,再执行主程序的初始化。因为还有一些其他的依赖库,因此这也是一个递归的过程。需要注意的是在
initializeMainExecutable
方法的最后判断了环境变量DYLD_PRINT_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
,这两个环境变量设置之后系统会在命令行打印出启动时间的一些数据,帮助我们进行一些启动时间的优化。
初始化方法调用关系如下:runInitializers
->processInitializers
->recursiveInitialization
执行完毕初始化之后,会发出镜像初始化完毕的通知。
sNotifyObjCInit
赋值的代码如下:
由_dyld_objc_notify_register
调用
_dyld_objc_notify_register
方法在dyld
源码中并没有被显式调用,我们打符号断点运行。
发现该方法被_objc_init
调用,而_objc_init
方法位于libobjc
中,观察调用堆栈如下:
其实sNotifyObjCInit
是在执行初始化过程中注册的回调。这一步对于后续runtime
的初始化至关重要。
11. 寻找主程序的main函数地址并返回。