美文网首页
XNU 是如何加载 APP 的?

XNU 是如何加载 APP 的?

作者: _涼城 | 来源:发表于2022-03-22 13:13 被阅读0次

iOS 系统架构

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

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

Darwin 操作系统

    Darwin 是由苹果公司于 2000 年所发布的一个开放源代码操作系统。Darwin 是 macOS 和 iOS 操作环境的操作系统部分。对于 iOS 系统来说,Darwin 是用户态的下层支撑,是 iOS 系统的核心。Darwin 的内核是 XNU,而 XNU 是在 UNIX 的基础上做了很多改进以及创新。

Darwin 架构模型

什么是 XNU

    XNU is Not UnixXNU 是两种技术 Mach 和 BSD 的混合BSD 层确保了 Darwin 系统的 UNIX 特性,真正的内核是 Mach,但是对外部隐藏。BSD 以上属于用户态,所有的内容都可以被应用程序访问,而应用程序不能访问内核态。当需要从用户态切换到内核态的时候,需要通过 mach trap 实现切换。
    苹果已经将 XNU 开源,点击XNU 源码可以查看,XNU 内部由 MachBSDIOKit 组成,这些都依赖于 libkernlibsaPlatform Expert。如下图所示:

XNU 系统架构

Mach

    MachXNU 的原子核,作为 UNIX 内核的替代,是一个微内核轻量级操作系统,仅处理最核心的任务:

  • 进程和线程抽象
  • 任务调度
  • 进程间通讯和消息传递
  • 虚拟内存管理

    进程对应到 MachMach TaskMach 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++ 编写了驱动程序基类,比如 OSObjectOSArrayOSString 等,新驱动可以继承这些基类来写。

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 内核启动后,启动的第一个进程是 launchdlaunchd 启动之后会启动其他的守护进程。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 的作用就是加载 launchdload_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 文件,最后映射到内存空间。流程可以概括为:

  1. fork 新进程;
  2. Mach-O 分配内存;
  3. 解析 Mach-O
  4. 读取 Mach-O 头信息;
  5. 遍历 load command 信息,将 Mach-O 映射到内存;
  6. 启动 dyld。

相关文章

网友评论

      本文标题:XNU 是如何加载 APP 的?

      本文链接:https://www.haomeiwen.com/subject/gvjfjrtx.html