iOS启动优化之从exec()到main()

2,120 阅读5分钟

上一篇文章我们讲到了启动优化需要的一些理论知识,这篇文章我们讲一下从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启动优化实践篇

关注公众号,获取更多文章内容