iOS 程序 main函数之前发生什么

主要参考:
iOS程序启动->dyld加载->runtime初始化 过程
iOS 程序 main 函数之前发生了什么

image.png

一个iOS Appmain函数位于main.m中,这是我们熟知的程序入口。但对objc了解更多之后发现,程序在进入我们的main函数前已经执行了很多代码,比如熟知的+load方法等。

简单总结

  • 系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境。

  • 开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。

  • 当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类机构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到程序入口main函数。

一. 从dyld开始

Mach-O文件

Mach-O文件格式是OS XiOS系统上的可执行文件格式,像我们编译过程产生的.O文件,以及程序的可执行文件,动态库等都是Mach-O文件,它的结构如下:

image.png
  • Header: 保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等。

-LoadCommands: 可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。

-Data:这里包含了具体的代码、数据等。

我们可以通过Mach-O文件查看器MachOView查看一个项目编译后的可执行文件内容:

Mach-O文件内容.png

可以看出:

  • dyld的路径在LC_LOAD_DYLINKER命令里,一般都是在/usr/lib/dyld路径下。
  • LC_MAIN指的是程序main函数加载地址
  • LC_LOAD_DYLIB指向的都是程序依赖库加载信息。
  • 如果我们程序使用到AFNetworking,这里就会多出一条名LC_LOAD_DYLIB(AFNetworking)的命令。如下图:
三方库.png

可以看出我们比较常用的三方库: AFNetworking,IQKeyboard等。

系统加载程序可执行文件后,通过分析文件来获得dyld所在路径来加载dyld,然后就将后面的事情交给dyld.

动态链接库

iOS 中用到的所有系统framework都是动态链接的,类比成插头和插排,静态链接的代码在编译后的静态链接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态链接需要在程序启动时有需要再去完成插好相关的插头和插排,所以在我们写的代码执行前,动态连接器需要完成准备工作。

这个是在Xcode中看到的Link列表:

image.png

这些framework将会在动态连接过程中被加载,另外还有隐含link的framework,可以测试出来:先找到可执行文件,我这里叫TestMain的工程,模拟器路径下找到TestMain.app,可执行文件默认同名,在通过otool命令:

$ otool -L TestMain

-L参数打印出所有linkframework(去掉了版本信息如下)

TestMain:
    /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics
    /System/Library/Frameworks/UIKit.framework/UIKit
    /System/Library/Frameworks/Foundation.framework/Foundation
    /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
    /usr/lib/libobjc.A.dylib
    /usr/lib/libSystem.dylib

出了多了CoreFoundation(被UIKit依赖)外,有两个默认添加的lib: libobjcobjcruntimelibSystem中包含了很多系统级别的lib,列几个熟知的。

- libdispatch(GCD)
- libsystem_c(C语言库)
- libsystem_blocks(Block)
- libCommonCrypto(加密库,比如常用的md5)

这些lib都是dylib格式相当于windows中的dll,系统使用动态链接好处:

  • 代码共用: 很多程序都动态链接了这些lib,但是它们在内存和磁盘中只有一份

  • 易于维护:由于被依赖的lib是程序执行时才link的,所以这些lib很容易做更新,比如libSystem.dyliblibSystem.B.dylib的替身,哪天想升级直接换成libSystem.C.dylib然后再替换替身就可以

  • 减少可执行文件体积,相比静态链接,动态链接在编译时不需要打包进去,所以可执行文件的体积要小很多。

dyld

dyld(the dynamic link editor), Apple 的动态链接器,系统kernel做好启动程序的初始准备后,交给dyld负责,dyld作用顺序的概括:

1. 从kernel留下的原始调用栈引导和启动自己
2. 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
3.non-lazy符号立即link到可执行文件,lazy的存表里
4.Runs static initializers for the executable
5. 找到可执行文件的main函数,准备参数并调用
6. 程序执行中负责绑定lazy符号、提供runtime dynamic loading services、提供调试器接口。
7. 程序main函数return后执行static terminator
8. 某些场景下main函数结束后调libSystem的_exit函数。

由于dyld是开源的,我们可以看到dyldStartup.s这个文件,其中用汇编实现名为_dyld_start的方法,汇编太生涩,它主要做了这件事:

1. 调用dyldbootstrap::start()方法(省去参数)
2.上一个方法返回了main函数地址,填入参数并调用main函数。

这个步骤可以通过设置一个符号断点断在_objc_init

image.png

这个函数是runtime的初始化函数。程序运行在很早的时候断住,这时候看调用栈:

image.png

看到栈底的dyldbootstrap::start()方法,继而调用了dyld::_main()方法,其中完成了刚从说的递归加载动态库过程,由于libSystem默认引入,栈中出现了libSystem_initializer的初始化方法。

我们可以看下_main函数:

dyld::_main函数代码.png

这里的_main函数是dyld的函数,并非我们程序里的main函数。

1. sMainExecutable = instantiateFromLoadedImage(....)与loadInsertedDylib(...)

这一步 dyld将我们可执行文件以及插入的lib加载进内存,生成对应的image.

sMainExecutable对应着我们的可执行文件,里面包含了我们项目中所有新建的类。

insertDylib一些插入的库,他们配置在全局的环境变量sEnv中,我们可以在项目中设置环境变量DYLD_PRINT_ENV1,来打印该sEnv的值。

环境变量设置.png

运行log如下:

插入库log.png

