The two most important days in your life are the day you are born and the day you find out why.

-- Mark Twain


转自 Jaminzzhang的博客
“你是谁?从哪里来?到哪里去?”,这三个富有哲学气息的问题,是每一个人在不断解答的问题。我们Code,Build,Run,一个活生生的App跃然方寸屏上,这一切是如何发生的?从用户点击App到执行main函数这短短的瞬间发生了多少事呢?探寻App的启动新生,可以帮助我们更了解App开发本身。

下图是App启动流程的关键节点展示:

下面我们就来一一解读。

1. App文件的组成


在详细研究启动流程之前,首先我们需要了解下iOS/OSX的App执行文件

一个应用,通常都是经过“编译-》链接-》打包”几个步骤之后,生成一个可在某平台上运行应用。应用文件在不同的平台上以不同的格式存在,如Windows上的exe,Android上的pkg,以及我们接下来要说的ipa。

iOS系统是由OS X发展而来,而OS X是由NeXTSTEP与Mac OS Classic的融合。因此iOS/OS X系统很多的特性都是源于NeXTSTEP系统,如Objective-C、Cocoa、Mach、XCode等,其中还有应用/库的组成——Bundle。Bundle的官方解释是a standardized hierarchical structure that holds executable code and the resources used by that code.,也就是包含执行代码和相关资源的标准层次结构;可以简单地理解为包(Package)。

OS X应用和iOS应用两者的bundle结构有些许差别,OS X的应用程序的层次结构比较规范,而iOS的App则相对来说比较散乱,而且与OS不同的是,iOS只有Apple原生的应用才会在/Applications目录下,从App Store上购买的应用会安装在/var/mobile/Applications目录下;OSX的应用不再本文讨论范围之内,所以我们先来看看iOS的App Bundle的层次结构:

128-bit GUID/
xxxx.app/
Documents/
iTunesArtwork
iTunesMetaData.plist
Library/
tmp/

其中xxx.app就是我们的app应用程序,主要包含了执行文件(xxx.app/xxx, xxx为应用名称)、NIB和图片等资源文件。接下来就主要看看本节的主角: Mach-O

1.1 Universal Binary

大部分情况下,xxx.app/xxx文件并不是Mach-O格式文件,由于现在需要支持不同CPU架构的iOS设备,所以我们编译打包出来的执行文件是一个Universal Binary格式文件(通用二进制文件,也称胖二进制文件),其实Universal Binary只不过将支持不同架构的Mach-O打包在一起,再在文件起始位置加上Fat Header来说明所包含的Mach-O文件支持的架构和偏移地址信息;

Fat Header的数据结构在<mach-o/fat.h>头文件上有定义:

#define FAT_MAGIC   0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */ struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
}; struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};

结构struct fat_header

  • 1). magic字段就是我们常说的魔数(与UNIX的ELF文件一样),加载器通过这个魔数值来判断这是什么样的文件,胖二进制文件的魔数值是0xcafebabe;
  • 2). nfat_arch字段是指当前的胖二进制文件包含了多少个不同架构的Mach-O文件;

fat_header后会跟着fat_arch,有多少个不同架构的Mach-O文件,就有多少个fat_arch,用于说明对应Mach-O文件大小、支持的CPU架构、偏移地址等;

可以用file命令来查看下执行文件的信息,如新浪微博:

ps:上述说“大部分情况”是因为还有一部分,由于业务比较复杂,代码量巨大,如果支持多种CPU架构而打包多个Mach-O文件的话,会导致ipa包变得非常大,所以就并没有支持新的CPU架构的。如QQ和微信:

ps:QQ V5.5.1版本单个Mach-O文件大小为51M

1.2 Mach-O

虽然iOS/OS X采用了类UNIX的Darwin操作系统核心,完全符合UNIX标准系统,但在执行文件上,却没有支持UNIX的ELF,而是维护了一个独有的二进制可执行文件格式:Mach-Object(简写Mach-O)。Mach-O是NeXTSTEP的遗产,其文件格式如下:

