從APP的啓動提及

iOS裏面APP的啓動,過程有些複雜,今天咱們來抽絲剝繭,一步步探討一下APP的啓動會經歷哪些過程。html

首先,用戶點擊iPhone裏面的某個APP的icon,Kernel內核會開始初始化空間並建立進程, 在調用exec_active_image後,開始加載Mach-O文件。macos

這裏咱們簡要說一下Mach-O文件。數組

Mach-Osass

Mach-O是iPhone下的可執行文件格式,咱們的APP對應的ipa文件,解壓縮之後就會看到這個Mach-O文件,咱們能夠用MachOView這個軟件來查看一下,如圖:架構

(注:這裏使用的是x86架構下的mach-o文件,也就是模擬器生成的,若是是arm架構的話會有一些區別,不過區別不大,總體結構差很少)app

 咱們拿其中幾個比較重要的來說解一下。ide

Mach64 Header:描述了Mach-O的CPU架構、文件類型以及加載命令等信息。函數

Load Commands:一系列的加載的命令集合,在Mach-O文件加載的時候用於給kernel和dyld調用,如圖:學習

LC_SEGMENT_64(__PAGEZERO):映射虛擬內存的第一頁地址和大小,通常是4G(0x1000000)大小。spa

LC_SEGMENT_64(__TEXT):代碼段的Header,裏面記錄了__TEXT的各類類型的偏移地址,如圖:

代表了__stubs的偏移地址以及一些相關的頭信息,其餘的Header也相似。

LC_SEGMENT_64(__DATA):數據段,裏面記錄的信息也是偏移地址和一些相關頭信息。

LC_SEGMENT_64(__LINKEDIT):記錄的是動態連接相關的偏移地址和頭信息(主要是dyld),動態連接十分重要,咱們在後面會說到。

LC_DYLD_INFO_ONLY:記錄了動態連接的rebase,binding,lazy binding等的頭信息和偏移地址。

LC_SYMTAB:符號表的信息,記錄符號表的位置,偏移量,數據個數等。一般跟Symbol Table還有String Table一塊兒來查找符號地址,以下圖:

在__Text代碼段找到代碼-[XFCorrelationNewsJSExport onload]的符號地址:0x1000014E0,經過LC_SYMTAB中的Symbol Table Offset找到地址 0x0012C218,而後根據此地址找到Symbols -[XFCorrelationNewsJSExport onload] 的偏移地址 0x00006D70 與 String Table的起始地址相加後計算出符號地址爲:0x0017DB7C,而後就能夠找到咱們符號對應的字符串,若是要收集crash,也就能夠拿到符號地址對應的符號的名字了。

LC_LOAD_DYLINKER:該Mach-O使用的連接器信息,記錄了具體使用哪一個連接器接管內核後續的加載工做,以及連接器的位置信息。

LC_LOAD_DYLIB:依賴庫信息,dyld會經過這個段去加載動態庫。列出了全部依賴的動態庫。

Mach-O文件就暫時介紹到這裏,後續提到動態連接器(dyld),動態庫(dylib),動態庫的延遲綁定問題時,還會繼續介紹Mach-O相關的Section。

這裏分享一點關於Mach-O的小感悟,一開始我在看Mach-O文件的各個section和segment的時候,以爲這麼多的section,這麼多的segment,我怎麼可能搞清楚每個都是幹什麼的,就算搞清楚了,時間長了也會忘記。後來我仔細想了一下,以爲Mach-O只是一種操做系統認識的可執行文件格式,因此他的各個section或者segment都是爲了在不一樣的時候和不一樣的階段提供不一樣的信息給操做系統使用的,因此,我我的認爲,只須要了解他的大體結構(MachHeader)和比較核心的幾個點(Load Commands,動態庫和動態連接相關)就能夠了。

在加載了Mach-O後,會開始載入動態連接器。

咱們來簡要說一下動態連接器。

動態連接器

在介紹動態連接器以前,咱們有必要先介紹一下什麼是連接,什麼是動態連接。

連接

連接就是經過連接器將執行文件中引用的其餘符號(變量和方法)作地址重定位的過程。連接分爲:靜態連接和動態連接。

靜態連接

