on
Ios应用程序启动全过程解析
在iOS应用启动的时候,我们都知道入口函数是main.m
的main方法:
int main(int argc, char * argv[]) {...}
但是这只是从应用层面上的入口,在这之前,系统加载的过程是非常值得我们了解和运用的
当应用程序被打开的时候,kernel就会将应用程序加载到内存中,然后kernel会加载另一个程序,就是我们的dyld(动态连接器),不论是我们的应用还是dyld都是一个可执行的程序,在Mac OS和iOS上称作Mach-O
Mach-O
简单介绍下Mach-O
文件内容,就拿我们的app来说,找到编译后的xxx.app
文件,这里是Test.app
,包内有个Test
执行文件,就是我们要找的Mach-O
文件,这里使用MachOView
这个软件来打开它,看看内部结构
从图中可以看到,大致分成了下面三部分(segment),其他的部分这里先不说
|------------|
| __TEXT | 只读
|------------|
| |
| __DATA | 读写
|------------|
| |
| __LINKEDIT | 只读
| |
|------------|
- __TEXT: 只读区域,包含了Mach Header,机器指令,只读的常量,执行代码等
- __DATA: 读写区域,包含了全局,静态变量,rebase的指针(后面会讲)
- __LINKEDIT: 只读区域,包含了如何加载程序,页面的Hash值(在内容签名的时候生成的),方法的参数名和地址等
其中每个区域都有多个页结构,因为iOS中的内存使用虚拟内存,使用了页式存储,在加载的时候也是需要按照页来加载,每个页的大小在arm64上是16K,其他的是4K大小
虚拟内存
提到虚拟内存,虽然作者没深入过,但是参考文档来的内容需要说下关于应用启动有几个点需要提一下:
基本原理: 由于物理内存有限,将物理内存映射成更大的虚拟内存,每个进程有独立的虚拟内存,需要进行文件操作的时候才真正计算出物理内存地址进行读写.
- 在iOS中也是将Mach-O文件布局在虚拟内存中,使用页式管理虚拟内存
- 通过缺页中断来与物理内存读写
- 页可共享在多个进程中
- 采取了ASLR,对缓冲区溢出的安全保护技术,加载到虚拟内存页中的应用程序地址被随机化了,所以后面的dyld需要创建访问指针
- 采取了代码签名,为每一页内容进行Hash签名
下面是虚拟内存加载我们的应用的内存模型:
|------------| 0x000000
| PAGEZERO | --> 防止NULL指针, 不可读写和执行
|------------| 0x???000
| App | --> app
|------------| 0x???000
| A.dylib | --> app依赖的所有动态库
|------------| 0x???000
| dyld | --> 负责加载所有动态库
|------------|
一个dylib加载到内存的过程大致可以分成:
-> 加载Mach-O header :里面包含应用程序的说明信息
-> 加载LINKEDIT的内容 :包含如何加载数据内容及方法名和地址
-> 加载DATA数据 :加载数据到__DATA段,进行读写
-> 卸载LINKEDIT内容 :因为加载完数据后,这部分内容不需要再使用了,可以卸载掉
说了这么多,还是没进入主题??…
下面就涉及到了我们的主角: dyld程序,万幸这是个开源程序,在这里看源码
dyld的工作
应用程序在main()
之前所做的工作大致可以分成几个步骤:
- kernel准备环境,将应用程序装载到内存中
- kernel装载dyld程序,并调用dyld进行应用初始化操作
- load dylibs 根据Mach-O的
Load Commands
段加载所有依赖的动态库 - rebase 将经过ASLR随机偏移过的访问首地址创建偏移指针并存储到__DATA段,方便后续访问使用
- bind 将应用程序内调用外部的指令绑定起来
- Objc 调用Objc的runtime环境并初始化,处理category和调用
+load()
方法 - initializers 调用所有动态库的initializer方法,初始化动态库
- call main() 调用app入口函数
main
我们先在工程中断点打在_objc_init
方法,来看一下dyld做的事情
dyld启动
dyld的执行从dyldStartup.s
汇编文件开始,
...
__dyld_start:
popq %rdi # param1 = mh of app
pushq $0 # push a zero for debugger end of frames marker
movq %rsp,%rbp # pointer to base of kernel frame
andq $-16,%rsp # force SSE alignment
subq $16,%rsp # room for local variables
# call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
movl 8(%rbp),%esi # param2 = argc into %esi
leaq 16(%rbp),%rdx # param3 = &argv[0] into %rdx
movq __dyld_start_static(%rip), %r8
leaq __dyld_start(%rip), %rcx
subq %r8, %rcx # param4 = slide into %rcx
leaq ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
leaq -8(%rbp),%r9
call __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
movq -8(%rbp),%rdi
...
汇编实在看不懂,但是从代码片段中的注释也能看得出来,这里调用了dyldbootstrap::start
方法
dyldbootstrap::start
方法进行了dyld自身的初始化操作(rebase, initialize),然后调用dyld::_main
方法开始对app的工作
dyld::_main
方法就进行了加载dylibs,rebase,bind等操作,这个方法内容太多,详细的可以去读源码,这里简要标注出它做了哪些事情,省略大部分代码:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
// 1.检查如果在模拟器中运行,就是用模拟器的dyld程序
result = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue);
// 2.设置ImageLoader的context
setContext(mainExecutableMH, argc, argv, envp, apple);
// 3.初始化各种存储变量
// make initial allocations large enough that it is unlikely to need to be re-alloced
sAllImages.reserve(INITIAL_IMAGE_COUNT);
sImageRoots.reserve(16);
sAddImageCallbacks.reserve(4);
sRemoveImageCallbacks.reserve(4);
sImageFilesNeedingTermination.reserve(16);
sImageFilesNeedingDOFUnregistration.reserve(8);
// 4.加载Image(代表镜像)
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
// 5.如果有插入的库,加载
loadInsertedDylib(*lib);
// 6.link链接
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
// 7.初始化调用image的初始化方法
initializeMainExecutable();
// 8.找到`main()`函数地址返回
result = (uintptr_t)sMainExecutable->getThreadPC();
return result;
}
ImageLoader
ImageLoader负责将每一个image加载到内存中,经过了一系列的调用过程,进行了
runInitializers -> processInitializers -> recursiveInitialization -> doInitialization -> doImageInit -> doModInitFunctions
之后,完成了
- 循环加载image
- 调用image的初始化方法
- 通知注册者加载完成的事件
其中调用image的初始化方法其实是调用了C++的构造方法,也就是被static __attribute__((constructor))
修饰的方法,这一点可以从加载libSystem.dylib
包中的libSystem_initializer
方法看到,libSystem.dylib
的源码在[这里]
(https://opensource.apple.com/source/Libsystem/Libsystem-169.3/)
在libSystem.dylib
的init
方法中初始化了多了dylib库,比如:liblaunch.dylib
,libc.a
,libdispatch.a
等
load 方法何时被调用?
从上面的堆栈图可以看到,进入_objc_init
方法是在doModInitFunctions
之后,这个时候ImageLoader会调用所有image的init方法,其中就包含了libObjc.dylib
包,libdispatch_init
方法被调用
void libdispatch_init(void)
{
...
// libDispatch.dylib初始化方法
_os_object_init();
...
}
void _os_object_init(void)
{
// 调用了libObjc.dylib初始化方法
_objc_init();
...
}
这里转到了libObjc.dylib
库中,这里就包含了runtime的和load
方法调用等初始化等操作,我们继续进入objc源代码看一下_objc_init
方法实现
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
}
可以看到最后_dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
想法dyld注册了回调函数,当imagemap
到内存中,当初始化完成image时和卸载image的时候都会回调注册者,load_images
函数:
void load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
在prepare_load_methods
方法中对load
方法进行了前期准备:
- 遍历所有的
load
方法,确保父类先于子类的顺序加载到一个类的load
方法list
中 - 遍历所有的category类,将
load
方法添加一个类目的load
方法list
中
load方法加载顺序: 父类 > 子类 > 类目(category)
在call_load_methods
方法中只做了一件事情: 先调用类的load
方法list
中所有方法, 然后调用类目的load
方法list
中的所有方法
总结:load方法在libObjc.dyld库初始化的时候调用,在main函数之前调用
category的加载
从上面的_objc_init
方法中注册的dyld回调可以发现,当dyld将image拷贝到内存中(mmap
)之后会调用map_2_images
方法,然后进行了部分初始化后调用_read_images
方法
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// 1. 初始化全局存储所有类的list
gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
// 2. 从Mach-O的__DATA区 __objc_classlist 获取所有类,并加入
// gdb_objc_realized_classes list中
classref_t *classlist = _getObjc2ClassList(hi, &count);
// 3. 注册Sel,并存储到全局变量namedSelectors的list中
sel_registerNameNoLock(name, isBundle);
// 4. 找到所有Protocol并处理引用
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
// 5. 初始化类结构,插入子类
realizeClass(cls);
// 6. 开始处理category
// 6.1 找到所有category,
category_t **catlist = _getObjc2CategoryList(hi, &count);
// 6.2 处理未绑定的category,存储到静态变量category_map的list中
addUnattachedCategoryForClass(cat, cls, hi);
// 6.3 如果元类已经创建了,就开始将category的方法,属性,Protocol绑定到元类上
attachCategories(cls, cats, true /*flush caches*/);
}
在6.3
中可以看到将category的方法,属性和Protocol信息添加到元类的方列表,属性列表和Protocol列表中了,但是添加顺序是如何的呢?
例如元类是NSObject
, category是NSObject+Cat
元类中有方法列表: [A, B, C] 三个方法
category类有方法列表: [A+, B+] 两个方法
在6.1中找到的所有category,在这个时候是按照load顺序加载进来的,比如`cat3, cat2, cat1`
到6.3的时候会按照加载顺序倒序或取出来所有category的方法,属性和Protocol列表,这里保证了最后加载的category会优先绑定到元类中
在6.3中继续进行category绑定到元类中,使用了`memmov`和`memcpy`来保证元类的方法,属性和Protocol会整体放在category的相关信息之后,例如上面方法列表合并之后变成了:
> [A+, B+, A, B, C]
值得注意的点:
- category的加载是在加载完image之后,
load
方法之前 - 所有category的方法覆盖是按照加载顺序倒序的,最后加载的category的方法等优先执行
- 所有category的方法都比元类方法先执行
link
继续回到dyld的_main
函数中来,继续加载根目录Framework
目录下的其他动态库.
加载完所有的dylibs之后,每个dylib之间还是没有关联,不知道怎么调用,这时候就该进行link操作了,link操作分成rebase和binding辆部分.
rebase: 由于ASLR访问地址被随机化,所以rebase在动态库内部进行修正访问地址,并创建访问地址存储在
__DATA
段,这个期间可能会产生缺页并进行IO操作binding: 主要负责动态库之间的调用地址的修正和创建
initialize
这里就比较简单了, 这个时候各个库都已经load完成,访问地址指针也已经修正过,就可以初始化所有dylib了, 会调用C++的初始化构造方法,也就是用__attribute__((constructor))
修饰的方法,被修饰的方法都会在main()
方法之前调用
调用main()
当执行完dyld::_main
方法之后,返回了main()
函数地址,这个时候所有初始化工作都已经完成了,正式进入Objc声明周期
总结
在main()函数之前,kernel和dyld已经做了非常多的底层工作来保证程序正常运行,大致可以总结成以下步骤:
- 解析image
- 映射image到虚拟内存
- rebase image
- bind image
- 初始化所有dylib
- 调用main()
其中在Objc这一层面上还有category的处理,类的结构处理和runtime环境的初始化等等工作,值得我们细致研究
参考
对于debug:在Scheme中添加环境变量
DYLD_PRINT_STATISTICS: 分析main方法之前的加载时间
DYLD_PRINT_INITIALIZERS : 打印初始化的dylib路径
DYLD_PRINT_ENV: 打印环境
dyld源码: https://github.com/opensource-apple/dyld
wwdc: https://developer.apple.com/videos/play/wwdc2016/406
Mach-O: https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics