Skip to content

Mach‐O文件是怎样被加载到内存中的

shenchunxing edited this page Aug 30, 2023 · 1 revision

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

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,指的是保存有符号信息用于分析闪退信息的文件。

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

苹果公司已经将 XNU 开源,并在 GitHub 上创建了镜像。要想编译 XNU,你可以查看“Building the XNU kernel on Mac OS X Sierra (10.12.X)”这篇文章;要想调试 XNU,可以查看“Source Level Debugging the XNU Kernel”这篇文章。

整个 fork 进程,加载解析 Mach-O 文件的过程可以在 XNU 的源代码中查看,代码路径是 darwin-xnu/bsd/kern/kern_exec.c,地址是https://github.com/apple/darwin-xnu/blob/master/bsd/kern/kern_exec.c,相关代码在 __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。
Clone this wiki locally