你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 由App的啟動說起

由App的啟動說起

編輯:IOS開發基礎

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

“你是誰?從哪裡來?到哪裡去?”,這三個富有哲學氣息的問題,是每一個人在不斷解答的問題。我們Code,Build,Run,一個活生生的App躍然方寸屏上,這一切是如何發生的?從用戶點擊App到執行main函數這短短的瞬間發生了多少事呢?探尋App的啟動新生,可以幫助我們更了解App開發本身。

下圖是App啟動流程的關鍵節點展示:

mach-o_execution.png

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的層次結構:

QQ截圖20160115172819.png

其中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;        /* 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命令來查看下執行文件的信息,如新浪微博:

QQ截圖20160115172928.png

ps:上述說“大部分情況”是因為還有一部分,由於業務比較復雜,代碼量巨大,如果支持多種CPU架構而打包多個Mach-O文件的話,會導致ipa包變得非常大,所以就並沒有支持新的CPU架構的。如QQ和微信:

QQ截圖20160115173106.png

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_format.png

由上圖,我們可以看到Mach-O文件主要包含一下三個數據區: 

(1). 頭部Header:在頭文件定義了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位的文件頭數據結構,頭文件還定義了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_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/

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 內核調用流程

可執行文件的內核流程如下圖:

flow_of_process_execution.png

啟動進程的流程

引用自《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 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如何進行切換應用等,本文並未涉及到,有興趣的同學可以繼續深入研究。

參考資料:

  • 《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

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved