dyld加载流程

1,002 阅读3分钟

对于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

在磁盘上,dyldDATA segment的所有指针是链在一起的,需要被修正已执行正确的指针。 当前修正链上的所有镜像的基地址都是0,因此偏移量slide就是加载的地址。

__guard_setup

设置栈溢出保护

dyld::_main

_maindyldmain函数,并不是我们系统的main函数,代码比较多,核心代码如下: 先看一下注释:dyld的入口。内核加载dyld并且跳转到__dyld_start__dyld_start方法设置了一些注册回调并且调用了_main函数。 设置上下文context,保存了当前的一些状态。

  1. 配置环境变量,可以通过在环境变量中设置DYLD_PRINT_ENV来打印出来当前的环境变量,如下图所示: 打印如下: 其中可以看到插入的动态库有三个,分别是: libBacktraceRecording.dyliblibMainThreadChecker.dyliblibViewDebuggerSupport.dylib
  2. 加载共享缓存库
  • checkShareRegionDisable函数实现如下: 其中最下面的注释为:iOS不能脱离了共享区域运行,就是说iOS必须使用共享缓存库。
  • mapShareCache函数的核心调用为loadDyldCache,代码如下: 由此可见,共享缓存库确实做到了共享这一特点。
  1. 接着回到_main方法,实例化主程序,就是读取macho中各项内容保存起来。
  2. 加载插入的动态库
  3. 链接主程序 其中会递归的加载依赖库。
  4. 链接插入的动态库,在链接主程序之后执行以确保插入的动态库依赖的其他动态库不会在程序使用的动态库之前。
  5. 递归绑定主程序以及主程序依赖的其他库
  6. 递归绑定插入的镜像
  7. 弱符号绑定
  8. 执行所有的初始化 代码如下: 先执行插入的动态库的初始化,再执行主程序的初始化。因为还有一些其他的依赖库,因此这也是一个递归的过程。需要注意的是在initializeMainExecutable方法的最后判断了环境变量DYLD_PRINT_STATISTICSDYLD_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函数地址并返回。

总结