由上图,我们可以看到Mach-O文件主要包含一下三个数据区: 
(1). 头部Header:在<mach-o/loader.h>头文件定义了Mach-O Header的数据结构:

/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
}; /* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */

以上引用代码是32位的文件头数据结构,<mach-o/loader.h>头文件还定义了64位的文件头数据结构mach_header_64,两者基本没有差别,mach_header_64多了一个额外的预留字段uint32_t reserved;,该字段目前没有使用。需要注意的是,64位的Mach-O文件的魔数值为#define MH_MAGIC_64 0xfeedfacf

(2). 加载命令 Load Commends

在mach_header之后的是加载命令,这些加载命令在Mach-O文件加载解析时,被内核加载器或者动态链接器调用,指导如何设置加载对应的二进制数据段;Load Commend的数据结构如下:

struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

OS X/iOS发展到今天,已经有40多条加载命令,其中部分是由内核加载器直接使用,而其他则是由动态链接器处理。其中几个主要的Load Commend为LC_SEGMENTLC_LOAD_DYLINKERLC_UNIXTHREADLC_MAIN等,这里不详细介绍,在<mach-o/loader.h>头文件有简单的注释,后续内核还会涉及。

  • ps: otool是查看操作Mach-O文件的工具,类似于UNIX下的ldd或readelf工具。
  • MachOView是查看Mach-O文件的可视化工具。

(3). 原始段数据 Raw segment data

原始段数据,是Mach-O文件中最大的一部分,包含了Load Command中所需的数据以及在虚存地址偏移量和大小;一般Mach-O文件有多个段(Segement),段每个段有不同的功能,一般包括:

  • 1). __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
  • 2). __TEXT: 包含了执行代码以及其他只读数据。该段数据的保护级别为:VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),防止在内存中被修改;
  • 3). __DATA: 包含了程序数据,该段可写;
  • 4). __OBJC: Objective-C运行时支持库;
  • 5). __LINKEDIT: 链接器使用的符号以及其他表

一般的段又会按不同的功能划分为几个区(section),标识段-区的表示方法为(SEGMENT.section),即段所有字母大小,加两个下横线作为前缀,而区则为小写,同样加两个下横线作为前缀;更多关于常见section的解析,请查看https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/

2. 内核Kernel


了解了App执行文件之后,我们从源码来看看,App经过了什么样的内核调用流程之后,来到了主程序入口main()。

2.1 XNU开源代码

虽然内核XNU是开源的,但只限于OS X, iOS的XNU内核一直是封闭的,但从历史角度来说,iOS是OS X的分支,两者比较大的区别就是支持的目标架构不一样(iOS目标架构为ARM,而不是OS X的Intel i386和x86_64),内存管理以及系统安全限制;而执行文件都是Mach-O。所以,本文预设两者在App启动执行这方面并没有太大差别。

本文参考的XNU版本为v2782.1.97;

2.2 内核调用流程

可执行文件的内核流程如下图:

引用自《Mac OS X and iOS Internals : To the Apple's Core》P555

上述流程对应到源代码的调用树为:

ps: 由于源代码较多,篇幅所限,只引用关键性的代码,并有简单的注释,本人注释以oncenote为前缀.