如今假設文件A,裏面有方法 a(),方法a()裏面引用了文件B裏面的方法b(),那麼在編譯器編譯的時候,會將方法a裏面調用的方法b的地址以0x0,0x2等這些來暫時代替,而後輸出可執行文件C,等到調用靜態連接器的時候,由靜態連接器來將真實的方法b的地址(這裏的真實地址實際上是指的虛擬地址)修改到C對應的位置上。

這裏有個問題就是靜態連接器如何知道哪些符號的地址須要重定位呢?

由於在編譯A的時候,會生成一個重定位表,裏面記錄了哪些符號須要被重定位。

動態連接

動態連接區別於靜態連接在於連接的時機不一樣,靜態連接是編譯的時候作連接,而動態連接是在APP啓動時作連接,並且對於動態庫而言,裏面的方法並不會作連接操做,只有當第一次運行到這個方法時,纔會去作連接操做,從而獲得真正的地址,這也叫:延遲綁定。

動態連接主要是針對動態庫(dylib,或者也能夠叫共享庫)的連接操做,在系統的/usr/lib目錄下,存放了大量供系統與應用程序調用的動態庫文件。動態庫不能直接運行,而是須要經過系統的動態連接器(dyld)進行加載到內存後執行,當dyld加載完動態庫之後,不一樣的APP可使用一樣的動態庫(跨進程共享代碼和部分數據)。可是須要注意的是,對於各進程共享的部分,只包括代碼和不須要修改的數據部分,對於會變更的數據部分,是會被分離出來,每一個進程一個副本。

這裏有一個問題,就是如何才能在各個進程間共享能夠共享的動態庫的代碼和無需修改的數據呢?

由於各進程調用動態庫的地址都是各個進程的虛擬地址,彼此獨立,因此你沒辦法修正動態庫的代碼的地址來適應全部進程調用,因而有人想到了用絕對地址,雖然能夠知足這一要求的,可是會帶來新的問題,即:

- 程序每引入一個共享庫或者共享庫更新後佔用空間更大,就須要預留更大的虛擬空間(可是事實上並非每一個函數都會被調用到),可執行文件或許就要從新編譯。
- 共享對象更新時,內部的符號地址可能變化,可執行文件又得從新編譯。

因此用到了地址無關代碼 (PIC, Position-independent Code) 技術:

不管目標模塊(包括共享目標模塊)被加載到內存中的什麼位置,數據段老是緊跟着地址段的。所以,代碼段中的任意指令與數據段中的任意變量之間的距離在運行時都是一個常量,而與代碼和數據加載的絕對內存位置無關。

例子:

 1 //動態庫代碼 Person.h
 2 extern const NSString * _Nonnull str;
 3 
 4 extern int add(int a, int b);
 5 
 6 NS_ASSUME_NONNULL_BEGIN
 7 
 8 @interface Person : NSObject
 9 
10 - (void)printStr:(NSString *)str;
11 
12 @end
13 
14 //動態庫代碼 Person.m
15 const NSString * _Nonnull str  = @"abc";
16 
17 int add(int a, int b) {
18     return a + b;
19 }
20 
21 @implementation Person
22 
23 - (void)printStr:(NSString *)str {
24     
25     NSLog(@"sss:%@", str);
26 }
27 
28 @end
29 
30 //另外一個項目引入動態庫後調用的代碼
31 - (void)viewDidLoad {
32     [super viewDidLoad];
33     // Do any additional setup after loading the view.
34     Person *person = [[Person alloc] init];
35     [person printStr:@"ttt"];
36     
37     NSLog(@"%@", str);
38     
39     NSLog(@"%d", add(3, 5));
40 }

動態連接對於數據引用和方法引用,處理的方式有些區別。

數據引用:

編譯器在代碼段和數據段之間建立了一個GOT(Global Offset Table,全局偏移表),裏面存儲的是目標模塊引用的動態庫中的變量,如圖:

 

初始狀態下,這些GOT中的地址都是0x0,到了app啓動的時候,在Binding階段(後面會講到)動態連接器會將GOT中的數據地址都作一次修正。由於GOT是一個數組,因此修正的方式比較簡單,即:GOT[n] = 代碼段的地址 + 代碼段與數據段的固定偏移 + GOT數據大小

方法引用(延遲綁定):

