iOS-底層原理32-啟動優化二進制重排

《iOS底層原理文章匯總》

上一篇文章《iOS-底層原理28-啟動優化》介紹了二進制重排能減少缺頁中斷PageFault的頁數,優化應用程序的啟動時間,那啟動時刻調用了哪些方法呢?此篇文章將分析啟動時刻調用的方法。進而將啟動時刻調用的方法盡量都放在集中的頁中,從而減少啟動時間。

clang插樁:參考官方文檔Tracing PCs

  • 1.添加標記,Build Settings -> 搜索Other C Flags,添加-fsanitize-coverage=trace-pc-guard
fsanitize-coverage=trace-pc-guard@2x.png
  • 2.編譯,會報兩個符號找不到的錯___sanitizer_cov_trace_pc_guard_init和___sanitizer_cov_trace_pc_guard,添加這兩個方法,說明配置了上面的標記位后,調用了這兩個函數
    根據官方文檔添加這兩個函數。
sanitizer_cov_trace_pc_guard@2x.png

報錯的方法找不到的情況,刪掉方法__sanitizer_symbolize_pc,注釋掉不用的代碼,能正常運行,此時運行,打印出結果,start和end的值分別為0x104ca1100和0x104ca1118

__sanitizer_symbolize_pc@2x.png
增加兩個方法@2x.png
打印結果@2x.png

開始地址start為0x104919100,讀取該開始地址下的內存值,一直讀取16個字節,一排有16個字節,4個字節4個字節的讀取,一直讀到結尾,stop指向的地址為0x104919118,指向數據最后的端,要獲取數據結尾處的數據應該往前再讀4個字節,因為uint32_t是無符號整形占用4個字節,06表示的十進制數為6

(lldb) x 0x104919100
0x104919100: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x104919110: 05 00 00 00 06 00 00 00 78 2f a7 04 01 00 00 00  ........x/......
(lldb) x 0x104919118-0x4
0x104919114: 06 00 00 00 78 2f a7 04 01 00 00 00 00 00 00 00  ....x/..........
0x104919124: 00 00 00 00 43 75 91 04 01 00 00 00 00 00 00 00  ....Cu..........
stop往前讀4個字節@2x.png

在ViewController增加一個方法touchBegin,此時發現結尾處的數據為07,這說明了啥?結尾處的數據存的就是方法的個數嗎?繼續在ViewController.m文件里面增加函數,block。無論是函數,方法,block都能獲取到。


增加方法touchsBegan@2x.png
增加函數和block@2x.png

增加一個屬性呢?增加了三個,setter,getter方法,還有一個cxx的析構函數。

增加屬性@2x.png
  • 綜上,上述哨兵函數能監聽方法,函數,block,屬性。

1.獲取符號地址:監聽方法的調用

點擊屏幕,調用了touchBegan方法,點一下監聽一次

39.gif

若touchBegan中調用了其他方法,則繼續監聽到了其他方法,__sanitizer_cov_trace_pc_guard全免捕捉到了函數,方法,block的調用

tochBegan-test-block@2x.png
40.gif

那么__sanitizer_cov_trace_pc_guard為什么能捕捉到所有函數的調用呢,原理是什么?
進入匯編代碼查看,sp操作棧空間,bl是跳轉函數,斷點進入test方法,查看匯編,發現只要是在Build Settings設置了讓clang開辟這個功能-fsanitize-coverage=trace-pc-guard,clang在讀取所有代碼的時候生成ir(中間代碼)之前,都將__sanitizer_cov_trace_pc_guard這個函數插入到每個函數、方法、block的開頭或邊緣,從而能進行監聽,從而每一個方法,函數,block的調用都會來到此函數void __sanitizer_cov_trace_pc_guard(uint32_t *guard)

OtherCFlags@2x.png
__sanitizer_cov_trace_pc_guard@2x.png
41.gif

避開load方法if (!*guard) return;,驗證下,注釋掉此行代碼,在ViewController類中重寫load方法,發現會多走一次方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard),從而多打印一次guard: 0x100e99388 0 PC

避開load方法@2x.png
42.gif

我們可以利用上面的void __sanitizer_cov_trace_pc_guard(uint32_t *guard)來進行clang插樁,目標是拿到所有調用的方法名

