#看這篇就夠了--啟動優(yōu)化之二進制重排:理論及實踐(附源碼)

在做二進制重排之前,首先需要了解到幾個知識點.例如:物理內(nèi)存,虛擬內(nèi)存,內(nèi)存分頁管理

物理內(nèi)存

早期的操作系統(tǒng),只有物理內(nèi)存

當一個應(yīng)用啟動后,會全部加載到內(nèi)存中,并按照內(nèi)存真實地址排列

Pasted Graphic 5.png

這樣就會面臨一些問題,比如:

  • 內(nèi)存會不夠用
  • 不安全,因為在內(nèi)存中App是使用真實地址訪問,所以App可以訪問其以外的內(nèi)存

虛擬內(nèi)存(官方文檔)

MMU把虛擬內(nèi)存地址ow.png

iOS中,一個虛擬內(nèi)存與一個進程 一一對應(yīng),大小為4G, 虛擬內(nèi)存里會分為很多頁(Page),每頁的大小16KB

當有了虛擬內(nèi)存之后.CPU訪問進程數(shù)據(jù)相對上面的有了變化:

  • 一個進程啟動后,系統(tǒng)為該進程建立一個對應(yīng)的虛擬內(nèi)存,里面記錄了進程每項數(shù)據(jù)的虛擬內(nèi)存地址(比如圖中"進程1虛擬頁表")
  • 當進程的某部分活躍后,MMU(Memory Management Unit-內(nèi)存管理單元)會把這部分數(shù)據(jù)的虛擬內(nèi)存地址翻譯成其對應(yīng)的物理內(nèi)存地址,然后CPU通過物理地址訪問到物理內(nèi)存上的數(shù)據(jù)。
  • 如果在page上沒有找到對應(yīng)的物理地址時(圖中"進程1虛擬頁表的"P2),說明此page上所關(guān)聯(lián)的進程數(shù)據(jù)沒被加載到物理內(nèi)存中,此時會觸發(fā)缺頁異常(Page Fault),中斷當前進程,先將當前頁所對應(yīng)的進程數(shù)據(jù)加載到物理內(nèi)存中,然后page會記錄該項數(shù)據(jù)的物理地址,CPU再通過物理地址來訪問內(nèi)存上的數(shù)據(jù)(此過程耗時是毫秒級)

相比早期的純物理內(nèi)存,虛擬內(nèi)存的優(yōu)勢

  • 內(nèi)存使用更高效:進程的數(shù)據(jù)經(jīng)過分頁管理后,只將活躍的page所關(guān)聯(lián)的數(shù)據(jù)加載在物理內(nèi)存中,當物理內(nèi)存都被占用的時候,此時會覆蓋掉不活躍的內(nèi)存,加載當前活躍的page數(shù)據(jù),這樣就能提高對內(nèi)存的使用效率
  • 內(nèi)存數(shù)據(jù)更安全:每次啟動進程,系統(tǒng)都會重新建立對應(yīng)的虛擬內(nèi)存,并為虛擬內(nèi)存分配一個ASLR隨機值(Address Space Layout Randomization),數(shù)據(jù)的虛擬地址即為:ASLR隨機值+偏移值,這樣數(shù)據(jù)的虛擬地址每次都會變,并且CPU是通過虛擬內(nèi)存來間接訪問物理內(nèi)存的,在這個過程中物理內(nèi)存地址沒有暴露出來,所以就能保證內(nèi)存數(shù)據(jù)的安全性

ASLR (Address Space Layout Randomization)

首先如果沒有ASLR,虛擬內(nèi)存是有安全隱患的

  • 每個虛擬頁表開頭都是0 (0~4G).如果做靜態(tài)分析,定位到一個函數(shù),找到函數(shù)偏移地址,每次都可拿到該函數(shù)

ASLR可以彌補上述的安全缺陷

百度百科上ASLR的解釋:(Address Space Layout Randomization ) 地址空間配置隨機化;ASLR通過隨機放置進程關(guān)鍵數(shù)據(jù)區(qū)域的地址空間來防止攻擊者能可靠地跳轉(zhuǎn)到內(nèi)存的特定位置來利用函數(shù)

更直白的解釋就如上面提到的:每次啟動進程,系統(tǒng)都會重新建立對應(yīng)的虛擬內(nèi)存,并為虛擬內(nèi)存分配一個ASLR隨機值(Address Space Layout Randomization),數(shù)據(jù)的虛擬地址即為:ASLR隨機值+偏移值,這樣數(shù)據(jù)的虛擬地址每次都會變

