iOS里面APP的啟動,程序有些復雜,今天我們來抽絲剝繭,一步步探討一下APP的啟動會經歷哪些程序,
首先,用戶點擊iPhone里面的某個APP的icon,Kernel內核會開始初始化空間并創建行程, 在呼叫exec_active_image后,開始加載Mach-O檔案,

這里我們簡要說一下Mach-O檔案,
Mach-O
Mach-O是iPhone下的可執行檔案格式,我們的APP對應的ipa檔案,解壓縮以后就會看到這個Mach-O檔案,我們可以用MachOView這個軟體來查看一下,如圖:

(注:這里使用的是x86架構下的mach-o檔案,也就是模擬器生成的,如果是arm架構的話會有一些區別,不過區別不大,整體結構差不多)
我們拿其中幾個比較重要的來講解一下,
Mach64 Header:描述了Mach-O的CPU架構、檔案型別以及加載命令等資訊,
Load Commands:一系列的加載的命令集合,在Mach-O檔案加載的時候用于給kernel和dyld呼叫,如圖:

LC_SEGMENT_64(__PAGEZERO):映射虛擬記憶體的第一頁地址和大小,一般是4G(0x1000000)大小,
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 onl oad]的符號地址:0x1000014E0,通過LC_SYMTAB中的Symbol Table Offset找到地址 0x0012C218,然后根據此地址找到Symbols -[XFCorrelationNewsJSExport onl oad] 的偏移地址 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,
|
綜上所述,我們可以簡單羅列一下靜態鏈接庫和元件的區別: 1、靜態鏈接庫在編譯后,庫里的方法及變數地址就確定了(虛擬地址),元件則是在運行時才能確定,而動態庫中的方法則需要到呼叫到的時候才能確定, 2、靜態鏈接庫會打包進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的啟動程序十分復雜,還有很多細節和知識點需要我們仔細深入研究和學習,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/106772.html
標籤:其他
