我们知道,操作系统是电脑、手机上最基本的软件,任何其他的软件都必须在操作系统的支持下才能够运行。同理,软件的启动也必须在操作系统的支持下才能够运行。对于iOS系统来说,操作系统内核是XNU(X is not Unix),那么在一个app的启动过程中,XNU发挥了什么作用呢?本篇文章,我们来探究一下这个问题。
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()函数开始分析。
上面说到了,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()函数会使用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函数会按照可执行文件的格式,而执行不同的函数。目前有三种格式,单指令集可执行文件,多指令集可执行文件,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()函数会为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()函数做的工作主要有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()函数主要负责加载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在系统中的路径。
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()函数中主要是调用了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函数实际上设置入口地址,设置的是_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做的工作,之后的文章再介绍。
完。