獲取方法名:引入void *PC = __builtin_return_address(0);,打印PC指針變量的地址,PC指向的地址0x0000000104b646f8和main函數的地址0x0000000104b646f8一致

INIT: 0x104b69388 0x104b693bc
(lldb) p PC
(void *) $0 = 0x0000000104b646f8
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
  * frame #0: 0x0000000104b64264 TraceDemo`__sanitizer_cov_trace_pc_guard(guard=0x0000000104b693b8) at ViewController.m:38:34
    frame #1: 0x0000000104b646f8 TraceDemo`main(argc=0, argv=0x0000000000000000) at main.m:12
    frame #2: 0x000000018212256c libdyld.dylib`start + 4
(lldb) 
PC@2x.png
  • 點擊屏幕touchBegan再次進行驗證,touchBegan方法的內存地址和PC內存地址是否一致,驗證結果一致
PC和touchBegan內存地址@2x.png
  • 查看匯編代碼,touchBegan怎么調用到函數void __sanitizer_cov_trace_pc_guard(uint32_t *guard)中去,void __sanitizer_cov_trace_pc_guard(uint32_t *guard)調用完畢,返回ret,還要返回到touchBegan方法中
touchBegan.png
touchBegan調用__sanitizer_cov_trace_pc_guard.png
__sanitizer_cov_trace_pc_guard.png

void __sanitizer_cov_trace_pc_guard(uint32_t *guard)調用完畢,返回ret,還要返回到touchBegan方法中

返回touchBegan.png

函數調用棧:調用棧里面的內存地址并不是函數的開始位置,查看匯編代碼可以得知函數棧中的touchBegan的地址為0x0000000102fa82f0,匯編代碼中touchBegan的地址為0x102fa82c4,兩者并不相等,所以函數棧中的地址并不是touchBegan的開始地址,而是上一個函數void __sanitizer_cov_trace_pc_guard(uint32_t *guard)調用后的返回地址

函數棧中地址并不是touchBegan的開始位置.png

這是有原因的,因為lldb的bt也是用的這個函數__builtin_return_address(0),驗證如下:刪除Other C Flags里面的哨兵函數標記-fsanitize-coverage=trace-pc-guard

刪除guard監聽.png

  • 在touchBegan函數里面調用兩個方法test()和block(),查看函數棧和匯編代碼,如果函數棧中的touchBegan的地址為函數的首地址,則在同一個函數中,函數的地址不變,則block的函數棧中touchBegin的地址也為函數的首地址,兩個值應該相等,若函數棧中的touchBegan的地址不為函數的首地址,為touchBegan上一個函數(test)或block()的返回地址,則查看test()和block()的匯編,就會發現不一致,兩個值并不相等,所以得出test()和block()返回后地址并不是touchBegan函數的首地址,而是test()和block()調用的返回地址
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    test();
    block();
}

void test(){
}
void (^block)(void)=^(void){
};
  • 斷點在test函數中查看bt,發現函數調用棧中touchBegan的地址為0x00000001001ec560
test.png
touchBegan函數調用棧.png
touchBegan調用test返回.png
  • 斷點在block中查看bt,發現函數調用棧中的touchBegan的地址為0x000000010270c578,和test函數中的地址不一致,因為0x000000010270c578和0x00000001001ec560根本就不是touchBegan的起始地址,而是test和block的返回地址,返回到了touchBegan中,這就lldb的原理
touchBegan調用block.png

blr

block中的地址.png

2.獲取符號

綜上,通過void *PC = __builtin_return_address(0);,PC為當前函數返回到上一個調用的地址,0代表我當前函數回到哪里去,1代表我上一個函數回到哪里去

address中參數為1返回上上個函數的地址@2x.png
  • 由上得知PC指向了當前函數返回到上一個函數調用的地址,怎么能拿到函數的符號呢?

引入#import <dlfcn.h>,通過dladdr(PC, &info);函數獲取到函數所在的MachO文件名和地址,以及函數符號名和地址

touchBegan調用test()和block()@2x.png
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("fname:%s \nfbase:%p\nsname:%s \nsaddr:%p\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
  char PcDescr[1024];
  printf("\nguard: %p %x PC %s\n", guard, *guard, PcDescr);
}

fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo 
fbase:0x1003c4000
sname:-[ViewController touchesBegan:withEvent:] 
saddr:0x1003cc268

