Wrappres' Studio.

iOS系统内核XNU

字数统计: 1.3k阅读时长: 5 min
2020/02/16 Share

iOS系统内核XNU

iOS系统架构

基于ARM架构

  • 用户体验层:主要提供用户界面。这一层包含了SpringBoard,Spotlight,Accessibility。
  • 应用架构层:开发者会用到,包含了Cocoa Touch。
  • 核心架构层:系统核心功能的框架层。包含了各种图形和媒体核心框架,Metal等。
  • Darwin层:操作系统的核心,属于操作系统的内核态。包含了系统内核XNU,驱动等

img

XNU

XNU内部由Mach,BSD,驱动API IOKit组成,这些都依赖于libkern,libsa,Platform Expert。

img

Mach

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 Thread 表示一个线程,是Mach里的最小执行单元。Mach Thread有自己的状态,包括及其状态,线程栈,调度优先级(有128个,数字越大表示优先级越高),调度策略,内核Port,异常Port。

Mach Thread既可以有Mach Task处理,也可以扩展为Uthread,通过BSD Process处理。这是因为XNU采用的是微内核Mach和宏内核BSD的混合内核,具备微内核和宏内核的优点。

  • 微内核剋提高系统的模块化程度,提供内存保护的信息传递机制
  • 宏内核可以叫单内核,在出现高负荷状态时依然能够让系统保持高效运作

XNU加载App

iOS的可执行文件和动态库都是Mach-O格式,所以加载App实际上就是加载Mach-O文件。

Mach-O header信息结构代码:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; // 64位还是32位
cpu_type_t cputype; // CPU 类型,比如 arm 或 X86
cpu_subtype_t cpusubtype; // CPU 子类型,比如 armv8
uint32_t filetype; // 文件类型
uint32_t ncmds; // load commands 的数量
uint32_t sizeofcmds; // load commands 大小
uint32_t flags; // 标签
uint32_t reserved; // 保留字段
};

其中filetype有

  • OBJECT,指的是.O文件或者.a文件
  • EXECUTE,指的是IPA拆包后的文件
  • DYLIB,指的是.dylib或.framework文件
  • DYLINKER,指的是动态链接器
  • DSYM,指的是保存有符号信息用于分析闪退信息的文件

加载Mach-O文件,内核会fork进程,并对进程进行一些基本设置,比如为进程分配虚拟内存,为进程创建主线程,代码签名等。用户态dyld会对Mach-O文件做库加载和符号解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
{
// 字段设置
...
int is_64 = IS_64BIT_PROCESS(p);
struct vfs_context context;
struct uthread *uthread; // 线程
task_t new_task = NULL; // Mach Task
...

context.vc_thread = current_thread();
context.vc_ucred = kauth_cred_proc_ref(p);

// 分配大块内存,不用堆栈是因为 Mach-O 结构很大。
MALLOC(bufp, char *, (sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap)), M_TEMP, M_WAITOK | M_ZERO);
imgp = (struct image_params *) bufp;

// 初始化 imgp 结构里的公共数据
...

uthread = get_bsdthread_info(current_thread());
if (uthread->uu_flag & UT_VFORK) {
imgp->ip_flags |= IMGPF_VFORK_EXEC;
in_vfexec = TRUE;
} else {
// 程序如果是启动态,就需要 fork 新进程
imgp->ip_flags |= IMGPF_EXEC;
// fork 进程
imgp->ip_new_thread = fork_create_child(current_task(),
NULL, p, FALSE, p->p_flag & P_LP64, TRUE);
// 异常处理
...

new_task = get_threadtask(imgp->ip_new_thread);
context.vc_thread = imgp->ip_new_thread;
}

// 加载解析 Mach-O
error = exec_activate_image(imgp);

if (imgp->ip_new_thread != NULL) {
new_task = get_threadtask(imgp->ip_new_thread);
}

if (!error && !in_vfexec) {
p = proc_exec_switch_task(p, current_task(), new_task, imgp->ip_new_thread);

should_release_proc_ref = TRUE;
}

kauth_cred_unref(&context.vc_ucred);

if (!error) {
task_bank_init(get_threadtask(imgp->ip_new_thread));
proc_transend(p, 0);

thread_affinity_exec(current_thread());

// 继承进程处理
if (!in_vfexec) {
proc_inherit_task_role(get_threadtask(imgp->ip_new_thread), current_task());
}

// 设置进程的主线程
thread_t main_thread = imgp->ip_new_thread;
task_set_main_thread_qos(new_task, main_thread);
}
...
}

由于Mach-O文件,_mac_execve函数会先为Mach-O分配一大块内存imgp,接下来会初始化imgp里的公共数据。内存处理完,_mac_execve函数就会通过fork_create_chhild()函数fork出一个新的进程。新进程fork后,会通过exec_activate_image()函数解析Mach-O文件到内存imgp里。最后使用task_set_main_thread_qos()函数设置新fork出进程的主线程。

exec_activate_image()函数

1
2
3
4
5
6
7
8
9
struct execsw {
int (*ex_imgact)(struct image_params *);
const char *ex_name;
} execsw[] = {
{ exec_mach_imgact, "Mach-o Binary" },
{ exec_fat_imgact, "Fat Binary" },
{ exec_shell_imgact, "Interpreter Script" },
{ NULL, NULL}
};

加载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会加载App相关的动态库,处理完成后返回App的入口地址,然后到App的main函数。

CATALOG
  1. 1. iOS系统内核XNU
    1. 1.1. iOS系统架构
    2. 1.2. XNU
      1. 1.2.1. Mach
      2. 1.2.2. XNU加载App