iOS 应用程序启动过程可以以 main 函数为界,这里我们先不用管 main() 函数调用后的过程,主要来分析一下 mian() 函数调用之前的dyld阶段。
我们可以先写个简单的程序来看看系统在调用 main() 之前,调用了哪些函数。

这里给 load 方法添加了一个断点。从调用栈可以看到最先调用的是 __dyld_start 函数。我们可以从 dyld 源码 dyldStartup.s 中找到 __dyld_start 的实现。此函数由汇编实现,兼容各种平台架构,此处主要以arm64 架构下的汇编代码为例:
1 | #if __arm64__ |
这里主要关注一下 bl 指令:
1 | // call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue) |
从注释了解到,其实就是调用 dyldbootstrap::start() 函数。在 dyldInitialization.cpp 中可以找到 start 函数的实现:
1 | // |
start 函数中做了很多 dyld 初始化相关的工作,包括:
- rebaseDyld() dyld 重定位
- mach_init() mach消息初始化
- __guard_setup() 栈溢出保护
初始化工作完成后,此函数调用到了 dyld::_main,再将返回值传递给 __dyld_start 去调用真正的 main() 函数。
我们可以在 dyld.cpp 中找到 _main 的实现, 代码比较长,就不贴代码了,不过我们可以看看 _main 函数的注释:
Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which sets up some registers and call this function.
Returns address of main() in target program which __dyld_start jumps to
这个是说,内核加载 dyld,并跳转到 __dyld_start 函数,它主要设置一些寄存器,并且调用了 _main函数。这里刚好跟上面分析的过程相吻合。
dyld::_mina() 是应用程序启动的关机函数,主要做了以下一些事情:
- 设置运行环境
- 实例化主程序
- 加载共享缓存
- 加载插入的动态库
- 链接主程序
- 链接插入的动态库
- 执行弱符合绑定
- 执行初始化方法
- 查找入口并返回
设置运行环境
这一步主要是设置运行参数、环境变量等。代码在开始的时候,将入参mainExecutableMH 赋值给了sMainExecutableMachHeader,这是一个macho_header 结构体,表示的是当前主程序的 Mach-O 头部信息,加载器依据 Mach-O 头部信息就可以解析整个 Mach-O 文件信息。接着调用 setContext() 设置上下文信息,包括一些回调函数、参数、标志信息等。如 loadLibrary() 函数实际调用的是 libraryLocator(),负责加载动态库。代码片断如下
1 | static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[]) |
实例化主程序
1 | // instantiate ImageLoader for main executable |
这一步将主程序的 Mach-O 加载进内存,并实例化一个 ImageLoader。instantiateFromLoadedImage() 首先调用 isCompatibleMachO() 检测Mach-O 头部的magic、cputype、cpusubtype 等相关属性,判断 Mach-O 文件的兼容性,如果兼容性满足,则调用ImageLoaderMachO::instantiateMainExecutable() 实例化主程序的ImageLoader,代码如下:
1 | // The kernel maps in main executable before dyld gets control. We need to |
ImageLoaderMachO::instantiateMainExecutable() 函数里面首先会调用sniffLoadCommands() 函数来获取一些数据,包括:
- compressed:若Mach-O存在LC_DYLD_INFO和LC_DYLD_INFO_ONLY加载命令,则说明是压缩类型的Mach-O
- segCount:根据 LC_SEGMENT_COMMAND 加载命令来统计段数量。
- libCount:根据 LC_LOAD_DYLIB、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB、LC_LOAD_UPWARD_DYLIB 这几个加载命令来统计库的数量,库的数量不能超过4095个。
- codeSigCmd:通过解析LC_CODE_SIGNATURE来获取代码签名加载命令。
- encryptCmd:通过LC_ENCRYPTION_INFO和LC_ENCRYPTION_INFO_64来获取段的加密信息。
ImageLoader 是抽象类,其子类负责把 Mach-O 文件实例化为 image,当sniffLoadCommands() 解析完以后,根据 compressed 的值来决定调用哪个子类进行实例化,代码如下:
1 | if ( compressed ) |
instantiateMainExecutable() 执行完后,会调用 addImage() 函数将 image 加入到 sAllImages 全局镜像列表中。并将image映射到申请的内存中, 其代码如下:
1 | static void addImage(ImageLoader* image) |
加载共享缓存
1 | // load shared cache |
这一步先调用 checkSharedRegionDisable() 检查共享缓存是否禁用。该函数的iOS实现部分仅有一句注释,从注释我们可以推断iOS必须开启共享缓存才能正常工作。接下来调用 mapSharedCache() 来加载共享缓存。
加载插入的动态库
1 | // load any inserted libraries |
这一步是加载环境变量DYLD_INSERT_LIBRARIES中配置的动态库,先判断环境变量DYLD_INSERT_LIBRARIES中是否存在要加载的动态库,如果存在则调用 loadInsertedDylib() 依次加载。
loadInsertedDylib() 内部设置了一个LoadContext 后,调用了 load() 函数。该函数内部调用的一系列的 loadPhase*。
1 | // try all path permutations and check against existing loaded images |
大致会按照下图的顺序搜索动态库,并调用不同的函数来继续处理。

