iOS系统内核加载【进阶之路一】

iOS系统内核加载【进阶之路一】

IOS小彩虹2021-08-24 13:16:51580A+A-

前言

针对一个工作几年以上的iOS,如果仅仅停留在业务页面能力,其实并没有增值!接下来的一个月左右本人将主要讲述App启动方面的底层知识,保证通过这一两个月的学习和关注,一定让你对App启动有一个更深的理解!!!

App启动及框架底层的研究,会以下面一个逻辑分为5篇博客进行讲解:欢迎关注及点赞!!!

iOS系统架构

iOS系统是基于ARM的,大致可以分为四层:

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

其中用户体验层,应用框架层和核心框架层属于用户态,是上层App的活动空间。Darwin是用户态的下层支撑,是iOS系统的核心。

Darwin的内核是XNU,而XNU实在UNIX的基础上做了很多改进以及创新。了解XNU的内部是怎么样的,有利于解决系统层面的问题。

所以接下来,就一起看看XNU的框架,看看它的内部到底包含了什么?

XNU

XNU基本内容

XNU是由Mach、BSD、驱动API IOKit组成,这些都依赖于libkern、libsa、Platform Expert。如下图所示:

其中,Mach是作为 UNIX 内核的替代,主要解决 UNIX 一切皆文件导致抽象机制不足的问题,为现代操作系统做了进一步的抽象工作。 Mach 负责操作系统最基本的工作,包括进程和线程抽象、处理器调度、进程间通信、消息机制、虚拟内存管理、内存保护等。

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()函数完成。

每个 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 等,新驱动可以继承这些基类来写。

Mach Ports

所有进程间通信最终都依赖于Mach内核api提供的功能。

Mach端口重量轻,功能强大,但是缺乏文档记录 发送一个消息在一个给定的Mach端口下来到一个单一的mach_msg_send调用,但它需要一点配置,以建立消息被发送:

natural_t data;
mach_port_t port;

struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_type_descriptor_t type;
} message;

message.header = (mach_msg_header_t) {
    .msgh_remote_port = port,
    .msgh_local_port = MACH_PORT_NULL,
    .msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
    .msgh_size = sizeof(message)
};

message.body = (mach_msg_body_t) {
    .msgh_descriptor_count = 1
};

message.type = (mach_msg_type_descriptor_t) {
    .pad1 = data,
    .pad2 = sizeof(data)
};

mach_msg_return_t error = mach_msg_send(&message.header);

if (error == MACH_MSG_SUCCESS) {
    …
}

在接收端,事情稍微容易一些,因为消息只需要声明,不需要初始化:

mach_port_t port;

struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_type_descriptor_t type;
    mach_msg_trailer_t trailer;
} message;

mach_msg_return_t error = mach_msg_receive(&message.header);

if (error == MACH_MSG_SUCCESS) {
    natural_t data = message.type.pad1;
    …
}

幸运的是,Core Foundation和Foundation为Mach端口提供了更高级别的api。CFMachPort / NSMachPort是内核api上的包装器,可以用作runloop源,而CFMessagePort / NSMessagePort促进两个端口之间的同步通信。

 实际上,CFMessagePort对于简单的一对一通信非常好。只需几行代码,本地命名的端口可以附加为runloop源文件,以便在每次收到消息时运行回调:

static CFDataRef Callback(CFMessagePortRef port,
                          SInt32 messageID,
                          CFDataRef data,
                          void *info)
{
    …
}

CFMessagePortRef localPort =
    CFMessagePortCreateLocal(nil,
                             CFSTR("com.example.app.port.server"),
                             Callback,
                             nil,
                             nil);

CFRunLoopSourceRef runLoopSource =
    CFMessagePortCreateRunLoopSource(nil, localPort, 0);

CFRunLoopAddSource(CFRunLoopGetCurrent(),
                   runLoopSource,
                   kCFRunLoopCommonModes);

发送数据也很简单。只需指定远程端口、消息有效负载和发送和接收的超时。CFMessagePortSendRequest负责其余部分:

CFDataRef data;
SInt32 messageID = 0x1111; // Arbitrary
CFTimeInterval timeout = 10.0;

CFMessagePortRef remotePort =
    CFMessagePortCreateRemote(nil,
                              CFSTR("com.example.app.port.client"));

SInt32 status =
    CFMessagePortSendRequest(remotePort,
                             messageID,
                             data,
                             timeout,
                             timeout,
                             NULL,
                             NULL);
if (status == kCFMessagePortSuccess) {
    …
}

接下来,XNU是怎么加载APP的?

XNU 怎么加载 App?

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

关于Mach-O文件的讲解,在iOS启动优化之路中详细讲解

Mach-O header 信息结构代码如下:

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;   // 保留字段
};

如上面代码所示,包含了表示是 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,指的是保存有符号信息用于分析闪退信息的文件。

整个 fork 进程,加载解析 Mach-O 文件的过程可以在 XNU 的源代码中查看,代码路径是 darwin-xnu/bsd/kern/kern_exec.c,地址是github.com/apple/darwi…,相关代码在 __mac_execve 函数里,代码如下

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_child() 函数 fork 出一个新的进程。新进程 fork 后,会通过 exec_activate_image() 函数解析加载 Mach-O 文件到内存 imgp 里。最后,使用 task_set_main_thread_qos() 函数设置新 fork 出进程的主线程。

exec_activate_image() 函数会调用不同格式对应的加载函数,代码如下:

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 函数的实现代码在 dyld 仓库中的 dyldStartup.s 文件里。__dyld_start 会加载 App 相关的动态库,处理完成后会返回 App 的入口地址,然后到 App 的 main 函数。

概括

介绍了 iOS 系统的内核 XNU,以及 XNU 是如何加载 App 的。总体来说,XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。流程可以概括为:

  1. fork 新进程;

  2. 为 Mach-O 分配内存;

  3. 解析 Mach-O;

  4. 读取 Mach-O 头信息;

  5. 遍历 load command 信息,将 Mach-O 映射到内存;

  6. 启动 dyld。

总结

本篇是App加载专辑的第一篇,后续会出以下专辑!

本人会不断的更新有营养的,有自己见解和体会的博客,如果想要一起成长,欢迎点赞与关注本人以及留言。谢谢!!!

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示 | 支付宝红包
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们