上一篇文章我们讲到了启动优化需要的一些理论知识,这篇文章我们讲一下从exec()到main()系统帮我们做了哪些操作。
什么是exec()?
exec()函数是一个系统调用,当启动一个应用程序的时候,系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用了ASLR),并将起始位置到0x000000这段范围的进程权限都标记为不可读写不可执行。
- 如果是32位进程,这个范围至少是4KB。
- 如果是64位进程,至少是4GB。
- 可以捕捉任何空指针引用。
- 捕捉任何指针截断。
关于Dylibs
首先,内核加载动态链接库的帮助程序Dyld,让Dyld来启动应用的进程。Dyld的工作是:
- 加载所有依赖的Dylib。
- 它拥有和应用一样的权限。
Dyld的加载步骤
加载dylibs
- 从主执行文件的header获取需要加载的依赖库列表。
- 找到动态库的Mach-O文件。
- 打开和开始运行每一个文件。
- 确保它是mach-o文件。
- 找到代码签名并将其注册到内核。
- 然后在该dylib的每个segment上调用mmap()。
应用所依赖的dylib文件可能会依赖其它的dylib,所以dyld加载dylib的过程是一个递归的调用过程。
- 加载应用程序所有的直接依赖项
- 加载每个dylib所依赖的dylib
- 清洗和重复
- 应用一般会加载100-400个dylib文件。
- 大部分都是系统的dylib。
- 系统的dylib会预加载和缓存那些dyld要做的工作,加载速度很快。
修复(Fix-ups)
在加载完所有依赖的dylib后,它们是彼此独立的,我们需要将它们绑在到一起,这个过程就是修复。因为代码签名的存在,我们无法修复指令,那么就不能让一个dylib调用另一个dylib,这时需要加载更多的中间层。
现代的code-gen被称为动态PIC(位置无关代码),可以加载到该地址上,并且是动态的,也就是说地址被间接的分配了。当调用发生时,code_gen会在__DATA段中创建一个指向被调用者的指针,然后加载该指针并跳转过去。
所以dyld做的事情就是修正(fix-up)指针和数据,Fix-up有两种类型,rebasing(重设地址)和binding(绑定)。
Rebasing指的是在镜像内调整指针,Binding指的是在镜像外调整指针。
可以在任何二进制文件上使用dyldinfo指令来查看所有的修复:
Rebasing
在过去,dyld会把dylib加载到指定的地址,所有指针和数据对于代码来说都是对的,dyld无需做任何fix-up。如今使用了ASLR会将dylib加载到新的随机地址,这个随机地址跟代码和数据指向的旧地址会有偏差,dyld需要修正这个偏差(slide),Rebasing就是将dylib内部的指针地址都加上这个偏移量,计算方法如下:
Slide = actual_address - preferred_address
当我们重设地址时,实际上在所有的Data页面上都产生了错误,然后对页面进行修改,就会产生COW(写入时复制),所以重设地址有时会非常昂贵。这可能会产生I/O瓶颈,但因为rebase的顺序是按地址排列的,所以从内核的角度来看这是个有次序的任务,它会进行预读,减少I/O消耗。
Binding
Binding是处理那些指向dylib外部的指针,这些指针通过名称进行绑定,实际上就是个字符串。运行时,dyld需要找到该符号指针的位置,这需要计算,遍历查找符号表,一旦找到,就将该值存储到数据指针里。计算复杂度比Rebasing要高的多,但是I/O很少,因为Rebasing完成了大部分I/O操作。ObjC
- 大多数ObjC的设置都是通过rebasing和binding来完成的,比如Class中指向超类的指针和指向方法的指针。
- ObjC是个动态语言,可以用类的名字来实例化一个类的对象。这意味着ObjC运行时需要维护一张映射类名与类的全局表。当加载dylib时,其定义的所有类都需要注册到这个全局表中。
- C++中有个问题叫做脆弱的基类问题。ObjC就没有这个问题,因为会在加载时通过fix-up动态类中改变实例变量的偏移量。
- 可以定义类别,改变另一个类中的方法。
- 选择器是唯一的。
Initializers
- C++编译器生成初始化器来完成那些抽象DATA的初始化。
- ObjC中通过+load方法来完成该操作,但不建议使用+load方法。如果有+load方法,此时开始运行。
- 所有的dylib都需要运行初始化器。
- 从下往上开始运行初始化器,这样可以保证很安全的调用所依赖的内容。
- 所有的初始化器调用完成之后,Dyld就会调用可执行文件的main()函数。
下期预告:iOS启动优化实践篇
关注公众号,获取更多文章内容