手淘架構組最新實踐 | iOS基於靜態庫插樁的⼆進位制重排啟動最佳化

阿里巴巴淘系技術發表於2020-01-07
手淘架構組最新實踐 | iOS基於靜態庫插樁的⼆進位制重排啟動最佳化
本文知識點提煉:
1、APP 啟動時 PageFault 的效能分析 
2、靜態庫插樁重排方案的技術原理

背景

近期抖音和 Facebook 分享了自己透過二進位制重排最佳化啟動時間的方案,手淘 iOS 架構團隊也對二進位制重排進行了研究,由於手淘工程模組已經二進位制化,因此實現了一套基於靜態庫插樁的重排方案。

▐  APP 啟動 和 PageFault

當我們向作業系統申請記憶體時,作業系統並不是直接分配給我們實體記憶體,而是隻標記當前程式擁有該段記憶體,當真正使用這段記憶體時才會分配。這種延遲分配實體記憶體的方式就透過 page fault 機制來實現的。當我們訪問一個記憶體地址時,如果該地址非法,或者我們對其沒有訪問許可權,或者該地址對應的實體記憶體還未分配, cpu 都會生成一個 page fault ,進而執行作業系統的 page fault handler 。如果是因為還未分配實體記憶體,作業系統會立即分配實體記憶體給當前程式,然後重試產生這個 page fault 的記憶體訪問指令。
手淘架構組最新實踐 | iOS基於靜態庫插樁的⼆進位制重排啟動最佳化
App 在啟動時,需要執行各種函式,我們需要讀取 TEXT 段程式碼到實體記憶體中,這個過程會發生缺⻚中斷,由於啟動時所需要執行的程式碼分佈在 TEXT 段的各個部分,會讀取很多⻚面,導致啟動時 Page Fault 數量非常多。與直接訪問實體記憶體不同, page fault 過程大部分是由軟體完成的,消耗時間比較久,所以是影響啟動效能的一個關鍵指標。

例如下圖中,手淘啟動時首先的呼叫的幾個方法 會分佈在虛擬記憶體的各個⻚面中, 執行這些方法時,需要從讀取到物理內容中,就會產生多次 page fault 。

如果能將啟動階段需要的讀取程式碼集中排布,將這些方法全都放到相鄰的區域中,我們讀取這些方法可能就只需要極少的 page fault 次數。可以減少不必要的 page fault 時間。達到最佳化啟動時間的效果。

重排前後的函式在頁面的佈局對比:

手淘架構組最新實踐 | iOS基於靜態庫插樁的⼆進位制重排啟動最佳化

重排方案

▐  如何獲取方法的執行順序

為了生成 order_file , 我們需要確定應用啟動時方法的執行順序。之前抖音和 facebook 都分享過自己的方案,在實際操作的過程中,我們發現抖音和 facebook 的方案並不適用於手淘。

抖音透過靜態掃描和執行時 Trace 等方法確定 order_file,該方案無法覆蓋 initialize、block 和 C++ 透過暫存器的間接函式呼叫靜態掃描不出來呼叫。

facebook 分享過透過 llvm 插樁的確定 order_file 的方案,需要使用原始碼重新打包。由於手淘幾乎全是已經編譯好的二進位制模組,在手淘使用該方案不現實。

只能想其他辦法...

手淘之前已經做過 pod 預編譯,我和師兄念紀想到了是否可以透過在彙編層面對 pod 編譯後的靜態庫進行插樁。在啟動時,插樁後的方法都會呼叫記錄方法,從而獲得啟動方法的執行順序。在參考了離青對彙編插樁的研究後,確定了靜態庫插樁的實現方案。

▐  靜態庫插樁


我們編譯過的靜態庫由 .o 檔案組成,我們可以對 .o 中的函式程式碼進行修改,在每個函式的開頭插入呼叫我們指定記錄函式的指令。

舉個例子:

插入前 -[MyApp window]: 的彙編程式碼
-[MyApp window]:
0000000000002d88 adrp x8, #0x
0000000000002d8c ldrsw x8, [x8, #0xf18]
; 0x2f18@PAGEOFF, _OBJC_IVAR_$_MyApp._window
0000000000002d90 ldr x0, [x0, x8]
0000000000002d94 ret

插入後的 彙編程式碼,可以看到 增加了跳轉到 _record_method 的指令,並且補上了 prologue 和 epilogue 。
-[MyApp window]:
0000000000002ebc stp x29, x30, [sp, #-0x10]!
0000000000002ec0 mov x29, sp
0000000000002ec4 bl _record_method
0000000000002ec8 ldp x29, x30, [sp], #0x
0000000000002ecc adrp x8, #0x
0000000000002ed0 ldrsw x8, [x8, #0xc0]
0000000000002ed4 ldr x0, [x0, x8]
0000000000002ed8 ret

▐  生成 order file


linkmap 記錄了連線過程中的相關資訊。其中包含連結用到的 symbol 相關的資訊。透過 pc address 減去 slide 得到的地址,我們可以在 linkmap 中找到對應的 symbol .
address = pc - slide. // 因為ASLR, APP 可執行檔案隨機載入的原因,需要處理一下偏移
量。
我們需要將之前記錄的地址轉換成對應的符號,為了真實還原線上的執行環境,我們只是在 app 中簡單地的記錄了 pc 地址 和 Image 的偏移量。透過解析 linkmap ,獲取函式的地址區間, 得到距離 address 最近的 symbol ,生成 order_file 。
linkmap 檔案:
# Symbols:
# Address Size File Name
0x100001630 0x00000039 [ 2] -[ViewController viewDidLoad]
0x100001670 0x00000092 [ 3] _main
0x100001710 0x00000080 [ 4] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100001790 0x00000040 [ 4] -[AppDelegate applicationWillResignActive:]
0x1000017D0 0x00000040 [ 4] -[AppDelegate applicationDidEnterBackground:]
0x100001810 0x00000040 [ 4] -[AppDelegate applicationWillEnterForeground:]
0x100001850 0x00000040 [ 4] -[AppDelegate applicationDidBecomeActive:]
0x100001890 0x00000040 [ 4] -[AppDelegate applicationWillTerminate:]

▐  更改符號的排列順序


預設情況下, ld 連結器會按照連結的順序將各個 .o 檔案的資料重新佈局生成可執行檔案。ld 連結器提供 -
order-file 選項操控資料排列的順序。在 Xcode 中可以透過 Order File 選項指定符號排序檔案。
//Order file 內容例子:
+[xxxxx1 load]
+[xxxxx2 swizzleResumeAndSuspendMethodForClass:]
+[xxxxx3 load]
+[xxxxx4 initialize]___
+[xxxxx5 initialize]_block_invoke
+[xxxxx6 initialize]___
+[xxxxx7 initialize]_block_invoke
...

最佳化效果

透過精準的啟動函式重排,最後重排效果還是很可觀的,在 iPhone6 上最佳化了400ms 的啟動時間。

參考

感謝抖音團隊和 Facebook 團隊提供最佳化新思路
抖音研發實踐:基於二進位制檔案重排的解決方案 APP啟動速度提升超15%
Improving iOS Startup Performance with Binary Layout Optimizations
https://atscaleconference.com/videos/performance-scale-improving-ios-startup-performance-with-binary-
layout-optimizations/
Linux下Page Fault的處理流程 https://cloud.tencent.com/developer/article/1459526

相關文章