iOS 系统架构
iOS 系统是基于 ARM 架构的,大致可以分为四层:
- 最上层是用户体验层,主要是提供用户界面。这一层包含了
SpringBoard
、Spotlight
、Accessibility
。 - 第二层是应用框架层,是开发者会用到的。这一层包含了开发框架
Cocoa Touch
。 - 第三层是核心框架层,是系统核心功能的框架层。这一层包含了各种图形和媒体核心框架、
Metal
等。 - 第四层是 Darwin 层,是操作系统的核心,属于操作系统的内核态。这一层包含了系统内核 XNU、驱动等。
iOS 系统架构
Darwin 操作系统
Darwin 是由苹果公司于 2000 年所发布的一个开放源代码操作系统。Darwin 是 macOS 和 iOS 操作环境的操作系统部分。对于 iOS 系统来说,Darwin 是用户态的下层支撑,是 iOS 系统的核心。Darwin 的内核是 XNU,而 XNU 是在 UNIX 的基础上做了很多改进以及创新。
![](https://img.haomeiwen.com/i2438680/813851f0a583e238.png)
什么是 XNU
XNU is Not Unix。XNU 是两种技术 Mach 和 BSD 的混合。BSD 层确保了 Darwin 系统的 UNIX 特性,真正的内核是 Mach,但是对外部隐藏。BSD 以上属于用户态,所有的内容都可以被应用程序访问,而应用程序不能访问内核态。当需要从用户态切换到内核态的时候,需要通过 mach trap
实现切换。
苹果已经将 XNU 开源,点击XNU 源码可以查看,XNU 内部由 Mach、BSD、IOKit 组成,这些都依赖于 libkern
、libsa
、Platform Expert
。如下图所示:
![](https://img.haomeiwen.com/i2438680/9fba07d81d03098f.png)
Mach
Mach 是 XNU 的原子核,作为 UNIX 内核的替代,是一个微内核轻量级操作系统,仅处理最核心的任务:
- 进程和线程抽象
- 任务调度
- 进程间通讯和消息传递
- 虚拟内存管理
进程对应到 Mach 是 Mach Task
,Mach Task
可以看做是线程执行环境的抽象,包含虚拟地址空间、IPC 空间、处理器资源、调度控制、线程容器。进程
每个 Mach Thread
表示一个线程,是 Mach 里的最小执行单位。Mach Thread
有自己的状态,包括机器状态、线程栈、调度优先级(有 128 个,数字越大表示优先级越高)、调度策略、内核 Port、异常 Port。
BSD
BSD 层建立在 Mach 之上,是对 Mach 封装。提供进程管理、安全、网络、驱动、内存、文件系统(HFS+)、网络文件系统(NFS)、虚拟文件系统(VFS)、POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)兼容。提供了更高层次的功能,包括:
- UNIX 进程模型
- POSIX线程模型(Pthread)及相关的同步原语
- UNIX 用户和组
- 网络协议栈(BSD Socket API)
- 文件系统访问
- 设备访问(通过/dev目录访问)
在 BSD 里进程是由 BSD Process
处理,BSD Process
扩展了 Mach Task
,增加了进程 ID、信号信息等,BSD Process
里面包含了扩展 Mach Thread
结构的 Uthread
。
IOKit
IOKit 是硬件驱动程序的运行环境,包含电源、内存、CPU 等信息。IOKit 底层 libkern
使用 C++ 子集 Embedded C++ 编写了驱动程序基类,比如 OSObject
、OSArray
、OSString
等,新驱动可以继承这些基类来写。
XNU 源代码树
- config - 为支持的架构和平台配置导出的 api
- SETUP - 用于配置内核、版本控制和 kextsymbol 管理的基本工具集。
- EXTERNAL_HEADERS - 来自其他项目的标头,以避免构建时的依赖周期。这些标头应在源更新时定期同步。
- libkern - 用于处理驱动程序和 kexts 的 C++ IOKit 库代码。
- libsa - 用于启动的内核引导代码
- libsyscall - 用户空间程序的系统调用库接口
- libkdd - 用于解析内核数据(如内核分块数据)的用户库的源代码。
- makedefs - 内核构建的顶级规则和定义。
- osfmk - 基于 Mach 内核的子系统
- pexpert - 平台特定代码,如中断处理、原子等。
- security - 强制访问检查策略接口和相关实现。
- bsd - BSD子系统代码
- tools - 一组用于测试、调试和分析内核的实用程序。
XNU 怎么加载 App?
iOS 的可执行文件和动态库都是 Mach-O 格式,所以加载 APP 实际上就是加载 Mach-O 文件。加载 Mach-O 文件,内核会 fork 进程,并对进程进行一些基本设置,比如,进程分配虚拟内存、为进程创建主线程、代码签名等。整个 fork 进程以及加载解析 Mach-O 文件的过程可以在 XNU 的源代码中查看,代码路径是 darwin-xnu/bsd/kern/kern_exec.c
load_init_program()
XNU 内核启动后,启动的第一个进程是 launchd
,launchd
启动之后会启动其他的守护进程。XNU 启动 launchd
的入口是 load_init_program()
,在kern_exec.c 文件中。
void load_init_program(proc_t p)
{
//……
error = ENOENT;
for (i = 0; i < sizeof(init_programs)/sizeof(init_programs[0]); i++)
error = load_init_program_at_path(p, (user_addr_t)scratch_addr, init_programs[i]);
if (!error) {
return;
} else if (error != ENOENT) {
printf("load_init_program: failed loading %s: errno %d\n", init_programs[i], error);
}
}
panic("Process 1 exec of %s failed, errno %d", ((i == 0) ? "<null>" : init_programs[i-1]), error);
}
launchd
上面是 load_init_program
的部分源码,其中 init_programs
就是用于存储守护进程路径的数组,在非 Debug
模式下,只会加载 "/sbin/launchd"
static const char * init_programs[] = {
#if DEBUG
"/usr/local/sbin/launchd.debug",
#endif
#if DEVELOPMENT || DEBUG
"/usr/local/sbin/launchd.development",
#endif
"/sbin/launchd",
};
load_init_program_at_path()
可以看出,load_init_program
的作用就是加载 launchd
,load_init_program_at_path()
函数用于验证输入参数和前置条件,并构造用于调用 execve()
函数的参数。
static int load_init_program_at_path(proc_t p, user_addr_t scratch_addr, const char* path)
{
……
/*
* Set up argument block for fake call to execve.
*/
init_exec_args.fname = argv0;
init_exec_args.argp = scratch_addr;
init_exec_args.envp = USER_ADDR_NULL;
/*
* So that init task is set with uid,gid 0 token
*/
set_security_token(p);
return execve(p, &init_exec_args, retval);
}
execve()
在调用 execve()
函数后,会配置执行参数 uap
, 其中 uap
是对可执行文件的抽象化,之后调用了__mac_execve()
函数
int execve(proc_t p, struct execve_args *uap, int32_t *retval)
{
struct __mac_execve_args muap;
muap.fname = uap->fname;
muap.argp = uap->argp;
muap.envp = uap->envp;
muap.mac_p = USER_ADDR_NULL;
err = __mac_execve(p, &muap, retval);
return(err);
}
__mac_execve()
通过下面 __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 出进程的主线程。
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* 结构很大。
bufp = kheap_alloc(KHEAP_TEMP,
sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap), Z_WAITOK | Z_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);
}
//...
}
exec_activate_image()
exec_activate_image()
函数会调用不同格式对应的加载函数,代码如下:
static int exec_activate_image(struct image_params *imgp)
{
//...
for (i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {
error = (*execsw[i].ex_imgact)(imgp);
switch (error) {
/* case -1: not claimed: continue */
case -2: /* Encapsulated binary, imgp->ip_XXX set for next iteration */
goto encapsulated_binary;
case -3: /* Interpreter */
nameidone(ndp);
vnode_put(imgp->ip_vp);
imgp->ip_vp = NULL; /* already put */
imgp->ip_ndp = NULL; /* already nameidone */
/* Use excpath, which exec_shell_imgact reset to the interpreter */
NDINIT(ndp, LOOKUP, OP_LOOKUP, FOLLOW | LOCKLEAF,
UIO_SYSSPACE, CAST_USER_ADDR_T(excpath), imgp->ip_vfs_context);
proc_transend(p, 0);
goto again;
default:
break;
}
}
//...
return (error);
}
execsw
其中,execsw
是一个数组,看一下这个数组的定义,
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}
};
目前支持的文件的格式有 3 种:
- 单指令集 Mach-O 可执行文件
- 多指令集 Mach-O 可执行文件,即 Fat Binary(如果是 Fat Binary,则会先进行指令集级别的 Mach-O 分解,然后再循环调用
execsw(...)
函数进行内存映射) - Shell 脚本
可以看出,加载 Mach-O 文件的是 exec_mach_imgact() 函数。
exec_mach_imgact()
exec_mach_imgact()
会通过 load_machfile()
函数加载 Mach-O 文件,以及调用 activate_exec_state() 函数处理解析加载 Mach-O 后的结构信息。
static int exec_mach_imgact(struct image_params *imgp)
{
struct mach_header *mach_header = (struct mach_header *)imgp->ip_vdata;
proc_t p = vfs_context_proc(imgp->ip_vfs_context);
int error = 0;
thread_t thread;
load_return_t lret;
load_result_t load_result;
// 判断是否是Mach-O文件
if ((mach_header->magic == MH_CIGAM) ||
(mach_header->magic == MH_CIGAM_64)) {
error = EBADARCH;
goto bad;
}
// 判断是否是可执行文件
if (mach_header->filetype != MH_EXECUTE) {
error = -1;
goto bad;
}
// 判断cputype和cpusubtype
if (imgp->ip_origcputype != 0) {
/* Fat header previously had an idea about this thin file */
if (imgp->ip_origcputype != mach_header->cputype ||
imgp->ip_origcpusubtype != mach_header->cpusubtype) {
error = EBADARCH;
goto bad;
}
} else {
imgp->ip_origcputype = mach_header->cputype;
imgp->ip_origcpusubtype = mach_header->cpusubtype;
}
task = current_task();
thread = current_thread();
uthread = get_bsdthread_info(thread);
/*
* Actually load the image file we previously decided to load.
*/
// 加载Mach-O文件,如果返回LOAD_SUCCESS,binary已经映射成可执行内存
lret = load_machfile(imgp, mach_header, thread, &map, &load_result);
// 设置内存映射的操作权限
vm_map_set_user_wire_limit(map, p->p_rlimit[RLIMIT_MEMLOCK].rlim_cur);
lret = activate_exec_state(task, p, thread, &load_result);
return(error);
}
load_machfile()
在 load_machfile()
函数中已经为 Mach-O 文件分配了虚拟内存,
load_return_t load_machfile(
struct image_params *imgp,
struct mach_header *header,
thread_t thread,
vm_map_t *mapp,
load_result_t *result
)
{
struct vnode *vp = imgp->ip_vp;
off_t file_offset = imgp->ip_arch_offset;
off_t mach-o_size = imgp->ip_arch_size;
off_t file_size = imgp->ip_vattr->va_data_size;
pmap_t pmap = 0; /* protected by create_map */
vm_map_t map;
load_result_t myresult;
load_return_t lret;
boolean_t enforce_hard_pagezero = TRUE;
int in_exec = (imgp->ip_flags & IMGPF_EXEC);
task_t task = current_task();
proc_t p = current_proc();
mach_vm_offset_t aslr_offset = 0;
mach_vm_offset_t dyld_aslr_offset = 0;
if (macho_size > file_size) {
return(LOAD_BADMACHO);
}
result->is64bit = ((imgp->ip_flags & IMGPF_IS_64BIT) == IMGPF_IS_64BIT);
// 为当前task分配内存
pmap = pmap_create(get_task_ledger(ledger_task),
(vm_map_size_t) 0,
result->is64bit);
// 创建虚拟内存映射空间
map = vm_map_create(pmap,
0,
vm_compute_max_offset(result->is64bit),
TRUE);
/*
* Compute a random offset for ASLR, and an independent random offset for dyld.
*/
if (!(imgp->ip_flags & IMGPF_DISABLE_ASLR)) {
uint64_t max_slide_pages;
max_slide_pages = vm_map_get_max_aslr_slide_pages(map);
// binary(mach-o文件)随机的ASLR
aslr_offset = random();
aslr_offset %= max_slide_pages;
aslr_offset <<= vm_map_page_shift(map);
// dyld 随机的ASLR
dyld_aslr_offset = random();
dyld_aslr_offset %= max_slide_pages;
dyld_aslr_offset <<= vm_map_page_shift(map);
}
// 使用parse_machfile方法解析mach-o
lret = parse_machfile(vp, map, thread, header, file_offset, mach-o_size,
0, (int64_t)aslr_offset, (int64_t)dyld_aslr_offset, result,
NULL, imgp);
// pagezero处理,64 bit架构,默认4GB
if (enforce_hard_pagezero &&
(vm_map_has_hard_pagezero(map, 0x1000) == FALSE)) {
{
vm_map_deallocate(map); /* will lose pmap reference too */
return (LOAD_BADMACHO);
}
}
vm_commit_pagezero_status(map);
*mapp = map;
return(LOAD_SUCCESS);
}
parse_machfile()
parse_machfile()
根据解析 Mach-O 后得到的 load command
信息,通过映射方式加载到内存中。
// 1.Mach-o的解析,相关segment虚拟内存分配
// 2.dyld的加载
// 3.dyld的解析以及虚拟内存分配
static load_return_t parse_machfile(
struct vnode *vp,
vm_map_t map,
thread_t thread,
struct mach_header *header,
off_t file_offset,
off_t mach-o_size,
int depth,
int64_t aslr_offset,
int64_t dyld_aslr_offset,
load_result_t *result,
load_result_t *binresult,
struct image_params *imgp
)
{
uint32_t ncmds;
struct load_command *lcp;
struct dylinker_command *dlp = 0;
load_return_t ret = LOAD_SUCCESS;
// depth第一次调用时传入值为0,因此depth正常情况下值为0或者1
if (depth > 1) {
return(LOAD_FAILURE);
}
// depth负责parse_machfile 遍历次数(2次),第一次是解析mach-o,第二次'load_dylinker'会调用
// 此函数来进行dyld的解析
depth++;
// 会检测CPU type
if (((cpu_type_t)(header->cputype & ~CPU_ARCH_MASK) != (cpu_type() & ~CPU_ARCH_MASK)) ||
!grade_binary(header->cputype,
header->cpusubtype & ~CPU_SUBTYPE_MASK))
return(LOAD_BADARCH);
switch (header->filetype) {
case MH_EXECUTE:
if (depth != 1) {
return (LOAD_FAILURE);
}
break;
// 如果fileType是dyld并且是第二次循环调用,那么is_dyld标记为TRUE
case MH_DYLINKER:
if (depth != 2) {
return (LOAD_FAILURE);
}
is_dyld = TRUE;
break;
default:
return (LOAD_FAILURE);
}
// 如果是dyld的解析,设置slide为传入的aslr_offset
if ((header->flags & MH_PIE) || is_dyld) {
slide = aslr_offset;
}
for (pass = 0; pass <= 3; pass++) {
// 遍历load_command
offset = mach_header_sz;
ncmds = header->ncmds;
while (ncmds--) {
// 针对每一种类型的segment进行内存映射
switch(lcp->cmd) {
case LC_SEGMENT: {
struct segment_command *scp = (struct segment_command *) lcp;
// segment解析和内存映射
ret = load_segment(lcp,header->filetype,control,file_offset,macho_size,vp,map,slide,result);
break;
}
case LC_SEGMENT_64: {
struct segment_command_64 *scp64 = (struct segment_command_64 *) lcp;
ret = load_segment(lcp,header->filetype,control,file_offset,macho_size,vp,map,slide,result);
break;
}
case LC_UNIXTHREAD:
ret = load_unixthread((struct thread_command *) lcp,thread,slide,result);
break;
case LC_MAIN:
ret = load_main((struct entry_point_command *) lcp,thread,slide,result);
break;
case LC_LOAD_DYLINKER:
// depth = 1,第一次进行mach-o解析,获取dylinker_command
if ((depth == 1) && (dlp == 0)) {
dlp = (struct dylinker_command *)lcp;
dlarchbits = (header->cputype & CPU_ARCH_MASK);
} else {
ret = LOAD_FAILURE;
}
break;
case LC_UUID:
break;
case LC_CODE_SIGNATURE:
ret = load_code_signature((struct linkedit_data_command *) lcp,vp,file_offset,macho_size,header->cputype,result,imgp);
break;
default:
ret = LOAD_SUCCESS;
break;
}
}
}
if (ret == LOAD_SUCCESS) {
if ((ret == LOAD_SUCCESS) && (dlp != 0)) {
// 第一次解析mach-o dlp会有赋值,进行dyld的加载
ret = load_dylinker(dlp, dlarchbits, map, thread, depth,
dyld_aslr_offset, result, imgp);
}
}
return(ret);
}
activate_exec_state()
activate_exec_state()
函数处理解析加载 Mach-O 后的结构信息,设置执行 App 的入口点。设置完入口点后会通过 load_dylinker()
函数来解析加载 dyld,然后将入口点地址改成 dyld 的入口地址。这一步完后,内核部分就完成了 Mach-O 文件的加载。
static int activate_exec_state(task_t task, proc_t p, thread_t thread, load_result_t *result)
{
//...
thread_setentrypoint(thread, result->entry_point);
return KERN_SUCCESS;
}
void
thread_setentrypoint(thread_t thread, mach_vm_address_t entry)
{
pal_register_cache_state(thread, DIRTY);
if (thread_is_64bit(thread)) {
x86_saved_state64_t *iss64;
iss64 = USER_REGS64(thread);
iss64->isf.rip = (uint64_t)entry;
} else {
x86_saved_state32_t *iss32;
iss32 = USER_REGS32(thread);
iss32->eip = CAST_DOWN_EXPLICIT(unsigned int, entry);
}
}
总结
总体来说,XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。流程可以概括为:
- fork 新进程;
- 为 Mach-O 分配内存;
- 解析 Mach-O;
- 读取 Mach-O 头信息;
- 遍历
load command
信息,将 Mach-O 映射到内存; - 启动 dyld。
网友评论