guard: 0x1003d13a0 5 PC X\260m\261?
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo 
fbase:0x1003c4000
sname:test 
saddr:0x1003cc00c

guard: 0x1003d1394 2 PC \224\302<
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo 
fbase:0x1003c4000
sname:block_block_invoke 
saddr:0x1003cc028

guard: 0x1003d1398 3 PC ?m=
  • 此時拿到的地址是函數的起始地址,0x1003cc00c,為test函數的起始地址,通過dis -s 0x1003cc00c能查看
dis-s-地址@2x.png
  • TraceDemo的MachO的文件名為/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo,文件地址為0x1003c4000,同時可以通過image list鏡像拿到,拿到文件和動態庫的地址,都為虛擬地址。隨機值怎么獲取呢?ASLR的值,TraceDemo的MachO文件的首地址為0x00000001003c4000,則隨機值為0x3c4000,本身為000,加上隨機值3c4,從1開始為4個G的內存地址了,逆向班會講
image-list@2x.png
動態庫內存地址首地址@2x.png

3.符號拿到之后,生成相應的order文件

注意:在Build Settings ->Other C Flags,添加-fsanitize-coverage=trace-pc-guard后,在任何地方寫這兩個方法void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop)void __sanitizer_cov_trace_pc_guard(uint32_t *guard)都能監聽到,在main.m文件中實現都行,且只需要實現一次,在哪里結束寫在哪里

  • 1.在監聽方法里面存PC,啟動結束的方法里面取PC,此處有個坑點,在touchBegan方法的while循環中取符號方法,一直跳不出循環,什么導致循環無法停止呢?
取函數符號死循環.png
43.gif

因為while循環也被方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)hook了,循環一次hook一次,點擊屏幕,在循環中進入匯編查看,多的一次__sanitizer_cov_trace_pc_guard屬于循環,bl是條件跳轉,b是無條件跳轉,一次循環也會被hook一次,只要是跳轉(bl和b的匯編指令),就會被hook

touchBeganhook三次@2x.png

解決辦法:在Other C Flags中添加參數func,-fsanitize-coverage=func,trace-pc-guard,再次點擊屏幕,不會產生循環,打印的方法如下

sname:-[ViewController touchesBegan:withEvent:] 
sname:-[AppDelegate window] 
sname:-[ViewController viewDidLoad] 
sname:-[AppDelegate window] 
sname:-[AppDelegate window] 
sname:-[AppDelegate application:didFinishLaunchingWithOptions:] 
sname:-[AppDelegate setWindow:] 
sname:-[AppDelegate window] 
sname:main 
  • 2.load沒加載,被if (!*guard) return;直接return了,在void __sanitizer_cov_trace_pc_guard(uint32_t *guard)方法中刪除這一句
sname:-[ViewController touchesBegan:withEvent:] 
sname:-[AppDelegate window] 
sname:-[ViewController viewDidLoad] 
sname:-[AppDelegate window] 
sname:-[AppDelegate window] 
sname:-[AppDelegate application:didFinishLaunchingWithOptions:] 
sname:-[AppDelegate setWindow:] 
sname:-[AppDelegate window] 
sname:main 
sname:+[ViewController load] 
  • 3.去重,取反,不是OC方法添加_
     BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
     NSString * symbolName = isObjc ? name :[@"_" stringByAppendingString:name];
     [symbolNames addObject:symbolName];
     
     NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    //創建一個新的數組
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString *name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//數組中不包含name
            [funcs addObject:name];
        }
    }
    //去除touchBegan
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
  • 4.寫入cloud.order文件到沙盒中
    //數組轉成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    //字符串寫入文件
    //文件路徑
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cloud.order"];
    //文件內容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
  • 5.將生成的cloud.order文件拖入工程中,按照cloud.order文件進行編譯,重新運行,查看運行順序是否和配置的OrderFile文件一致
cloud.order放入工程根目錄@2x.png
Build Settings order file配置@2x.png

將Build Settings中Write Link Map File選項改為Yes,運行程序,在Product同級目錄Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-LinkMap-normal-arm64.txt中查看文件中每一個方法的排列順序

函數按cloud.order文件執行順序@2x.png
44.gif
  • 6.若調用了swift的函數呢?應該怎么處理,在Build Settings中設置-sanitize-coverage=func -sanitize=undefined
swift配置Other Swift Flags@2x.png
45.gif
order文件中的swift函數@2x.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容