当前位置:   article > 正文

APP的启动_darwin xnu 源码 分析

darwin xnu 源码 分析

从用户点击App到执行main函数这短短的瞬间发生了多少事呢?探寻App的启动新生,可以帮助我们更了解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的层次结构:

这里写图片描述
其中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的数据结构在头文件上有定义:


#define FAT_MAGIC   0xcafebabe

#define FAT_CIGAM  0xbebafeca  /* NXSwapLong(FAT_MAGIC) */

struct fat_header {

    uint32_t    magic;        

    uint32_t    nfat_arch;    

};

struct fat_arch {

    cpu_type_t  cputype;  

    cpu_subtype_t   cpusubtype;   

    uint32_t    offset;       

    uint32_t    size;     

    uint32_t    align;        

};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

结构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 Header的数据结构:

以上引用代码是32位的文件头数据结构,头文件还定义了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;     

    uint32_t cmdsize; 

};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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/

  1. 内核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: 由于源代码较多,篇幅所限,只引用关键性的代码。

execve(proc_t p, struct execve_args *uap, int32_t *retval) 

{

    __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)

    {

                exec_activate_image(struct image_params *imgp)

        {

            for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {
                error = (*execsw[i].ex_imgact)(imgp); 
                exec_mach_imgact(struct image_params *imgp)

                {

                    load_machfile(struct image_params *imgp, ...) 

                    {
                        if (create_map) {

                            vm_map_create();

                        }

                        if (!(imgp->ip_flags & IMGPF_DISABLE_ASLR)) {

                            aslr_offset = random();

                        }
                        parse_machfile(struct vnode *vp, ..., load_result_t *result)

                        {

                        }

                    }

                    if (load_result.unixproc) {
                        thread_setuserstack(thread, ap);

                    }

                    thread_setentrypoint(thread, load_result.entry_point);
                    stopprofclock(p);

                    execsigs(p, thread);

                    ...

                }
            }
        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

由于篇幅所限,本文就不对源码进行展开讲解。通过上述的调用树,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的入口点,程序启动完成;

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

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

)

{









    if (depth > 6) {

        return(LOAD_FAILURE);

    }



    depth++;







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

    }















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



























    for (pass = 1; pass validentry == 0)) {

            thread_state_initialize(thread);

            ret = LOAD_FAILURE;

            break;

        }















        offset = mach_header_sz;

        ncmds = header->ncmds;



        while (ncmds--) {









            lcp = (struct load_command *)(addr + offset);

            oldoffset = offset;

            offset += lcp->cmdsize;



            switch(lcp->cmd) {



            case LC_SEGMENT:

                if (pass != 2) 

                    break;



                ret = load_segment(lcp, header->filetype, control, file_offset, macho_size, vp, map, slide, result);

                break;



            case LC_SEGMENT_64:



                break;



            case LC_UNIXTHREAD:

                if (pass != 1)

                    break;







                ret = load_unixthread((struct thread_command *) lcp, thread, slide, result);

                break;



            case LC_MAIN:

                if (pass != 1)

                    break;

                if (depth != 1)

                    break;





                ret = load_main((struct entry_point_command *) lcp, thread, slide, result);

                break;



            case LC_LOAD_DYLINKER:

                if (pass != 3)

                    break;



                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:   

                break;



#if CONFIG_CODE_DECRYPTION

            case LC_ENCRYPTION_INFO:    

            case LC_ENCRYPTION_INFO_64:

                break;

#endif

            default:





                ret = LOAD_SUCCESS;

                break;

            }

            if (ret != LOAD_SUCCESS)

                break;

        }

        if (ret != LOAD_SUCCESS)

            break;

    }





    if (ret == LOAD_SUCCESS) { 



        if ((ret == LOAD_SUCCESS) && (dlp != 0)) {









            ret = load_dylinker(dlp, dlarchbits, map, thread, depth, dyld_aslr_offset, result);

        }

    }





    return(ret);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371

再来看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

)

{





    ret = get_macho_vnode(name, archbits, header,

        &file_offset, &macho_size, macho_data, &vp);

    if (ret)

        goto novp_out;



    *myresult = load_result_null;















    ret = parse_machfile(vp, map, thread, header, file_offset,

                         macho_size, depth, slide, 0, myresult);





    if (ret == LOAD_SUCCESS) {



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

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  1. 总结

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

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

参考资料:

《Mac OS X Internals: A Systems Approach》

《Mac OS X and iOS Internals : To the Apple’s Core》

XNU源代码

The App Launch Sequence on iOS

Mach-O Programming Topics

DYLD Detailed

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/891262
推荐阅读
相关标签
  

闽ICP备14008679号