了解上面的知識點后,下面會介紹二進制重排

二進制重排

上面有提到,當加載一個未加載到物理內(nèi)存的數(shù)據(jù)時,會觸發(fā)一個系統(tǒng)中斷 (PageFault), 雖然單次耗時是毫秒級,但是有一種情況會出現(xiàn)大量的PageFault,那就是App啟動, 通過二進制重排來減少App啟動速度的核心就是減少App啟動時PageFault的次數(shù)

在重排之前,我們可以先通過Link Map文件,來查看我們的項目加入到內(nèi)存時的默認順序是什么 (LinkMap記錄了二進制文件的布局)

  • xcode - build settings - Write Link Map File - 設(shè)為YES
  • run一下項目,然后到項目工程目錄的Products找到 xxx.app,右鍵show in finder
  • 往回退兩層目錄,然后按照目錄 Intermediates.noindex/項目名字.build/Debug-iphonesimulator/項目名字.build/項目名字-LinkMap-normal-arm64.txt 找到linkMap文件

內(nèi)容如下:


image.png

可以發(fā)現(xiàn)這個順序默認是按照Compile Source的順序,單個文件內(nèi)的不同方法是按照代碼書寫的順序

另外,我們還可以通過xcode - Instruments - System Trace來查看App啟動時的pageFault次數(shù)

如截圖:(用的是真實項目,項目相關(guān)信息打了馬賽克)


image.png

這里有個問題:app首次打開的時候Page Fault的次數(shù)很多,打開之后再打開的話就比較少,當打開多個其他app的時候,在打開檢測的app發(fā)現(xiàn)也會有不少Page Faults

這是由于操作系統(tǒng)的機制,當應(yīng)用殺掉了,他所訪問的物理內(nèi)存不是立馬就清空;它所訪問的物理內(nèi)存,需要通過其他app申請開辟覆蓋釋放掉,

我們要做的就是把啟動所需要的代碼,放在一起,放在最靠前的位置,減少啟動時非必要的pageFault次數(shù).

總結(jié)來講就是以下兩點

  • 找到App啟動時所需要調(diào)用的所有函數(shù)
  • 更改App數(shù)據(jù)加入到內(nèi)存的順序

獲取項目中啟動時刻所調(diào)用的方法順序

本文采用clang插樁方式:

原理: 在編譯時刻,在每個函數(shù)內(nèi)部,都會靜態(tài)插入方法__sanitizer_cov_trace_pc_guard,然后我們在項目中注冊其回調(diào)函數(shù),App每次調(diào)用方法(包括OC方法,C語言方法,block等所有方法),都會通過__sanitizer_cov_trace_pc_guard來回調(diào),由此我們可以記錄App啟動時所需的所有方法

  • 配置 other c Flags
build Settings - other c Flags
添加內(nèi)容為 -fsanitize-coverage=func,trace-pc-guard

注: 官方文檔上的-fsanitize-coverage=trace-pc-guard這種方式,會在while循環(huán)中同樣插入hook代碼,多次靜態(tài)加入__sanitizer_cov_trace_pc_guard調(diào)用,導(dǎo)致死循環(huán)
所以我們要加func參數(shù),代表只有hook函數(shù)時調(diào)用
  • 導(dǎo)入頭文件
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
  • 注冊回調(diào)函數(shù)

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {

  static uint64_t N;  // Counter for the guards.

  if (start == stop || *start) return;  // Initialize only once.

  printf("INIT: %p %p\n", start, stop);

  for (uint32_t *x = start; x < stop; x++)

    *x = ++N;  // Guards should start from 1.

}


void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

//    if (!*guard) return;

    void *PC = __builtin_return_address(0);

    Dl_info info;

    dladdr(PC, &info);

    printf("%s\n",info.dli_sname); //打印方法名字
}

image.png

可以通過LLDB來簡單調(diào)試一下,為了效果明顯,可以按照以下操作

  • 新建一個項目,隨便寫幾個方法,并把上述代碼加進去(加哪里都可以,可以加在ViewController.m)
  • 然了后再來個點擊相應(yīng)的方法,運行項目后,斷電加在__sanitizer_cov_trace_pc_guard(如圖), 然后點擊屏幕觸發(fā)點擊方法,走入斷點

