iOS XNU - 进程及 Mach-O 格式
本来小编想先把内存这块梳理下,但是联系到内存在 iOS
平台中也是 app
编译后的 Mach-O
可执行文件,所以就先把 OS
编译启动过程来讲述。
进程生命周期
因为小编是一位 iOSer
这里讲解进程可以大概理解为:进程 == App
,下面给出进程的生命周期。进程的生命周期可以分为:创建(SIDL)、运行(SRUN)、睡眠(SSLEEP)、停止(SSTOP)、退出ing(SZONE)和终止(Dead)。
创建(SIDL)
SIDL
这个状态是被父进程刚刚 Fork
创建生成一个唯一的 PID
,处于一个临时空闲状态。此时让然被称为:“正在初始化”,不会响应任何的信号和操作。这个初始化的过程是在单线程中来进行内存布局设置和加载所需要的引来模块。
实现处理完成只有进程可以执行,同时不会返回 SIDL
状态。相当与用户在手机屏幕点击 ICON
按钮后执行。
运行(SRUN)
SRUN
如果在进一步来进行细分可以分为两种:
(1)可运行状态:当一个进程被添加到运行队列在等待时期,但是因为
CPU
忙于运行其他的App
此时还没有加载改进程的寄存器时 既短暂状态。
(2)运行状态:当CPU
从运行队列中开始执行进程的寄存器,改App
也就处于运行转台。
在 App
(进程) 在执行过程中时间片用完或者是被更高优先级的进程占用时,此时会由 运行转态加入到可以运行队列中变为可运行状态。
在
iOS
中App
启动的标志如下 ==SRUN
:
//下面执行是在系统加载完 `Mach-O` 和 `dyld` 链接库之后执行
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ applicatxion: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { }
//每次 `App` 第一次加载或者从后台 --> 前台均会执行
func applicationDidBecomeActive(_ application: UIApplication) { }
}
睡眠(SSLEEP)
SSLEEP
在 App
执行过程中可能出现等待某一个资源或者是进行后台工作,这时进程就不要使用 CPU
甚至不用进入 可运行队列 而是进行睡眠。当再次获取改资源或者进行前台 通常会重新加入到可运行队列,并且当在当前运行队列之后。
这里睡眠状态也可以在结束信号时间来处理:
pid_suspend
和pid_resume
可以实现App
进行休眠和唤醒之间的切换。这个睡眠我们可以成为深度睡眠,这个操作是在我们说的Mach
层面来实现的,可以玩限次来进行切换操作。
在手机中我们采用按HOME
键就是基础此来实现,App 睡眠状态。
在应用程序从 SRUN
状态到我们所说的 SSLEEP
休眠状态过程中在 iOS
会执行如下操作:
可以文件
xxxx.plist
设置UIApplicationExitsOnSuspend
||Application does not run in background
设置对应的属性true
||false
来设置进入后台设置,在App
中按下Home
键 ==SSLEEP
。
class AppDelegate: UIResponder, UIApplicationDelegate {
//下面是在运行期间点击 `Home` 键,`App` 进行后台
func applicationWillResignActive(_ application: UIApplication) { }
func applicationDidEnterBackground(_ application: UIApplication) {
//此位置如果有需求可以实现延迟进入后台
}
//从后台进入运行状态
func applicationWillEnterForeground(_ application: UIApplication) { }
func applicationDidBecomeActive(_ application: UIApplication) { }
}
停止(SSTOP)
SSTOP
这个状态是通过一个特殊的信号TSTOP
|| TOSTOP
来时正在进行的 App
停止运行,使程序来处在 深度睡眠状态。可以通过信号 CONT
来切为可运行状态来重新被调度。
退出ing(SZONE)
在程序执行完之后运行 exit()
来对程序进行退出。
iOS
中程序的关闭 ==SZONE
//程序在关闭时 `iOS` 的函数调用
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) { }
}
终止(Dead)
在程序退出之后,也就是会终止该进程所有线程。在此之前会极端的时间出现一个僵尸状态。
僵尸状态: 进程的空壳,占用的资源全部释放,只是对应的
PID
还在占用。如果父类进行没预释放就交由父类线程,如果释放就交给其祖先进程(PID
= 1) 可以理解为操作系统来进行收养。
可执行文件
特殊文件在内存中加载执行就是我们所见到的进程,执行过程中文件中会有一个签名来表明当前文件是否有效:“魔数”。
下面给出 OS X
中对应的魔数和可执行文件类型:
执行格式 | 魔数 | 用途 |
---|---|---|
通用二进制格式 |
0xcafebabess (小尾顺序) 0xbebafeca (大尾顺序) |
包含多种架构的二进制格式,目前只在 OS X 使用。 目前我们见过一些 Framework 的库可以在 Mac 的虚拟机上运行及支持 Intel 的 i386 等,同时也支持移动端的 ARM 的 32 位和 64 位高通处理器。包体偏大。
|
Mach-O |
0xfeedface (32位) 0xfeedfacf (64位) |
OS X 原生二进制。 |
下面给出在 Fat
(通用二进制格式) VS Mach
格式头文件:
胖二进制(Fat)
fat_headers
magic
:固定值在上面给出0xcafebabe
,来表示通用二进制文件;
nfa_arch
:当前通用二进制包含的架构数目。
fat_arch
cputype
:定义的CPU
类型;
cpusubtype
:定义的机器标识符;
offset
:架构的二进制代码在整个通用二进制文件中的偏移量;
magic
:内层二进制代码大小;
magic
:对其页边界(4k),表示 2 幂函数。
在通用二进制代码加载执行时,Mach
加载器会根据自己的架构加载适合架构代码。所以不相关的架构代码不用占用相关的内存,但是加载整个 IPA
包体会比较大。这里可以采用 lipo
来对二进制文件进行阉割,lipo
阉割文件
通用二进制的镜像文件做了优化,对其页边界,内核只需加载第一页就可以读取文件。用这个文件当做目录加载合适的镜像。
Mach-O
二进制格式
mach_header
magic
:0xfeedface
表示 32 位二进制,0xfeedfacf
表示 64 位二进制文件;
cputype
&cpusubtype
:CPU
类型和子类型;
filetype
:文件类型(可执行文件、库文件、核心转储文件、内核拓展等);
ncmds
&sizeofncmds
:用于加载器的 “加载命令” 的条目和大小;
flags
:动态连接器的标志(dyld
);
Reserved
: 64 位预留。。。
我们在目前编译过程总采用 Bitcode
来进行编码实际就是生成 Mach-O
文件,然后在由 Apple Server
根据应用端的处理架构来在实现安装时生成对应架构的 Mach-O
。
Mach-O
加载进程
内核加载进程
(1) 非配虚拟内存 LC_SEGMENT
|| LC_SEGMENT_64
LC_SEGMENT
||LC_SEGMENT_64
指导内核如何设置新运行进程的内存空间,__PAGEZERO
段(空指针陷阱)、__TEXT
段(程序代码)、__DATA
段(程序数据) 和__LINKEDIT
段(连接器使用的字符和其他段) 直接从Mach-O
二进制文件加载到内存中。
LC_SEGMENT
|| LC_SEGMENT_64
的参数:
参数 | 用途 |
---|---|
segment |
load_segment |
vmaddr |
所描述端的虚拟物理地址 |
vmsize |
为这个段分配的虚拟内存大小 |
fileoff |
段在该文件的偏移量 |
filesize |
表示段在文本中占用的字节数 |
maxport |
段的页面所需的最高内存保护,采用 8 进制表示 |
initport |
段的页面最初始内存保护 |
nsects |
段中的区 section 数量 |
flags |
杂项标志位 |
对于上面每一段,将文件中相应的内容加载到内存中:
从偏移量为fileoff
处加载filesize
字节到虚拟内存地址vmaddr
处的vmsize
字节。每段的页面都是根据initport
进行初始化,initport
指定如何通过 读/写 执行初始化页面的保护级别。
段进一步的分解区:
区 | 用途 |
---|---|
(2) 创建主线程 LC_MAIN
LC_MAIN:
主要的作用就是设置程序的入口点地址和栈的大小,在设置的郭晨中除了程序的计数器之外所有寄存器都设置 0。
(3) 代码签名 LC_CODE_SIGATURE
LC_CODE_SIGATURE:
包含了 Mach-O
二进制文件的签名,如果签名和代码本身不符合的话,内核就会立即给程序发送 SIGKILL
信号将程序杀掉。在后面苹果在 iOS
客户端采用 entitlement
机制后,代码签名就和沙盒机制绑定在一起,而且 entitlement
声明必须内嵌在 Mach-O
中并且通过签名盖章来设置执行安全敏感的操作时具有对应的权限。
(4) 加密 LC_ENCRYPTION_INFO
LC_ENCRYPTION_INFO
加密二进制文件,在 iOS
中普遍使用。
(5) Dyld
加载 LC_LOAD_DYLINKER
LC_LOAD_DYLINKER
调用 Dyld
(/user/lib/dyld) 来加载 dyld 如下。
Dyld
加载进程
在内核执行 LC_DYLINKER
来实现 Dyld
动态连接器来启动,然后就会把进程的控制权来交给 Dyld
,因为内核会把进程的入口点设置为 Dyld
的入口点。
此时连接器 Dyld
就会在启动时因为进程采用动态链接在 Mach-O
镜像文件中 “空洞” 来进行填补。这个过程可以成为:符号绑定(binding
)。
符号绑定具体实现过程
(1)二进制中使用外部定义的函数和符号,这时在文本段中会存在一个
__stubs
(桩)的区,在这个桩区存放本地未定义的占位符。
(2)编译器生成相关代码时创建符号桩区调用,链接其在运行时会解决桩区的调用。
(3)连接器解决的方式是在被调用的地址处防止一条JMP
指令,JMP
指令会将控制权交给真实的函数体,于此同时不会对栈有任何修改,在调用的过程就像直接调用真实的函数。
(4)在实际链接过程中LC_LOAD_DYLB
告诉连接器从哪里找到对应的符号,连接器采用递归的方式
加载每一个指定的库,并且搜索对应匹配的字符。链接的库有一个符号表,符号表将这些符号名称和地址关联起来。符号表可以通过Mach-O
目标文件地址可以通过LC_SYMTAB
加载命令指定的symoff
找到
dyld
是一个用户态进程,是由苹果公司来进行维护。从内核的角度来看, dyld
是一个可插入的组件,也可以替换为第三方连接器。
Dyld
共享库
Dyld
的另一个机制就是共享库缓存,共享库缓存:一些库经过预先链接,然后保存在磁盘的一个文件中。采用共享库缓存的预加载模式,可以节省加载时间。在 iOS 3.0
后 Apple
就开始把一些基础库移到次缓存,比如我们在开发中常用到的 UIKit
、Foundation
、Quartz
、AVFoundation
、StoreKit
等。
参考资料:
深入解析 Mac OS X & iOS 操作系统
第四章
iOS内存abort(Jetsam) 原理探究
iOS App 签名的原理
最后一次修改:
10月9日 02:09:45,广州天河🏠