XNU加载Mach-O和dyld

我们知道,操作系统是电脑、手机上最基本的软件,任何其他的软件都必须在操作系统的支持下才能够运行。同理,软件的启动也必须在操作系统的支持下才能够运行。对于iOS系统来说,操作系统内核是XNU(X is not Unix),那么在一个app的启动过程中,XNU发挥了什么作用呢?本篇文章,我们来探究一下这个问题。

XNU启动launchd

XNU的代码是开源的,可以从苹果开源代码平台上下载XNU的代码,通过分析XNU的源码,可以大致了解XNU是如何加载Mach-O文件以及dyld的。

XNU内核启动后,启动的第一个进程是launchd,launchd启动之后会启动其他的守护进程。XNU启动launchd的过程是 load_init_program() -> load_init_program_at_path()。可以看一下这两个函数的源码。

load_init_program()部分代码:

void load_init_program(proc_t p)
{
    ……

    error = ENOENT;
    // 核心代码在这里,加载初始化程序,对 init_programs数组遍历
    for (i = 0; i < sizeof(init_programs)/sizeof(init_programs[0]); i++) {
        // 调用load_init_program_at_path方法
        error = load_init_program_at_path(p, (user_addr_t)scratch_addr, init_programs[i]);
        if (!error)
            return;
    }

    panic("Process 1 exec of %s failed, errno %d", ((i == 0) ? "" : init_programs[i-1]), error);
}

init_programs是一个数组,可以看一下该数组的定义:

// 内核的debug模式下可以加载供调试的launchd,非debug模式下,只加载launchd
// 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的作用就是加载launchd,加载launchd使用的方法是load_init_program_at_path函数。load_init_program_at_path的部分代码如下:

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);
    // 会调用execve方法
    return execve(p, &init_exec_args, retval);
}

load_init_program_at_path调用了execve()函数,实际上,execve是加载Mach-O文件流程的入口函数。因为launchd进程比较特殊,所以多了两个方法。因此,接下来我们就从execve()函数开始分析。

XNU加载Mach-O

execve()函数

上面说到了,execve()函数是加载Mach-O文件的入口,看一下execve()函数做了哪些事情:

/*
 uap是对可执行文件的封装,uap->fname可以得到执行文件的文件名
 uap->argp 可以得到执行文件的参数列表
 uap->envp 可以得到执行文件的环境变量列表
 */
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;
    // 调用了__mac_execve方法
    err = __mac_execve(p, &muap, retval);

    return(err);
}

可以看到,execve()函数的作用主要是进行了一些赋值,然后调用了__mac_execve()函数,看来核心操作在__max_execve()函数中。

__mac_execve()函数

__mac_execve()函数会使用fork_create_child()函数启动新进程,之后使用新的进程,生成新的task。__mac_execve()函数的主要功能就是干这个,之后就调用了exec_activate_image()函数。

__mac_execve()函数的部分代码:

int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
{
    // 新的task定义
    task_t new_task = NULL;
    boolean_t should_release_proc_ref = FALSE;
    boolean_t exec_done = FALSE;
    boolean_t in_vfexec = FALSE;
    void *inherit = NULL;

    context.vc_thread = current_thread();
    context.vc_ucred = kauth_cred_proc_ref(p);  /* XXX must NOT be kauth_cred_get() */

    /* Initialize the common data in the image_params structure */
    // 使用uap初始化imgp结构体中的一些通用数据
    imgp->ip_user_fname = uap->fname;
    imgp->ip_user_argv = uap->argp;
    imgp->ip_user_envv = uap->envp;
    imgp->ip_vattr = vap;
    imgp->ip_origvattr = origvap;
    imgp->ip_vfs_context = &context;
    imgp->ip_flags = (is_64 ? IMGPF_WAS_64BIT : IMGPF_NONE) | ((p->p_flag & P_DISABLE_ASLR) ? IMGPF_DISABLE_ASLR : IMGPF_NONE);
    imgp->ip_seg = (is_64 ? UIO_USERSPACE64 : UIO_USERSPACE32);
    imgp->ip_mac_return = 0;
    imgp->ip_cs_error = OS_REASON_NULL;

    uthread = get_bsdthread_info(current_thread());
    if (uthread->uu_flag & UT_VFORK) {
        imgp->ip_flags |= IMGPF_VFORK_EXEC;
        in_vfexec = TRUE;
    } else {
        // 创建进程和新的task
        imgp->ip_new_thread = fork_create_child(current_task(),
                    NULL, p, FALSE, p->p_flag & P_LP64, TRUE);
        /* task and thread ref returned by fork_create_child */
        if (imgp->ip_new_thread == NULL) {
            error = ENOMEM;
            goto exit_with_error;
        }

        new_task = get_threadtask(imgp->ip_new_thread);
        context.vc_thread = imgp->ip_new_thread;
    }
    // 调用了exec_activate_image方法
    error = exec_activate_image(imgp);

    return(error);
}
exec_activate_image

exec_activate_image函数会按照可执行文件的格式,而执行不同的函数。目前有三种格式,单指令集可执行文件,多指令集可执行文件,shell 脚本。如果是多指令集的,最终还是会执行到但指令集所对应的函数。因为分析的是如何加载Mach-O文件,所以暂不考虑shell 脚本。来看一下exec_activate_image的内部实现:

// 根据二进制文件的不同格式,执行不同的内存映射函数
static int exec_activate_image(struct image_params *imgp)
{
encapsulated_binary:
    error = -1;
    // 核心在这里,循环调用execsw相应的格式映射的加载函数进行加载
    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 */
            imgp->ip_vp = NULL; /* already put */
            imgp->ip_ndp = NULL; /* already nameidone */
            goto again;

        default:
            break;
        }
    }

    return (error);
}