可以通過log看到star和stop分別是0x102591898和0x1025918f8.
此時進行memory read,讀取一下最后的內(nèi)存地址里面的內(nèi)容

(lldb) x 0x1025918f4

這里為什么是0x1025918f8 - 0x4 ?

start.png

start和stop都是uint32_t類型,占4個字節(jié),end指向最后(如圖),所以要獲取最后一塊內(nèi)存地址中的內(nèi)容,需要減0x4

如果在這基礎(chǔ)上,再添加一個方法之后,同樣的操作獲取上述圖中紅框內(nèi)的數(shù)字,我們會發(fā)現(xiàn)圖中空框內(nèi)的數(shù)字正是方法的數(shù)量 (注:紅框內(nèi)的18是16進制,代表有個24個方法)

我們可以添加匯編代碼來看一下發(fā)生了什么

xcode - Debug - Debug Workflow - Always show Disassembly

給當前viewcontroller添加touchesBegan:withEvent:方法,并在方法內(nèi)部添加斷點,點擊屏幕后:

image.png

可以看出已經(jīng)給touchesBegan:withEvent:注入了方法__sanitizer_cov_trace_pc_guard.
此時如果在touchesBegan:withEvent:方法內(nèi)部再調(diào)用一個方法testMethod(), 通過斷點可以看到testMethod()方法內(nèi)部也會被注入__sanitizer_cov_trace_pc_guard方法.

上述記錄的方法是通過NSLog方式來打印的,如果在大型實戰(zhàn)項目中,我們可以考慮把方法名字寫入到本地文件, 我是參考了iOS啟動優(yōu)化:二進制重排這篇文章的方法,以下是全部代碼,可拿來直接用,BinarySortTool.h公開一個類方法+ (void)writeSortedFileMethod;可以在App啟動之后調(diào)用此方法,來寫入文件

//  BinarySortTool.m

//  Created by qwer on 2021/8/10.

#import "BinarySortTool.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
#import <libkern/OSAtomic.h>

@implementation BinarySortTool

+ (void)writeSortedFileMethod {

    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];

    while (YES) {

        //offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量

        SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));

        if (node == NULL) break;

        Dl_info info;

        dladdr(node->pc, &info);

        

        NSString * name = @(info.dli_sname);

        

        // 添加 _

        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];

        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];

        

        //去重

        if (![symbolNames containsObject:symbolName]) {

            [symbolNames addObject:symbolName];

        }

    }

    

    //取反

    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];

    //將結(jié)果寫入到文件

    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];

    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];

    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];

    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];

    if (result) {

        NSLog(@"%@",filePath);

    }else{

        NSLog(@"文件寫入出錯");

    }
}

//原子隊列

static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

//定義符號結(jié)構(gòu)體

typedef struct{

    void * pc;

    void * next;

}SymbolNode;


void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,

                                                    uint32_t *stop) {

  static uint64_t N;  // Counter for the guards.

  if (start == stop || *start) return;  // Initialize only once.

  printf("INIT: %p %p\n", start, stop);

  for (uint32_t *x = start; x < stop; x++)

    *x = ++N;

}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

    //if (!*guard) return;  // Duplicate the guard check.

    void *PC = __builtin_return_address(0);

    SymbolNode * node = malloc(sizeof(SymbolNode));

    *node = (SymbolNode){PC,NULL};

    //入隊

    // offsetof 用在這里是為了入隊添加下一個節(jié)點找到 前一個節(jié)點next指針的位置

    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));

}

@end


至此,我們會拿到.order的文件,由于項目隱私問題就不提供.order內(nèi)容的截圖了

拿到了.order文件后,就剩下最后一步了

更改App數(shù)據(jù)加入到內(nèi)存的順序

這一步相對上面的操作,就輕松很多了,直接去build settings設(shè)置一下order file的路徑即可

Pasted Graphic 6.png

到此,啟動優(yōu)化之二進制重排就結(jié)束了.我們可以通過上述介紹過的Instruments - System trace來驗證一下page fault次數(shù),不過要注意上述提到過當殺死App后,其所對應(yīng)的物理內(nèi)存的內(nèi)容不會立刻被清除的問題,可以嘗試多打開幾個App后再打開自己的項目,或者清除所有后臺然后關(guān)機開機.


參考:

抖音研發(fā)實踐:基于二進制文件重排的解決方案 APP啟動速度提升超15%

iOS啟動優(yōu)化:二進制重排

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容