// oncenote: /bsd/kern/ker_exec.c  line: 2615
execve(proc_t p, struct execve_args *uap, int32_t *retval)
{
__mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
{// oncenote: /bsd/kern/ker_exec.c line: 2654
// oncenote: /bsd/kern/ker_exec.c line: 2735
// 加载执行文件镜像并设置环境
exec_activate_image(struct image_params *imgp)
{
// oncenote: /bsd/kern/kern_exec.c line: 1328
// 遍历execsw执行格式,执行对应的ex_imgact函数
for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {
// 1.对于Mach-o Binary,执行exec_mach_imgact
// 2.对于Fat Binary,执行exec_fat_imgact
// 3.对于Interpreter Script,执行exec_shell_imgact
// 由于只支持Mach-O这种执行格式,所以exec_fat_imgact和exec_shell_imgact最终都会调到exec_mach_imgact
// 返回错误码0,则表示mach file被正确加载处理;只有exec_mach_imgact会返回0
error = (*execsw[i].ex_imgact)(imgp); // oncenote: 对于Mach-o,执行(*execsw[i].ex_imgact)(imgp) = exec_mach_imgact(imgp)
exec_mach_imgact(struct image_params *imgp)
{
// oncenote: /bsd/kern/kern_exec.c line: 893
load_machfile(struct image_params *imgp, ...)
{// oncenote: /bsd/kern/mach_loader.c line: 287 // oncenote: oncenote: /bsd/kern/mach_loader.c line: 336
// 设置内存映射
if (create_map) {
vm_map_create();
} // oncenote: /bsd/kern/mach_loader.c line: 373
// 设置地址空间布局随机数
if (!(imgp->ip_flags & IMGPF_DISABLE_ASLR)) {
aslr_offset = random();
} // oncenote: /bsd/kern/mach_loader.c line: 392
parse_machfile(struct vnode *vp, ..., load_result_t *result)
{
// oncenote: 递归深度解析mach file, 在2.3中详细讲解
} } // oncenote: /bsd/kern/kern_exec.c line: 973
if (load_result.unixproc) {
/* Set the stack */ //oncenote
thread_setuserstack(thread, ap);
} // oncenote: /bsd/kern/kern_exec.c line: 1014
// 设置入口点(寄存器状态来自LC_UNIXTHREAD)
/* Set the entry point */
thread_setentrypoint(thread, load_result.entry_point); /* Stop profiling */
stopprofclock(p); /*
* Reset signal state.
*/
execsigs(p, thread); ...
} }
}
} }

由于篇幅所限,本文就不对源码进行展开讲解。通过上述的调用树,App启动在内核中的大概流程已非常清晰,如想更深入研究,请下载源代码,并辅以文末参考资料,进行阅读;

2.3 加载并解析Mach-O文件

前一节描述了可执行文件的执行流程,本节探讨下,内核是如何加载解析Mach-O文件的。

函数load_machfile()加载Mach-O文件,然后调用函数parse_machfile()解析Mach-O文件。函数load_machfile()本身并没有太复杂的逻辑,因此parse_machfile()函数是加载解析Mach-O文件的核心逻辑。在阅读具体代码观察解析流程之前,先明确下parse_machfile()三个特别的逻辑:

  • 首先,parse_machfile()是递归解析的,最初的递归深度为0,最高深度到6,防止无限递归。使用递归解析,主要是将不同Mach-O文件类型按照依赖关系,分前后进行解析。如解析可执行二进制文件类型(MH_EXECUTABLE)的Mach-O文件需要调用load_dylinker来处理加载命令LC_LOAD_DYLINKER,而动态链接器也是Mach-O文件,所以就需要递归到不同的深度进行解析;

  • 其次,parse_machfile()的每一次递归,在解析加载命令时,会将内核需要解析的加载命令按照加载循序划分为三组进行解析,在代码的体现上就是通过三次循环,每趟循环只关注当前趟需要解析的命令: (1):解析线程状态,UUID和代码签名。相关命令为LC_UNIXTHREAD、LC_MAIN、LC_UUID、LC_CODE_SIGNATURE (2):解析代码段Segment。相关命令为LC_SEGMENT、LC_SEGMENT_64; (3):解析动态链接库、加密信息。相关命令为:LC_ENCRYPTION_INFO、LC_ENCRYPTION_INFO_64、LC_LOAD_DYLINKER

  • 最后,关于Mach-O的入口点。解析完可执行二进制文件类型的Mach-O文件(假设为A)之后,我们会得到A的入口点;但线程并不立刻进入到这个入口点。这是由于我们还会加载动态链接器(dyld),在load_dylinker()中,dyld会保存A的入口点,递归调用parse_machfile()之后,将线程的入口点设为dyld的入口点;动态链接器dyld完成加载库的工作之后,再将入口点设回A的入口点,程序启动完成;