当内部调用到 loadPhase5load() 函数的时候,会先在共享缓存中搜索,如果存在则调用 ImageLoaderMachO::instantiateFromCache() 来实例化ImageLoader,否则通过 loadPhase5open() 打开文件并读取数据到内存后,再调用 loadPhase6() ,通过 ImageLoaderMachO::instantiateFromFile() 来实例化 ImageLoader,最后调用 checkandAddImage() 验证镜像并将其加入到全局镜像列表中。
链接主程序
1 | // link main executable |
这一步调用 link() 函数将实例化后的主程序进行动态修正,让二进制变为可正常执行的状态。link() 函数内部调用了ImageLoader::link() 函数,从源代码可以看到,这一步主要做了以下几个事情:
recursiveLoadLibraries()加载所有依赖的库到内存。recursiveUpdateDepth()递归刷新依赖库的层级。recursiveRebase()由于ASLR的存在,必须递归对主程序以及依赖库进行重定位操作。recursiveBind()把主程序二进制和依赖进来的动态库全部执行符号表绑定。weakBind()如果链接的不是主程序二进制的话,会在此时执行弱符号绑定,主程序二进制则在link()完后再执行弱符号绑定。context.registerDOFs(dofs)注册DOF(DTrace Object Format)。
链接插入的动态库
1 | // link any inserted libraries |
这一步与链接主程序一样,将前面调用 addImage() 函数保存在 sAllImages 中的动态库列表循环取出并调用 link() 进行链接,需要注意的是,sAllImages 中保存的第一项是主程序的镜像,所以要从 i+1的位置开始,取到的才是动态库的 ImageLoader。
接下来循环调用每个镜像的 registerInterposing() 函数,该函数会遍历Mach-O 的 LC_SEGMENT_COMMAND 加载命令,读取__DATA, __interpose,并将读取到的信息保存到 fgInterposingTuples 中。
执行弱符合绑定
1 | // <rdar://problem/12186933> do weak binding only after all inserted images linked |
weakBind() 首先通过 getCoalescedImages() 合并所有动态库的弱符号到一个列表里,然后调用 initializeCoalIterator() 对需要绑定的弱符号进行排序,接着调用 incrementCoalIterator() 读取dyld_info_command 结构的 weak_bind_off 和 weak_bind_size 字段,确定弱符号的数据偏移与大小,最终进行弱符号绑定。
执行初始化方法
1 |
|
这一步由 initializeMainExecutable() 完成。dyld 会优先初始化动态库,然后初始化主程序。该函数首先执行 runInitializers(),内部再依次调用 processInitializers()、recursiveInitialization()。在 processInitializers() 之后会发送 dyld_image_state_initialized 通知。
在 recursiveInitialization() 的实现中有这么一行代码:
1 | // let objc know we are about to initialize this image |
注释告诉我们,这个函数主要目的是让 objc 知道 image 即将被初始化。之后执行初始化操作:
1 | // initialize this image |
在 doInitialization() 中首先调用了 doImageInit() ,然后调用 doModInitFunctions() 。
doImageInit 执行镜像的初始化函数,也就是 LC_ROUTINES_COMMAND中记录的函数,然后再执行 doModInitFunctions 来解析并执行_DATA_ 中__mod_init_func 这个 section 中保存的函数。_mod_init_funcs 中保存的是全局C++对象的构造函数以及所有带 __attribute__((constructor) 的C函数。
可以简单的写几行代码验证一下:
1 | __attribute__((constructor)) |
代码编译后可以使用 MachOView来查看 Mach-O 中的内容。

可以看到 _mod_init_funcs 这个 section 中刚好有两个数据。
继续回到 doInitialization() 函数,在其实现中我们可以找到最终调用的方法:
1 | Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide); |
Initializer 是一个指向初始化方法的函数指针,这里的初始化方法就是上面 __mod_init_func 这个 section 中保存的函数。
我们可以通过添加 DYLD_PRINT_INITIALIZERS 环境变量在打印程序中依赖库的 initializer 方法:

从打印中可以看到最先调用 libSystem.B.dylib 的 initializer :
1 | **dyld: calling initializer function 0x7fff780ee94c in /usr/lib/libSystem.B.dylib** |
可以从这里找到 libSystem 的 initializer 的完整实现。这里截取了部分代码:
1 | _libkernel_init(libkernel_funcs); |
这里我们只要关注一下 libdispatch_init() 函数。因为 libdispatch_init() 函数最终调用了 runtime 的初始化方法 _objc_init。我们可以打个符号断点来验证一下:

这里可以看到 _objc_init 调用的顺序,先 libSystem_initializer 调用 libdispatch_init 再到 _objc_init 初始化 runtime。
这样从这里找到 libdispatch_init() 函数的实现。其中有这么几个函数调用:
1 | _dispatch_hw_config_init(); |
我们可以在 _os_object_init() 函数实现中发现确实调用了 _objc_init():
1 | void |
这个时候 runtime 被初始化了。
查找入口点并返回
这一步调用主程序的 getEntryFromLC_MAIN(),从加载命令读取 LC_MAIN入口,如果没有 LC_MAIN 就调用 getEntryFromLC_UNIXTHREAD() 读取LC_UNIXTHREAD,找到后就跳到入口点指定的地址并返回。
至此,整个dyld的加载过程就完成了。