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的加载过程就完成了。