理解了上述逻辑之后,我们通过源代码最直观地探索解析流程:

// oncenote: oncenote: /bsd/kern/mach_loader.c  line: 483
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
)
{
/*
* Break infinite recursion
*/
//oncenote: 最大深度6的控制
if (depth > 6) {
return(LOAD_FAILURE);
} depth++; //oncenote: 不同的深度解析不同的Mach-o文件类型,
//如可执行二进制文件类型MH_EXECUTE,只在第一次深度,因此不存在MH_EXECUTE依赖MH_EXECUTE的情况
switch (header->filetype) { case MH_OBJECT:
case MH_EXECUTE:
case MH_PRELOAD:
if (depth != 1) {
return (LOAD_FAILURE);
}
break; case MH_FVMLIB:
case MH_DYLIB:
if (depth == 1) {
return (LOAD_FAILURE);
}
break; case MH_DYLINKER:
if (depth != 2) {
return (LOAD_FAILURE);
}
break; default:
return (LOAD_FAILURE);
} // ...
//oncenote: 将所有的加载命令都映射到内核内存中,准备解析
/*
* Map the load commands into kernel memory.
*/
addr = 0;
kl_size = size;
kl_addr = kalloc(size);
addr = (caddr_t)kl_addr;
if (addr == NULL)
return(LOAD_NOSPACE); error = vn_rdwr(UIO_READ, vp, addr, size, file_offset,
UIO_SYSSPACE, 0, kauth_cred_get(), &resid, p); // ...
//nocenote: 开始解析加载命令(Load Command),分三趟进行解析
/*
* Scan through the commands, processing each one as necessary.
* We parse in three passes through the headers:
* 1: thread state, uuid, code signature
* 2: segments
* 3: dyld, encryption, check entry point
*/ for (pass = 1; pass <= 3; pass++) { /*
* Check that the entry point is contained in an executable segments
*/
if ((pass == 3) && (result->validentry == 0)) {
thread_state_initialize(thread);
ret = LOAD_FAILURE;
break;
} /*
* Loop through each of the load_commands indicated by the
* Mach-O header; if an absurd value is provided, we just
* run off the end of the reserved section by incrementing
* the offset too far, so we are implicitly fail-safe.
*/
offset = mach_header_sz;
ncmds = header->ncmds; while (ncmds--) { /*
* Get a pointer to the command.
*/
lcp = (struct load_command *)(addr + offset);
oldoffset = offset;
offset += lcp->cmdsize; switch(lcp->cmd) { case LC_SEGMENT:
if (pass != 2) //oncenote: 第二趟进行解析
break; ret = load_segment(lcp, header->filetype, control, file_offset, macho_size, vp, map, slide, result);
break; case LC_SEGMENT_64:
//oncenote: 与命令LC_SEGMENT相同
break; case LC_UNIXTHREAD:
if (pass != 1)
break;
//oncenote: load_unixthread() 依次调用load_threadstack()、load_threadentry()和load_threadstate()
//oncenote: 启动一个unix线程,加载线程的初始化状态,并载入入口点
ret = load_unixthread((struct thread_command *) lcp, thread, slide, result);
break; case LC_MAIN:
if (pass != 1)
break;
if (depth != 1)
break;
//oncenote: 代替LC_UNIXTHREAD,与LC_UNIXTHREAD类似
ret = load_main((struct entry_point_command *) lcp, thread, slide, result);
break; case LC_LOAD_DYLINKER:
if (pass != 3)
break; //在第一次深度的递归调用,解析到LC_LOAD_DYLINKER,设置dlp,用于后续加载动态链接库
if ((depth == 1) && (dlp == 0)) {
dlp = (struct dylinker_command *)lcp;
dlarchbits = (header->cputype & CPU_ARCH_MASK);
} else {
ret = LOAD_FAILURE;
}
break; case LC_UUID: //oncenote: 省略
break; case LC_CODE_SIGNATURE: //oncenote: 省略
break;
#if CONFIG_CODE_DECRYPTION
case LC_ENCRYPTION_INFO: //oncenote: 省略
case LC_ENCRYPTION_INFO_64:
break;
#endif
default:
//内核不处理其他命令,其他命令交由动态链接器dyld来处理
/* Other commands are ignored by the kernel */
ret = LOAD_SUCCESS;
break;
}
if (ret != LOAD_SUCCESS)
break;
}
if (ret != LOAD_SUCCESS)
break;
} //oncenote: 前面解析命令操作成功,加载动态链接器
if (ret == LOAD_SUCCESS) { if ((ret == LOAD_SUCCESS) && (dlp != 0)) {
/*
* load the dylinker, and slide it by the independent DYLD ASLR
* offset regardless of the PIE-ness of the main binary.
*/
ret = load_dylinker(dlp, dlarchbits, map, thread, depth, dyld_aslr_offset, result);
}
} // ... return(ret);
}

