33 | iOS 系统内核 XNU:App 如何加载?
下载APP
关闭
渠道合作
推荐作者
33 | iOS 系统内核 XNU:App 如何加载?
2019-05-25 戴铭 来自北京
《iOS开发高手课》
课程介绍
讲述:冯永吉
时长11:56大小10.90M
你好,我是戴铭。
在专栏的第 2 篇文章《App 启动速度怎么做优化与监控?》更新完之后,我看到很多同学对启动加载 App 的底层原理表示出了浓厚兴趣。所谓工欲善其事,必先利其器,相信有着好奇心的你,一定也会对支撑着 App 运行的操作系统有着各种各样的疑问。
所以,今天我会先跟你说说 iOS 系统的架构是怎样的,各部分的作用是什么,帮助你理解 iOS 系统的原理,进而更全面地理解它在 App 加载时做了哪些事情?
接下来,我就先跟你聊聊 iOS 的系统架构是怎样的。在理解 iOS 系统架构之前,你最好掌握一些操作系统原理的基础知识。
iOS 系统架构
iOS 系统是基于 ARM 架构的,大致可以分为四层:
最上层是用户体验层,主要是提供用户界面。这一层包含了 SpringBoard、Spotlight、Accessibility。
第二层是应用框架层,是开发者会用到的。这一层包含了开发框架 Cocoa Touch。
第三层是核心框架层,是系统核心功能的框架层。这一层包含了各种图形和媒体核心框架、Metal 等。
第四层是 Darwin 层,是操作系统的核心,属于操作系统的内核态。这一层包含了系统内核 XNU、驱动等。
图 1 iOS 系统架构
其中,用户体验层、应用框架层和核心框架层,属于用户态,是上层 App 的活动空间。Darwin 是用户态的下层支撑,是 iOS 系统的核心。
Darwin 的内核是 XNU,而 XNU 是在 UNIX 的基础上做了很多改进以及创新。了解 XNU 的内部是怎么样的,将有助于我们解决系统层面的问题。
所以接下来,我们就一起看看 XNU 的架构,看看它的内部到底都包含了些什么。
XNU
XNU 内部由 Mach、BSD、驱动 API IOKit 组成,这些都依赖于 libkern、libsa、Platform Expert。如下图所示:
图 2 XNU 系统架构
其中,Mach是作为 UNIX 内核的替代,主要解决 UNIX 一切皆文件导致抽象机制不足的问题,为现代操作系统做了进一步的抽象工作。 Mach 负责操作系统最基本的工作,包括进程和线程抽象、处理器调度、进程间通信、消息机制、虚拟内存管理、内存保护等。
进程对应到 Mach 是 Mach Task,Mach Task 可以看做是线程执行环境的抽象,包含虚拟地址空间、IPC 空间、处理器资源、调度控制、线程容器。
进程在 BSD 里是由 BSD Process 处理,BSD Process 扩展了 Mach Task,增加了进程 ID、信号信息等,BSD Process 里面包含了扩展 Mach Thread 结构的 Uthread。
Mach 的模块包括进程和线程都是对象,对象之间不能直接调用,只能通过 Mach Msg 进行通信,也就是 mach_msg() 函数。在用户态的那三层中,也就是在用户体验层、应用框架层和核心框架层中,你可以通过 mach_msg_trap() 函数触发陷阱,从而切至 Mach,由 Mach 里的 mach_msg() 函数完成实际通信,具体实现可以参看 NSHipster 的这篇文章“Inter-Process Communication”。
每个 Mach Thread 表示一个线程,是 Mach 里的最小执行单位。Mach Thread 有自己的状态,包括机器状态、线程栈、调度优先级(有 128 个,数字越大表示优先级越高)、调度策略、内核 Port、异常 Port。
Mach Thread 既可以由 Mach Task 处理,也可以扩展为 Uthread,通过 BSD Process 处理。这是因为 XNU 采用的是微内核 Mach 和 宏内核 BSD 的混合内核,具备微内核和宏内核的优点。
微内核可以提高系统的模块化程度,提供内存保护的消息传递机制;
宏内核也可以叫单内核,在出现高负荷状态时依然能够让系统保持高效运作。
Mach 是微内核,可以将操作系统的核心独立在进程上运行,不过,内核层和用户态各层之间切换上下文和进程间消息传递都会降低性能。为了提高性能,苹果深度定制了 BSD 宏内核,使其和 Mach 混合使用。
宏内核 BSD 是对 Mach 封装,提供进程管理、安全、网络、驱动、内存、文件系统(HFS+)、网络文件系统(NFS)、虚拟文件系统(VFS)、POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)兼容。
早期的 BSD 是 UNIX 衍生出的操作系统,现在 BSD 是类 UNIX 操作系统的统称。XNU 的 BSD 来源于 FreeBSD 内核,经过深度定制而成。IEEE 为了保证软件可以在各个 UNIX 系统上运行而制定了 POSIX 标准,iOS 也是通过 BSD 对 POSIX 的兼容而成为了类 UNIX 系统。
BSD 提供了更现代、更易用的内核接口,以及 POSIX 的兼容,比如通过扩展 Mach Task 进程结构为 BSD Process。对于 Mach 使用 mach_msg_trap() 函数触发陷阱来处理异常消息,BSD 则在异常消息机制的基础上建立了信号处理机制,用户态产生的信号会先被 Mach 转换成异常,BSD 将异常再转换成信号。对于进程和线程,BSD 会构建 UNIX 进程模型,创建 POSIX 兼容的线程模型 pthread。
iOS 6 后,为了增强系统安全,BSD 实行了 ASLR(Address Space Layout Randomization,地址空间布局随机化)。随着 iPhone 硬件升级,为了更好地利用多核,BSD 加入了工作队列,以支持多核多线程处理,这也是 GCD 能更高效工作的基础。 BSD 还从 TrustdBSD 引入了 MAC 框架以增强权限 entitlement 机制的安全。
除了微内核 Mach 和宏内核 BSD 外,XNU 还有 IOKit。IOKit 是硬件驱动程序的运行环境,包含电源、内存、CPU 等信息。IOKit 底层 libkern 使用 C++ 子集 Embedded C++ 编写了驱动程序基类,比如 OSObject、OSArray、OSString 等,新驱动可以继承这些基类来写。
了解了 XNU 后,接下来,我再跟你聊聊 XNU 怎么加载 App 的?
XNU 怎么加载 App?
iOS 的可执行文件和动态库都是 Mach-O 格式,所以加载 APP 实际上就是加载 Mach-O 文件。
Mach-O header 信息结构代码如下:
如上面代码所示,包含了表示是 64 位还是 32 位的 magic、CPU 类型 cputype、CPU 子类型 cpusubtype、文件类型 filetype、描述文件在虚拟内存中逻辑结构和布局的 load commands 数量和大小等文件信息。
其中,文件类型 filetype 表示了当前 Mach-O 属于哪种类型。Mach-O 包括以下几种类型。
OBJECT,指的是 .o 文件或者 .a 文件;
EXECUTE,指的是 IPA 拆包后的文件;
DYLIB,指的是 .dylib 或 .framework 文件;
DYLINKER,指的是动态链接器;
DSYM,指的是保存有符号信息用于分析闪退信息的文件。
加载 Mach-O 文件,内核会 fork 进程,并对进程进行一些基本设置,比如为进程分配虚拟内存、为进程创建主线程、代码签名等。用户态 dyld 会对 Mach-O 文件做库加载和符号解析。
苹果公司已经将 XNU 开源,并在 GitHub 上创建了镜像。要想编译 XNU,你可以查看“Building the XNU kernel on Mac OS X Sierra (10.12.X)”这篇文章;要想调试 XNU,可以查看“Source Level Debugging the XNU Kernel”这篇文章。
整个 fork 进程,加载解析 Mach-O 文件的过程可以在 XNU 的源代码中查看,代码路径是 darwin-xnu/bsd/kern/kern_exec.c,地址是https://github.com/apple/darwin-xnu/blob/master/bsd/kern/kern_exec.c,相关代码在 __mac_execve 函数里,代码如下:
可以看出,由于 Mach-O 文件很大, __mac_execve 函数会先为 Mach-O 分配一大块内存 imgp,接下来会初始化 imgp 里的公共数据。内存处理完,__mac_execve 函数就会通过 fork_create_child() 函数 fork 出一个新的进程。新进程 fork 后,会通过 exec_activate_image() 函数解析加载 Mach-O 文件到内存 imgp 里。最后,使用 task_set_main_thread_qos() 函数设置新 fork 出进程的主线程。
exec_activate_image() 函数会调用不同格式对应的加载函数,代码如下:
可以看出,加载 Mach-O 文件的是 exec_mach_imgact() 函数。exec_mach_imgact() 会通过 load_machfile() 函数加载 Mach-O 文件,根据解析 Mach-O 后得到的 load command 信息,通过映射方式加载到内存中。还会使用 activate_exec_state() 函数处理解析加载 Mach-O 后的结构信息,设置执行 App 的入口点。
设置完入口点后会通过 load_dylinker() 函数来解析加载 dyld,然后将入口点地址改成 dyld 的入口地址。这一步完后,内核部分就完成了 Mach-O 文件的加载。剩下的就是用户态层 dyld 加载 App 了。
Dyld 的入口函数是 __dyld_start,dyld 属于用户态进程,不在 XNU 里,__dyld_start 函数的实现代码在 dyld 仓库中的 dyldStartup.s 文件里。__dyld_start 会加载 App 相关的动态库,处理完成后会返回 App 的入口地址,然后到 App 的 main 函数。
小结
今天我跟你介绍了 iOS 系统的内核 XNU,以及 XNU 是如何加载 App 的。总体来说,XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。流程可以概括为:
fork 新进程;
为 Mach-O 分配内存;
解析 Mach-O;
读取 Mach-O 头信息;
遍历 load command 信息,将 Mach-O 映射到内存;
启动 dyld。
课后作业
在今天这篇文章中,我主要和你分享的是系统内核加载 App 的流程,而关于用户态 dyld 加载过程没有展开说。如果你想了解 dyld 加载过程的话,可以看看 Mike Ash 的“dyld: Dynamic Linking On OS X”这篇文章。
相应地,今天的课后思考题,我希望你能够和我分享一下这篇文章的读后感。
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 4
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
32 | 热点问题答疑(三)
下一篇
34 | iOS 黑魔法 Runtime Method Swizzling 背后的原理
精选留言(14)
- bart2019-05-30推荐大家回顾一下大学的《操作系统》,然后看一下《 深入解析Mac OSX & IOS 操作系统》,这样听起来会畅快很多。29
- 毛成方2019-06-03Mike Ash文章最后总结说道大部分工程师不会去在意这些细节 但是当你在遇到动态链接等报错的时候 你讲更好去分析和解决它们。换句话说 我们要走出自己的舒适区 去研究新的技术 碰壁 总结 才能有更好的成长 去挑战更大的平台。9
- 赵国辉2019-05-29老师,有没有计划讲解一下dyld的工作过程和原理,非常想学习一下
作者回复: 可以看这篇 https://www.mikeash.com/pyblog/friday-qa-2012-11-09-dyld-dynamic-linking-on-os-x.html
4 - Geek2019-05-30读后感: 真的看不懂,iOS开发需要这么深入吗?共 3 条评论2
- Bill2019-10-09多从提到了BSD BSD全称到底是啥
作者回复: https://zh.wikipedia.org/zh/BSD
1 - xilie2019-05-29读后感: 真的看不懂,iOS开发需要这么深入吗?共 1 条评论1
- 烩面2019-05-26老师,是先 fork 出新进程,还是先分配内存呢? 小结和上面对 __mac_execve 函数的分析上好像有点出入 。。。
作者回复: fork 新进程之前内存并没有分配给新进程,是在 fork 新进程后再分配的,调用的函数是 exec_activate_image,这个函数会依据 execsw 加载二进制文件格式进行内存映射,对内存进行分配,对于 Mach-O 格式使用的是 exec_mach_imgact() 函数。文中已有说明。
1 - D2021-08-11所以App的Mach-O 文件到底是由XNU加载到内存的还是由dyld加载到内存的?以前一直认为是dyld 加载App的Mac-o 并解析加载load commands。现在越来越看不懂了...
- Wim2020-12-08大神能讲一下“点击appIcon到app启动”或者“被别的应用拉起的启动”具体的底层过程吗?
- Geek_9dbcb42020-04-18文中“加载 Mach-O 文件,内核会 fork 进程,并对进程进行一些基本设置,比如为进程分配虚拟内存、为进程创建主线程、代码签名等”。 问题,这个地方的代码签名,不同于我们证书的私钥对APP的签名吧?也不同于苹果的私钥对APP的再次签名吧?
- Geek_9dbcb42020-04-18文中“XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。”,最后这个虚拟内存空间就没有了吧?例如进程内所有的地址访问,都是真实内存地址了,是吧
- yujian2019-05-26请问:app被拒后,申诉仅解释一下,但是代码不动还用原来的ipa,想问是否还需要重新提交审核么共 1 条评论
- 凛冬一壶酒2019-05-26这个可以有 哈哈哈
- 吹啊吹吹2019-05-25帅!看来得多听几遍了