最近字節跳動技術團隊放出了一篇文章:抖音研發實踐:基於二進制文件重排的解決方案 APP啓動速度提高超15%,提到經過重排 Mach-O中的二進制,減小啓動流程中的缺頁中斷次數,爲 App 節約了 200ms 左右的啓動時間(根據抖音目前的啓動速度估算),本着嚴謹的態度,本文將對這種技術方案的可行性和價值進行分析和驗證。html
應用程序啓動時,系統會爲應用分配虛擬內存,隨後將 Mach-O 載入,在調用符號時,若是 __TEXT, __text
段還未分配物理內存,會引發缺頁中斷,接下來內核纔會爲符號所在的頁實際分配物理內存,這類硬缺頁錯誤較爲耗時。再加上 iOS 的調頁策略不包含預加載,即缺頁時僅調入當前頁,並不會根據局部性原理主動調入後續頁面,若是啓動鏈路依賴的符號分散在多個頁上,將會引起很是屢次的缺頁中斷,經過將這些符號重排,使他們儘量集中在一些連續的區域,就能使得調入頁時儘量調入更多啓動鏈路的符號,減小缺頁次數,提升啓動速度。前端
筆者首先重啓了設備,隨後使用 Instruments 的 Systam Trace 分析了某中型 App 的冷啓動過程,發現缺頁次數多達 1.8W 次,單次的耗時在微秒到毫秒級範圍內,第一次缺頁發生在對 AliPaySDK 的初始化過程,啓動鏈路缺頁的總耗時超過了 300ms,拋開動態庫加載等不可避免的缺頁外,這其中還有很多的優化空間。git
筆者首先想經過閱讀 darwin-xnu 的源碼分析其對 Page Fault 的處理過程,奈何功力不足,內核函數讀起來十分吃力,所以找了另外一條路:經過構造一些分散在不一樣頁的符號並調用他們,而後分析缺頁中斷報告。github
爲了構造分散在不一樣頁的符號,筆者使用匯編寫了 4 個強制 16K 對齊的函數,使他們分散在 4 個頁上,彙編代碼以下:bash
.section __TEXT,__text,regular,pure_instructions
.global _m0, _m1, _m2, _m3
.p2align 14
_m0:
sub sp, sp, #48
stp x29, x30, [sp, #-32]
str x2, [sp, #-48]
mov x2, 0
bl _m1
add x2, x2, x0
bl _m2
add x2, x2, x0
bl _m3
add x2, x2, x0
mov x0, x2
ldp x29, x30, [sp, #-32]
ldr x2, [sp, #-48]
add sp, sp, #48
ret
.p2align 14
_m1:
mov x0, #1
ret
.p2align 14
_m2:
mov x0, #2
ret
.p2align 14
_m3:
mov x0, #3
ret
複製代碼
代碼的功能很簡單,m0 將 m1 ~ m3 的調用結果累加並返回,mx 將返回 x,關鍵點在於聲明 .p2align 14
來強制按照 16K 對齊,隨後咱們能夠看到二進制中這些符號之間的間隔剛好爲 16K,即剛好分散在了 4 個頁上:app
# Symbols:
# Address Size File Name
0x100008000 0x00004000 [ 4] _m0
0x10000C000 0x00004000 [ 4] _m1
0x100010000 0x00004000 [ 4] _m2
0x100014000 0x00000008 [ 4] _m3
複製代碼
隨後在 main.m 中調用 m0,這會引發 m0 -> m1 -> m2 -> m3 的串行調用,分析結果以下: less
其中高亮的行是非系統庫引發的第一次缺頁中斷,地址爲 0x100ac8000,減去程序的基址 0xac8000,能夠獲得符號的地址是 0x100008000,經過 MachOView 分析 Symbol Table 這就是符號 m0 的地址: tcp
接下來兩次,減去基址分別發生在 0x10000c000 和 0x100010000,絕不意外的,他們剛好是 m1 和 m2 的地址: 函數
這說明內核在處理缺頁中斷時並無預載入相鄰的頁,程序的運行過程是由缺頁中斷驅動的,且耗時在數十到數百微秒級,甚至能到毫秒級。以某個中型 App 爲例,整個 __TEXT,__text
段共計 41M,包含了約 2563 個頁,以最極端狀況,若是啓動鏈路不幸每一個頁都要雨露均沾一下,就會發生 2000 屢次缺頁中斷,若是按照每次 100us 計算,這將消耗 200 ms 以上。源碼分析
根據 Apple 官方文檔,其中提到了對重排符號的設置方法:
The path to a file which alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file is moved to the start of its section and laid out in the same order as in the order file. Order files are text files with one symbol name per line. Lines starting with a # are comments. A symbol name may be optionally preceded with its object file leafname and a colon (e.g. foo.o:_foo). This is useful for static functions/data that occur in multiple files. A symbol name may also be optionally preceded with the architecture (e.g. ppc:_foo or ppc:foo.o:_foo). This enables you to have one order file that works for multiple architectures. Literal c-strings may be ordered by quoting the string in the order file (e.g. 「Hello, world」). Generally you should not specify an order file in Debug or Development configurations, as this will make the linked binary less readable to the debugger. Use them only in Release or Deployment configurations.
簡言之,即建立一個文本文件,每行一個符號,將 order_file 的路徑配置到 Xcode 的 Build Settings 中的 Order File 配置項,隨後連接器就會按照 order_file 中的順序來排列符號了。注意這個地方有個坑,若是符號都是用匯編寫的,例如上文中的 m0 ~ m3,order_file 是不會生效的。
所以筆者在 main.m 中定義了兩個 C 函數來實驗重排:
int m4() {
return 4;
}
int m5() {
return 5;
}
複製代碼
他們在 Mach-O 中的默認分佈以下:
# Symbols:
# Address Size File Name
0x100010018 0x00000008 [ 2] _m4
0x100010020 0x00000008 [ 2] _m5
複製代碼
下面咱們在 order_file 中寫入排序規則:
_m5
_m4
複製代碼
再次 Build,查看 linkmap 結果:
# Symbols:
# Address Size File Name
0x100004000 0x00000008 [ 2] _m5
0x100004008 0x00000008 [ 2] _m4
0x100004010 0x00000208 [ 2] _main
0x100004218 0x00000088 [ 3] -[AppDelegate application:didFinishLaunchingWithOptions:]
複製代碼
能夠看到 _m5, _m4 被插入到了 __TEXT, __text
段的最前端,且按照 order_file 的順序進行了排序。
這一起主要有三個思路,靜態分析、堆棧採樣和 Hook。
靜態分析即從__TEXT, __text
段的 _main 方法開始,經過 capstone 等反彙編庫將機器碼轉成彙編代碼分析控制流,收集啓動鏈路上的符號,這裏比較有挑戰的是一些間接尋址的計算,若是簡單分析只能覆蓋到大多數符號。
這種方式即在啓動流程中以某個採樣頻率獲取當前調用棧,將這些函數和方法記錄爲啓動鏈路的符號,這種方式聽起來比較靠譜,因爲還沒有實踐,不知道準確度和效果如何。
最容易想到是 Hook objc_msgSend 來 cover 住啓動鏈路上全部的 OC 方法,對於 C/C++ 函數結合靜態分析方案便可;
小夥伴提到在越獄機上 Hook XNU 的 vm_fault 等函數,從源頭處分析引發缺頁的符號,理論上講經過 vm_map 等結構體分析虛擬內存的值可肯定哪些是由目標進程引發的缺頁,從而進一步定位到引發缺頁的段偏移量,進而找到符號,這裏有個疑問是進入 vm_fault 時的偏移量多是頁的起始地址,這時可能已經丟失了符號地址,只能定位到是哪一頁,沒法定位到具體符號,筆者是個內核小白,對這種方案也只是猜想,望大佬們指點。
通過一些實驗性的實踐和分析,二進制重排對啓動速度的影響彷佛是不可小覷的,並且因爲連接器自然支持了符號重排選項,下降了手動調整二進制文件帶來的極大風險,但不知道會不會引入其餘的坑。