当我们对某些技术了解足够多的时候,总是希望能知道它背后的原理,iOS也是一样,我们每次使用IDE Xcode将源代码编译成可执行文件之后,然后可执行文件会通过数据线传输到iPhone,可执行文件会被系统进行加载,我们的App就run起来了。那么在可执行文件被系统加载这个阶段,从一个静态的持久化Binary文件到一个动态的进程这个过程中,一直有几层窗户纸在我们的面前:
我们的App肯定是跑在一个进程上,那么这个进程是被谁创建的,进程又是如何分配内存的?
我们的可执行文件是如何被系统识别和读取的,又是如何被映射进内存的?
我们都知道UIKit
、Foundation
这些系统库都是动态库,那么这些动态库又是如何被加载进内存的,不同App之间又是如何实现共享的呢?
我们运行时如果动态来加载一个动态库,这时的动态库又是如何被加载进当前的进程呢?
系统又是如何对持久化Binary和动态库进行签名检测的呢?
是的,我们本次的源码分析,就是为了捅破这些窗户纸,希望能够从系统层面理解Mach-O
和动态库的加载过程,让我们对iOS能有一次更深层次的挖掘。本次XNU
、dyld
源码分析由于篇幅比较长,会分为两篇,第一篇为XNU
源码分析,第二篇为dyld
源码分析。
由于本次源码分析中会涉及到一些名词,这些名词可能在平时普通业务开发中不太会接触到,我们在这里先进行一下名词解释,希望能够帮助更多的iOS同学完全理解本次源码分析。
Mach
: Mach
是一个微内核,主要实现了基本的进程、虚拟内存管理、任务调度、进程通信和消息机制。
Mach-O
: Mach-O
是iOS、MacOS、WatchOS等苹果系操作系统的可执行文件格式,Mach-O
文件格式大家可以自行搜索一下,有较多进行分析的文章,在此不再展开。
BSD
: 最早BSD
(Berkeley Software Distribution,伯克利软件套件)是Unix的衍生系统,现在BSD
并不特指任何一个BSD
衍生版本,而是类Unix操作系统中的一个分支的总称,iOS中的BSD
是指对Mach
层的封装和扩展,它提供了更现代API和对POSIX
的兼容性,比如Mach
层fork
、vfork
可用来创建进程,而BSD
层则定义了posix_spawn
来进行进程创建,还有进程结构proc
,这是BSD层的进程结构,扩展了Mach
层的task
进程结构 也就是说,task
结构是proc
结构的一部分。
POSIX
: POSIX
表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX
),POSIX
标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称。
XNU
: 最底层包括Mach
、BSD
、主要基于开源技术的驱动器等组成了XNU
。
Darwin
: Darwin
是苹果操作系统内核的内部代号, 由XNU
和Darwin
库等组成。
dyld
: dyld
是一个动态库加载器,负责将动态库加载进内存。
dlopen
: dlopen
函数是以指定模式打开指定的动态库文件,并返回一个句柄给调用进程,dlopen
调用成功的话,指定的动态库就会被加载进内存中。
mmap
: mmap
函数是指将一个文件或者其它对象映射进内存。
虚拟内存
: 我们开发者开发过程中所接触到的内存均为虚拟内存,虚拟内存使App认为它拥有连续的可用的内存(一个连续完整的地址空间),这是系统给我们的馈赠,而实际上,它通常是分布在多个物理内存碎片,系统的虚拟内存空间映射vm_map
负责虚拟内存和物理内存的映射关系。
ASLR
: ASLR
(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度。
用户态和内核态
: 出于安全考虑,需要限制不同程序之间的访问能力、获取其他程序的数据、防止访问外围设备等,所以区分了用户态和内核态,用户态只能受限的访问内存, 且不允许访问外围设备等,内核态以访问内存所有数据, 包括外围设备等,用户态如果需要做一些内核态的事情,需要通过系统调用机制从用户态切换到内核态。
在目前已知的iOS系统中我们经过测试,了解到了以下一些情况:
iOS10
之前相同签名的动态库和App,在我们日常开发中动态库即使是在Document
目录也能被正常映射为可执行内存,当然在Appstore下发的App苹果会对其进行签名和加密,我们无法伪造苹果的签名,所以我们无法将MainBundle
外的动态库映射为可执行内存。
iOS10
(包含)之后的App即使是在Document
目录的动态库也无法被加载进内存,只能加载MainBundle
内的动态库,即在/XXX.app
目录中的动态库。
如果在iOS10
之前的情况下想要加载MainBundle
外的动态库映射为可执行内存,需要绕过苹果的签名检测过程,iOS10
之后的话还要绕过MainBundle
以外目录加载动态库的权限问题。
同时我们也会有一些疑问:
当我们启动一个App的时候,系统内核是如何加载这个Mach-O
的,Mach-O
的各个segment
是如何映射可执行内存的。
用户态的dyld
又是如何加载进虚拟内存空间的,为何同是用户态,dyld
是可以通过mmap
来分配一个动态库的可执行内存,我们的App为何却不能分配可执行内存呢。
一个App的虚拟内存分布大概是怎样的,PAGEZERO
、ASLR
等又是在什么阶段来控制虚拟内存分布的,共享动态库又是如何实现不同App之间共享的。
当我们经过dlopen
来加载一个动态库的时候,系统层面到底发生了什么,保证将合法的动态库加载进当前进程的的可执行虚拟内存空间的,沙盒权限和签名是如何进行验证的。
带着这些已知情况和疑问,我们一起来分析一下XNU
和dyld
的源码,找寻一下Mach-O
和动态库的加载过程,把上述疑问进行一一揭秘。
首先,MacOS X中的X指的就是XNU
,XNU
代表的含义是X is Not Unix,它是基于Unix的一个内核,或者可以说XNU
中的Mach
是基于Unix
的一个内核,上层封装了BSD
和其他的库组成了Darwin
,Darwin
上层还依次有核心框架层(Open GL等)、应用框架层(Cocoa等)、用户体验层(Springboard)。
OSX/iOS
系统架构图
XNU
主要由4部分组成:
Mach
Mach
是一个微内核,主要实现了基本的进程、虚拟内存管理、任务调度、进程通信和消息机制。
BSD
对Mach
层的封装和扩展,名词解释中已经有过解释,在此不再展开。
libkern
libkern
是IOKit
的驱动程序。
IOKit
IOKit
是设备驱动程序运行时环境,比如设备的电源信息、内存信息、CPU信息等都是在IOKit
进行管理的。
XNU
非常复杂和优秀,在Unix的基础上,做了很多创新和改进,我们可以另起一篇文章给大家对XNU
做一下系统性的介绍,在此我们只讨论和Mach-O
、dyld
加载相关的流程,文中XNU
源码版本为xnu-3789.51.2
。
接下来我们来分析一下每一个流程中的源码,首先我们来看load_init_program
,load_init_program
函数负责加载系统初始化的进程,非debug
模式下,只会加载launchd
,launchd
是一个负责进程管理的后台守护进程daemon,我们来看一下它的相关源码。
init_programs
数组又是什么呢,我们可以看它的定义。
load_init_program_at_path
会调用__mac_execve
,__mac_execve
函数会启动新进程和task,调用exec_activate_image
,上层也可直接调用posix_spawn
生成新进程,posix_spawn
会自动调用exec_activate_image
。
exec_activate_image
函数会负责按照binary的格式分发映射内存的函数,目前格式有三种:单指令集binary、Fat binary、shell 脚本,如果是Fat binary,会先进行指令集级别Mach-O
的分解,然后再循环调用execsw来分发进行映射内存。
execsw
结构体和数组定义:
exec_mach_imgact
函数主要完成了以下几个过程:
为vfork
生成新的线程(vfork
生成进程不会生成线程)
把Mach-O
映射进内存
签名、uid等权限处理,dyld
相关的处理工作
释放资源
load_machfile
函数负责除了macho-o解析之外所有和加载相关的工作。
为当前task
分配可执行内存,task
是一个任务实例,负责进程内的虚拟内存空间,线程管理等工作。
Mach-O
和dyld
ASLR的随机
为exec_mach_imgact
回传结果
上面流程中有一个创建虚拟内存映射的函数vm_map_create(pmap,0,vm_compute_max_offset(result->is64bit),TRUE);
创建了一个0 - max_offset
大的空间,也就是一个App最大的虚拟内存空间,其中 vm_compute_max_offset
的具体实现是:
ARM处理器64bit
的架构情况下,MACH_VM_MAX_ADDRESS
的定义在mach/arm/vm_param.h
头文件中:
也就是0x000000000 - 0xFFFFFFFFF
,每个16进制数是4位,即2的36次幂,就是64GB,即App最大的虚拟内存空间为64GB。
接下来我们继续回到主流程,parse_machfile
函数主要完成了以下几个工作:
Mach-o
的解析,相关segment
虚拟内存分配
dyld
的加载
dyld
的解析及虚拟内存分配
load_dylinker
函数主要负责dyld
的加载,解析等工作.
到这里,我们就完成了一个Mach-O
及其dyld
加载过程的分析,大体流程其实很简单,创建进程、创建虚拟内存空间、解析和映射Mach-O
、解析映射dyld
,上文中的分析可能需要大家根据源码和注释进行反复多次理解,才能真正理解整个流程和这个流程中的细节。
文中我们提到了许多虚拟内存分布相关的东西,这时候我们需要做一些这方面的总结了,来看一下我们的App大体的内存分布应该是怎样的。
文中所提的内存均为虚拟内存,虚拟内存的名字在文章开头已经解释,共享动态库其实就是共享的物理内存中的那份动态库,App虚拟内存中的共享动态库并未真实分配物理内存,使用时虚拟内存会访问同一份物理内存达到共享动态库的目的,iPhone7 PLUS(之前的产品最大为2GB)的物理内存RAM也只有3GB,那么超过3GB的物理内存如何处理呢,系统会使用一部分硬盘空间ROM来充当内存使用,在需要时进行数据交换,当然磁盘的数据交换是远远慢于物理内存的,这也是我们内存过载时,App卡顿的原因之一。
以ARM64为例,根据上面XNU
的分析中App最大的虚拟内存空间为64GB,一个默认的PAGEZERO
捕获空指针异常区4GB,Mach-O
和dyld
的ASLR,共享动态库的随机slide(在dyld
的源码分析中我们会具体讲到共享动态库是如何进行加载的)等,我们大体可以得出一个App的虚拟内存分布:
好了,到这里我们已经分析完毕XNU
加载Mach-O
和dyld
的流程,那么dyld
又是如何加载和链接Mach-O
相关的动态库的呢?且听下回分解。
如对滴滴平台技术部的机会感兴趣,请发送简历至我们的招聘HR maosanyan@didichuxing.com ,或加微信&电话联系(18910686942)。