編譯器在編譯的時候會在__TEXT,__stubs裏面將動態庫的add方法生成一個佔位,這個佔位主要用來指向__DATA,____la_symbol_ptr裏面對應的項,如圖:

當運行到上面的代碼第39行,目標函數調用動態庫中的add方法,對應彙編如圖:

bl是彙編指令,跳轉到子程序的意思,使用Hopper Disassembler查看一下彙編,如圖:

ldr:將內存中的值存入到寄存器x16中,此時0x10000c018正好對應__DATA,____la_symbol_ptr中的項,

br:x16  跳轉到x16指向的地址,如圖:

第一次調用add方法的時候,__DATA,____la_symbol_ptr裏面還沒有記錄add的地址,而是指向__TEXT,__stub_helper裏面相關的內容(0x0000001000065E4),如圖:

w16:寄存器x16的低32位

.long 0x0000003f 找尋Dynamic Loader Info 中Lazy Binding Info的偏移3f的符號

上述代碼的意思就是:跳轉到__TEXT,__stub_helper頭部(65CC),而後調用 dyld_stub_binder(動態連接器的入口) 進行符號綁定,最後會將 add 的地址放到 __la_symbol_ptr 處,下次再調用就能夠直接取add的地址調用了。

繞了這麼大一圈終於完成了方法的綁定,簡化一下:

生產stub佔位 -> 運行時調用 -> 指向la_symbol_ptr -> 若是有地址則返回地址,若是沒有地址則指向stub_helper -> 調用dyld_stub_binder來綁定方法地址並修正la_symbol_ptr的地址。

這裏會產生一個問題,爲何須要la_symbol_ptr,直接在stub裏面修改地址不就完了嗎?

由於stub是代碼段,而代碼段是隻讀的,動態庫的指導思想就是共享代碼段,分離出可變數據段,因此須要la_symbol_ptr。

綜上所述,咱們能夠簡單羅列一下靜態連接庫和動態連接庫的區別:

一、靜態連接庫在編譯後,庫裏的方法及變量地址就肯定了(虛擬地址),動態連接庫則是在運行時才能肯定,而動態庫中的方法則須要到調用到的時候才能肯定。

二、靜態連接庫會打包進APP中,而動態連接庫則在系統的/usr/lib目錄下,若是是本身製做的動態庫,也會隨着APP一塊兒打包進去。

動態連接器(dyld)
蘋果操做系統的重要組成部分,負責連接和裝載動態庫,當xnu內核(開源的系統底層代碼,下載地址)加載了動態連接器之後,APP將從內核態過分到用戶態。

dyld自己也是mach-o格式的文件,可是dyld中不會再引用其餘動態庫的東西,因此就不存在動態綁定這個過程了,拿MachOView看看如圖:

動態連接器也是開源的,下載地址

接下來App的啓動就進入Rebase,Binding階段了。

這幾個階段都是由dyld來控制的,咱們來簡單分析一下他的這幾個過程

Rebasing

在過去,會把 dylib 加載到指定地址,全部指針和數據對於代碼來講都是對的,dyld 就無需作任何 fix-up 了。現在用了 ASLR 後會將 dylib 加載到新的隨機地址(actual_address),這個隨機的地址跟代碼和數據指向的舊地址(preferred_address)會有誤差,dyld 須要修正這個誤差(slide),作法就是將 dylib 內部的指針地址都加上這個偏移量,偏移量的計算方法以下:

Slide = actual_address - preferred_address

Binding

主要是針對那些外部符號作的綁定操做,好比咱們上面說的GOT中的內容。

剩餘啓動事件

App啓動到這裏接下來就是進入到Runtime環節,會初始化Runtime環境並初始化,處理category和調用+load()方法。

initializers 調用全部動態庫的initializer方法,初始化動態庫。

調用App的main函數,正式進入App的生命週期。

小結

App的啓動咱們來回顧一下,主要分爲:加載Mach-O、加載dyld、rebase、binding、加載dylib,Runtime、Initializer、main這幾個過程,咱們主要講解了一下Mach-O的文件結構,動態連接的GOT和動態綁定過程,還簡單介紹了rebase和binding。

能夠看出來,App的啓動過程十分複雜,還有不少細節和知識點須要咱們仔細深刻研究和學習。

相關文章
相關標籤/搜索