execsw是一个数组,看一下这个数组的定义:

struct execsw {
    int (*ex_imgact)(struct image_params *);
    const char *ex_name;
} execsw[] = {
    // 单指令集的Mach-O
    { exec_mach_imgact,     "Mach-o Binary" },
    // 多指令集的Mac-O exec_fat_imgact会先进行指令集分解,然后调用exec_mach_imgact
    { exec_fat_imgact,      "Fat Binary" },
    // shell脚本
    { exec_shell_imgact,        "Interpreter Script" },
    { NULL, NULL}
};

可以看到,单指令集的Mach-O文件最终调用的函数是exec_mach_imgact()。exec_mach_imgact()函数中的一个重要功能是将Mach-O文件映射到内存。将Mach-O文件映射到内存的函数是load_machfile(),因此,在介绍exec_mach_imgact()函数之前,先介绍一下load_machfile()函数。

load_machfile

load_machfile()函数会为mach-o文件分配虚拟内存,并且计算mach-o文件和dyld随机偏移量的值。之后会调用解析mach-o文件的函数parse_machfile()。看一下load_machfile的部分代码:

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           macho_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, macho_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);
}

PAGEZERO是可执行程序的第一个段程序的空指针异常,用于捕获,总是位于虚拟内存最开始的位置,大小和CPU的架构有关。在64位的CPU架构下,PAGEZERO的大小是4G。

可以看到,mach-o文件的随机偏移值和dyld的随机偏移值,其实就是获取了一个随机数,然后根据随机数进行了一些计算。在load_machfile()函数中已经为mach-o文件分配了虚拟内存,接下来看一下parse_machfile()函数做了哪些操作。

parse_machfile

parse_machfile()函数做的工作主要有3个:(1)Mach-O文件的解析,以及对每个segment进行内存分配;(2)dyld的加载(3)dyld的解析以及虚拟内存分配。

看一下parse_machfile()函数的部分代码:

// 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           macho_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);
}

parse_machfile()函数中调用了load_dylinker函数,看一下load_dylinker()函数中做了哪些操作。

load_dylinker

load_dylinker()函数主要负责加载dyld,以及调用parse_machfile()函数对dyld解析。

load_dylinker()的部分代码:

// load_dylinker函数主要负责dyld的加载,解析等工作
static load_return_t load_dylinker(
    struct dylinker_command *lcp,
    integer_t       archbits,
    vm_map_t        map,
    thread_t    thread,
    int         depth,
    int64_t         slide,
    load_result_t       *result,
    struct image_params *imgp
)
{
    struct vnode        *vp = NULLVP;   /* set by get_macho_vnode() */
    struct mach_header  *header;
    load_result_t       *myresult;
    kern_return_t       ret;
    struct macho_data   *macho_data;
    struct {
        struct mach_header  __header;
        load_result_t       __myresult;
        struct macho_data   __macho_data;
    } *dyld_data;

#if !(DEVELOPMENT || DEBUG)
    // 非内核debug模式下,会校验name是否和DEFAULT_DYLD_PATH相同,如果不同,直接报错
    if (0 != strcmp(name, DEFAULT_DYLD_PATH)) {
        return (LOAD_BADMACHO);
    }
#endif
    // 读取dyld
    ret = get_macho_vnode(name, archbits, header,
        &file_offset, &macho_size, macho_data, &vp);
    if (ret)
        goto novp_out;

    *myresult = load_result_null;
    myresult->is64bit = result->is64bit;

    // 解析dyld
    ret = parse_machfile(vp, map, thread, header, file_offset,
                         macho_size, depth, slide, 0, myresult, result, imgp);
novp_out:
    FREE(dyld_data, M_TEMP);
    return (ret);
}

dyld同样是Mach-O类型的文件,因此使用parse_machfile()函数针对dyld进行解析,以及加载其中的segment。代码中使用到了 DEFAULT_DYLD_PATH,看一下DEFAULT_DYLD_PATH的定义:

// dyld默认加载地址
#define DEFAULT_DYLD_PATH "/usr/lib/dyld"

从这里,也可以看到dyld在系统中的路径。

exec_mach_imgact

Mach-O文件以及dyld被映射到虚拟内存后,回过头来,我们再来看一下exec_mach_imgact()函数做的操作。

exec_mach_imgact()函数的部分代码:

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);
}

exec_mach_imgact()函数中做的操作是使用load_machfile()将Mach-O文件映射到内存中,以及设置了一些内存映射的操作权限,之后调用了activate_exec_state()函数。看一下activate_exec_state()函数中做了哪些操作。

activate_exec_state

activate_exec_state()函数中主要是调用了thread_setentrypoint()函数。

activate_exec_state()函数的部分代码:

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;
}
thread_setentrypoint

thread_setentrypoint函数实际上设置入口地址,设置的是_dyld_start函数的入口地址。从这一步开始,_dyld_start开始执行。_dyld_start是dyld起始函数,dyld是运行在用户态的,也就是从这里开始,内核态切换到了用户态。

thread_setentrypoint()函数的部分代码:

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);
    }
}

实际上就是把entry_point的地址直接写入到了寄存器里面。

总结

到这里,就完成了XNU如何将一个Mach-O文件以及dyld加载到内存中的流程分析。其实不看源码,大体流程我们也可以猜到,操作系统想要启动一个app,无非是给这个app分配进程,以及相应的进程空间,之后是给app分配内存,将app映射到内存中。通过源码,能看到每一步是如何实现的。这里只是分析到了XNU将Mach-O文件加载到内存中,实际上后续用户态的dyld还要做一些工作,才能真正的将一个app启动。关于后续dyld做的工作,之后的文章再介绍。

完。

你可能感兴趣的:(iOS开发)