再来看load_dylinker()的代码:

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
)
{ //oncenote: 获取dyld vnode
ret = get_macho_vnode(name, archbits, header,
&file_offset, &macho_size, macho_data, &vp);
if (ret)
goto novp_out; *myresult = load_result_null; /*
* First try to map dyld in directly. This should work most of
* the time since there shouldn't normally be something already
* mapped to its address.
*/
//oncenote: 递归调用parse_machfile()解析dyld
ret = parse_machfile(vp, map, thread, header, file_offset,
macho_size, depth, slide, 0, myresult); // ... if (ret == LOAD_SUCCESS) {
//oncenote: 解析成功,设置线程入口为dyld的入口,dyld开始加载共享库
result->dynlinker = TRUE;
result->entry_point = myresult->entry_point;
result->validentry = myresult->validentry;
result->all_image_info_addr = myresult->all_image_info_addr;
result->all_image_info_size = myresult->all_image_info_size;
if (myresult->platform_binary) {
result->csflags |= CS_DYLD_PLATFORM;
}
} // ... return (ret);
}

3. 总结


之前对App流程有个大体的概念,但于细节并不甚清楚,耗时1个多月,边学边复习边写文章,终于在出行旅游前完成。原计划是准备在第三段讲解下动态链接器dyld加载共享库的流程的,但限于本文篇幅实在太长,所以新起一篇文章来写会好一点。

关于App启动流程还有许多细节,如代码签名验证、虚存映射、iOS的触屏应用加载器SpringBoard如何进行切换应用等,本文并未涉及到,有兴趣的同学可以继续深入研究。

参考资料:

  1. 《Mac OS X Internals: A Systems Approach》
  2. 《Mac OS X and iOS Internals : To the Apple's Core》
  3. XNU源代码
  4. The App Launch Sequence on iOS
  5. Mach-O Programming Topics
  6. DYLD Detailed

版权所有,转载请保留Jaminzzhang署名