可以看出插入的库为:libBacktraceRecording.dyliblibViewDebuggerSupport.

有时我们会在三方AppMach-O文件中通过修改DYLD_INSERT_LIBRARIES的值来加入我们自己的动态库,从而注入代码,hook别人的App.

2. link(sMainExecutable,...)link(image, ...)
对上面生成的image进行链接。其主要有对image进行load(加载)rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:

link方法.png
  • recursiveLoadLibraries(context, prefightOnly,loaderRPaths)
    递归加载所有依赖库进内存

-recursiveRebase(context)
递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间配置随机加载),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错,需要重新对代码地址进行计算修复才能正常访问。

  • recursiveBind(context, forceLazyBound,neverUnload)
    对库中所有nolazy的符号进行bind,一般情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind.

3.initializeMainExecutable()
这一步主要是调用所有imageinitalizer方法进行初始化。这里的initalizers方法并非名为Initalizers的方法,而是C++静态对象初始化构造器,atribute(constructor)进行修饰的方法,在LmageLoader类中initializer函数指针锁指向该初始化方法的地址。

initallizer函数指针.png

我们可以在程序中设置环境变量DYLD_PRINT_INITALIZERS1来打印出程序的各种依赖库的initializer方法。

image.png

运行程序,系统log打印如下:

Initializer调用log.png

可以看到每个依赖库对应着一个初始化方法,名称各有不同。

这里最开始调用的libSystem.dylibinitializer function比较特殊,因为runtime初始化就在这一阶段,而这个方法其实和简单,我们可以在这里看到init.c源码,主要方法如下:

libSystem_initializer.png

其中libdispatch_init里调用了到runtime初始化方法_objc_init.我们可以在程序中打个符号断点来验证。

_objc_init.png

运行程序,然后断点命中,我们来看下调用栈:


objc_init调用栈.png

我们可以看到_objc_init调用顺序,先libSystem_initializer调用libdispatch_init,再到_objc_init初始化runtime.

runtime初始化后不会闲着,在_objc_init中注册了几个同志,从dyld这里接手几个活,其中包括初始化相应依赖库里的类结构,调用依赖库里所有load方法。

就拿sMainExcuateable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用+load方法,这些目前都发生在main函数前,但是由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行。

ImageLoader

当然这个image不是图片的意思,它大概表示一个二进制文件(可执行文件或so文件),里面是被编译过的符号、代码等,所以imageLoader作用是将这些文件加载进内存,且每一个文件对应一个imageLoader实例来负责加载。

两步走:

1.在程序运行时它先将动态链接的image递归加载(也就是上面ImageLoader的递归调用)
2.再从可执行文件image递归加载所有符号

当然所有这些都发生在我们真正的main函数执行之前。

runtime 与 +load

刚才讲到libSystem是若干个系统lib的集合,所以它只是一个容器lib而已,而且它也是开源的,里面实质上就是一个文件: init.c 由libSystem_initializer逐步调用到了_objc_init,这里就是objcruntime的初始化入口。

除了runtime环境的初始化外,_objc_init中绑定了新image被加载后的callback

dyld_register_image_state_change_handler(
dyld_image_state_bound, 1, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);

可见dyld担当了runtimeimageLoader中间的协调者,当新image加载进来后交由runtime去解析这个二进制文件的符号表和代码。继续上面的断点法,断住神秘的+load函数。

image.png

清楚的看到整个调用栈和顺序:

1. dyld开始将程序二进制文件初始化

2. 交由imageLoader读取image,其中包含了我们的类,方法等各种符号

3.由于runtime向dyld绑定了回调,当image加载到内存后,dyld会通知runtime进行处理

4. runtime接手后调用map_images做解析和处理,接下来load_images中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class的+load方法和Category的+load方法。

至此,可执行文件中和动态库所有的符号(Class, Protocol,Selector,IMP,...)都已经按格式成功加载到内存中,被runtime所管理,再这之后,runtime的那些方法(动态添加Class,swizzie等等才能生效)

关于+load方法的几个QA

Q:重载自己Class+load方法需不需要调父类
A:runtime负责按继承顺序递归调用,所以我们不能调用super

Q: 在自己Class+load方法时能不能替换系统framework(比如UIKit)中某个类的方法实现
A:可以,因为在动态链接过程中,所有依赖库的类是优先于自己的类加载的

Q:重载+load时需要手动添加@autoreleasepool吗?
A:不需要,在runtime调用+load方法前后是加了objc_autoreleasePoolPush()objc_autoreleasePoolPop()的。

Q:想让一个类的+load方法被调用是否需要在某个地方import这个文件
A:不需要,只要这个类的符号被编译到最后的可执行文件中,+load方法就会被调用.

总结

  • 整个事件由dyld主导,完成运行环境的初始化后,配合ImageLoader将二进制文件按格式加载到内存

  • 动态链接依赖库,并由runtime负责加载成objc定义的结构,所有初始化工作结束后,dyld调用真正的main函数。

  • 值得说明的是,这个过程远比写出来复杂,这里只提到了runtime这个分支,还有像GCD、XPC、等重头的系统库初始化分支没有提及(当然这里还有缓存机制)

  • 总结:在main函数执行之前,系统做了茫茫多的加载和初始化工作,但是被很好隐藏了。

孤独的main函数

当所有前期初始化工作结束是,dyld会清理现场,将调用栈回归,只剩下:

image.png

孤独的main函数,看上去像是程序的开始!

推荐阅读更多精彩内容