由App的启动说起(转)的更多相关文章

  1. Android性能优化之App应用启动分析与优化

    前言: 昨晚新版本终于发布了,但是还是记得有测试反馈app启动好长时间也没进入app主页,所以今天准备加个班总结一下App启动那些事! app的启动方式: 1.)冷启动      当启动应用时,后台没 ...

  2. App的启动过程

    App的启动过程 所有的app都是通过launcher去启动的 launcher自己也是一个app,一个系统级别的app,放在asystem/app/下,使用系统签名. 对代码进行分析

  3. iOS app 程序启动原理

    iOS app 程序启动原理 Info.plist: 常见设置     建立一个工程后,会在Supporting files文件夹下看到一个"工程名-Info.plist"的文件, ...

  4. iOS 9 failed for URL: "XXX://@" - error: "This app is not allowed to query for scheme XXX" iOS 从APP里启动另一APP

    iOS 从C APP里启动 D APP 首先在D APP里设置 URL Schemes 在info.plist 文件里添加URL Schemes URL Types -->item0 --> ...

  5. 如何设置App的启动图

    如何设置App的启动图,也就是Launch Image? Step1 1.点击Image.xcassets 进入图片管理,然后右击,弹出"New Launch Image" 2.如 ...

  6. 【转载】Android App应用启动分析与优化

    前言: 昨晚新版本终于发布了,但是还是记得有测试反馈app启动好长时间也没进入app主页,所以今天准备加个班总结一下App启动那些事! app的启动方式: 1.)冷启动  当启动应用时,后台没有该应用 ...

  7. 怎样做一个iOS App的启动分层引导动画?

    一. 为什么要写这篇文章? 这是一个很古老的话题,从两年前新浪微博开始使用多层动画制作iOS App的启动引导页让人眼前一亮(当然,微博是不是历史第一个这个问题值得商榷)之后,各种类型的引导页层出不穷 ...

  8. iOS App的启动过程

    一.mach-O Executable 可执行文件 Dylib 动态库 Bundle 无法被连接的动态库,只能通过 dlopen() 加载 Image 指的是 Executable,Dylib 或者 ...

  9. 从APP的启动说起

    iOS里面APP的启动,过程有些复杂,今天我们来抽丝剥茧,一步步探讨一下APP的启动会经历哪些过程. 首先,用户点击iPhone里面的某个APP的icon,Kernel内核会开始初始化空间并创建进程, ...

随机推荐

  1. python手记(30)

    #!/usr/bin/env python #-*- coding: utf-8 -*- import cv2 import numpy as np fn="test3.png" ...

  2. js图片预览插件,不涉及上传

    小小的几十行代码,很牛逼,很实用. 支持多个图片的预览,只要new多个对象就行了. html如下 <!-- zhouxiang www.zhou-xiang.com --> <!DO ...

  3. bzoj1211

    prufer码水题(n-2)!/[(d1-1)!*(d2-1)!*…*(dn-1)!] ..] of longint; x,n,i,j,s:longint; ans:int64; begin read ...

  4. 51Testing招聘软件测试课程研发人员

    最近有些两三年测试工作经验的小伙伴对自己的下一个工作有些迷茫,感觉很难有技术的突破,毕竟公司不是学校,不会允许员工海阔天空的去尝试各种新的技术.现在我就送给这些好学上进的小伙伴一个礼物,51Testi ...

  5. 关于JNI程序中引用另外一个lib

    我最近在写一个j2se的程序,我用的是开源的org.amse.ys.zip包里的代码,这部分代码是在FBReaderJ里抽取的,但是其中包含了一些native的方法,需要用的zlib库,而FBRead ...

  6. Android定义的路径全局变量

    Android定义的路径全局变量 ifeq (,$(strip $(OUT_DIR))) OUT_DIR := $(TOPDIR)out endif DEBUG_OUT_DIR := $(OUT_DI ...

  7. ssh互信自动化脚本(待更新)

    1.建立一个ip,端口,用户,密码列表 [root@localhost shell-key]# cat arg_list.txt 172.16.56.215 172.16.56.215 172.16. ...

  8. UVa 10029 hash + dp

    题目链接:http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...

  9. reloadData should be in main thread

    reloadData should be called in main thread, so if you call it in work thread, you should call it as ...

  10. mac 别名 ll

    在 Linux 下习惯使用 ll.la.l 等ls别名的童鞋到 mac os 可就郁闷了~~ 其实只要在用户目录下建立一个脚本“.bash_profile”, vim .bash_profile 并输 ...