我們會借助一些崩潰日志收集庫來定位和排查線上的崩潰信息,但是有些崩潰堆棧所提供的信息有限又不是必現(xiàn)崩潰,很難直觀排查出問題的所在。這里我給大家分享一個采用寄存器賦值追蹤的技術(shù)來排查和分析崩潰日志的技巧。話不多說先看案例:
//符號化后的崩潰堆棧
Date/Time: 2021-03-25 04:35:38.211 +0800
OS Version: iOS 10.3.2 (14F89)
Report Version: 104
Monitor Type: Unix Signal
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x00000000 at 0x00000001808f1014
Crashed Thread: 27
Pthread id: 1313098
Thread 27 Crashed:
0 libsystem_kernel.dylib __pthread_kill + 8
1 libsystem_pthread.dylib pthread_kill + 112
2 libsystem_c.dylib abort + 140
3 libsystem_malloc.dylib szone_error + 420
4 libsystem_malloc.dylib free_list_checksum_botch.295 + 36
5 libsystem_malloc.dylib tiny_free_list_remove_ptr + 288
6 libsystem_malloc.dylib tiny_free_no_lock + 684
7 libsystem_malloc.dylib free_tiny + 472
8 CoreFoundation _CFRelease + 1228
//只有這句信息可以參考
9 testApp __99-[XXX fn:queue:]_block_invoke + 384
10 libdispatch.dylib _dispatch_call_block_and_release + 24
11 libdispatch.dylib _dispatch_client_callout + 16
12 libdispatch.dylib _dispatch_queue_serial_drain + 928
13 libdispatch.dylib _dispatch_queue_invoke + 884
14 libdispatch.dylib _dispatch_root_queue_drain + 540
15 libdispatch.dylib _dispatch_worker_thread3 + 124
16 libsystem_pthread.dylib _pthread_wqthread + 1096
//這是原始崩潰棧
Pthread id: 1313098
Thread 27 Crashed:
0 libsystem_kernel.dylib 0x00000001808f1014 __pthread_kill + 8
1 libsystem_pthread.dylib 0x00000001809bb264 pthread_kill + 112
2 libsystem_c.dylib 0x00000001808659c4 abort + 140
3 libsystem_malloc.dylib 0x0000000180931828 szone_error + 420
4 libsystem_malloc.dylib 0x000000018093b74c 0x180924000 + 96076
5 libsystem_malloc.dylib 0x0000000180928994 0x180924000 + 18836
6 libsystem_malloc.dylib 0x000000018093ba00 0x180924000 + 96768
7 libsystem_malloc.dylib 0x000000018093c0c8 0x180924000 + 98504
8 CoreFoundation 0x00000001818a701c 0x1817ca000 + 905244
9 testApp 0x0000000103b9e5b8 __99-[XXX fn:queue:]_block_invoke + 384
10 libdispatch.dylib 0x00000001807ae9e0 0x1807ad000 + 6624
11 libdispatch.dylib 0x00000001807ae9a0 0x1807ad000 + 6560
12 libdispatch.dylib 0x00000001807bcad4 0x1807ad000 + 64212
13 libdispatch.dylib 0x00000001807b22cc 0x1807ad000 + 21196
14 libdispatch.dylib 0x00000001807bea50 0x1807ad000 + 72272
15 libdispatch.dylib 0x00000001807be7d0 0x1807ad000 + 71632
16 libsystem_pthread.dylib 0x00000001809b7100 _pthread_wqthread + 1096
Binary Images:
0x1000b8000 - 0x108263fff +testApp arm64 <cb8cc0075ed4352b802c6c586b8a93d5> /var/containers/Bundle/Application/0A1F4541-8749-4F9D-B60A-813FFEE69CA6/testApp.app/testApp
從上面的崩潰信息大概可以看出這是一個GCD隊列線程調(diào)用時發(fā)生了崩潰。其中崩潰的第9層顯示是在一個[XXX fn:queue:] 方法內(nèi)定義的block內(nèi)部代碼發(fā)生了崩潰,除此之外沒有其它信息。崩潰方法的源代碼定義如下:
//這是一個簡化代碼
@interface TestObj:NSObject
-(NSString*)testString;
-(NSInteger)length;
@end
//代碼片段
-(void)fn:(TestObj*)testObj queue:(dispatch_queue_t)queue {
dispatch_async(queue, ^{
@autoreleasepool {
if ([testObj length] != 0) {
NSString *suffix = [testObj testString];
const static int len = 4;
if (suffix.length > len) {
suffix = [suffix substringToIndex:len];
}
}
}
});
}
從源代碼來看確實是在方法-[XXX fn:queue:]內(nèi)調(diào)用了一個dispatch_async,然后block也是定義在方法內(nèi)。不過依然沒辦法定位到是哪行代碼發(fā)生了崩潰, 同時也不是必現(xiàn)的線上崩潰。
所以要想查明原因需要到匯編代碼級別定位崩潰原因?。?步驟如下:
先下載可執(zhí)行文件到本地或者從CI發(fā)布部門獲取可執(zhí)行的app包并解壓。
用系統(tǒng)自帶的otool工具,進行代碼的反匯編處理。下面的otool命令格式可以用來顯示具體的函數(shù)或者方法的反匯編代碼:
otool "可執(zhí)行文件路徑" -p "函數(shù)或者方法名" -V -t
otool命令中 -p 后面跟的是方法名或者函數(shù)名或者符號名。 這里需要注意的時因為系統(tǒng)編譯時可能會在函數(shù)名或者符號名前多增加一個下劃線_ 因此在指定符號名時需要多增加一個_。 -V 是表明打印函數(shù)對應(yīng)的匯編代碼。 -t 是表明打印代碼段中的代碼。
本例的問題使用otool如下:
otool "/Users/apple/Downloads/Payload/testApp.app/testApp" -p "___99-[XXX fn:queue:]_block_invoke" -V -t
匯編出來的局部代碼如下:
/Users/apple/Downloads/Payload/testApp.app/testApp:
(__TEXT,__text) section
___99-[XXX fn:queue:]_block_invoke:
......... 省略部分代碼
0000000103ae6554<+284> mov x20, x0
0000000103ae6558<+288> ldr x0, [x20, #x20]
0000000103ae655c<+292> adrp x8, 26695 ; 0x10a32d000
0000000103ae6560<+296> ldr x1, [x8, #0x940] ; Objc selector ref: testString
0000000103ae6564<+300> bl 0x107fac9e8 ; Objc message: -[x0 testString]
0000000103ae6568<+304> mov x29, x29
0000000103ae656c<+308> bl 0x107faca48 ; symbol stub for: _objc_retainAutoreleasedReturnValue
0000000103ae6570<+312> mov x25, x0
0000000103ae6574<+316> mov x0, x26
0000000103ae6578<+320> bl 0x107faca18 ; symbol stub for: _objc_release
0000000103ae657c<+324> mov x0, x25
0000000103ae6580<+328> mov x1, x22
0000000103ae6584<+332> bl 0x107fac9e8 ; Objc message: -[x0 testString] //這里是otool的bug真實應(yīng)該是 length方法。
0000000103ae6588<+336> cmp x0, #0x5
0000000103ae658c<+340> b.lo 0x103ae65bc
0000000103ae6590<+344> adrp x8, 26674 ; 0x10a318000
0000000103ae6594<+348> ldr x1, [x8, #0xb30] ; Objc selector ref: substringToIndex:
0000000103ae6598<+352> mov x0, x25
0000000103ae659c<+356> mov w2, #0x4
0000000103ae65a0<+360> bl 0x107fac9e8 ; Objc message: -[x0 substringToIndex:]
0000000103ae65a4<+364> mov x29, x29
0000000103ae65a8<+368> bl 0x107faca48 ; symbol stub for: _objc_retainAutoreleasedReturnValue
0000000103ae65ac<+372> mov x22, x0
0000000103ae65b0<+376> mov x0, x25
0000000103ae65b4<+380> bl 0x107faca18 ; symbol stub for: _objc_release
0000000103ae65b8<+384> mov x25, x22
從上述的崩潰信息可以看出崩潰的地址是0x0000000103b9e5b8。根據(jù)這個地址可以得出崩潰是在我們反匯編代碼的倒數(shù)第二行。0x0000000103ae65b4<+380>
上述的兩個地址都不同,結(jié)論是如何得出的?
- 線上的程序運行時程序時會在一個隨機的基地址上加載,從崩潰的原始堆棧中最下面部分可以看到程序映像的基地址就是 0x1000b8000。因此:
0x0000000103b9e5b8 - 0x1000b8000 = 0x0000000103ae65b8
-
又因為一般程序崩潰的地址都有3個特征:
a. 崩潰堆棧層級中的非頂層地址都是函數(shù)調(diào)用指令的下一條地址也就是LR的值,所以真實的崩潰指令處是第1步算出的結(jié)果再減去4也就是實際崩潰的地址是:0x0000000103ae65b4b. 如果崩潰信息出現(xiàn)在最頂層時,一般的崩潰指令都是帶有內(nèi)存訪問的指令。假如崩潰是在第上面的第二條指令,也就是在
ldr x0, [x20, #x20]處時很大概率是訪問的內(nèi)存地址無效產(chǎn)生的崩潰。c . 如果崩潰信息出現(xiàn)在最頂層即無內(nèi)存訪問也無函數(shù)調(diào)用的指令時,這種崩潰一般是觸發(fā)了brk斷點指令,或者產(chǎn)生了其他一些無法可判斷的原因了。前者比較好定位,后者就很難了。
從上面匯編代碼倒數(shù)第二行<+384>處可以看出是一個對象調(diào)用了_objc_release進行釋放時導(dǎo)致了崩潰。而_objc_release內(nèi)部可能會調(diào)用_CFRelease方法,這也就是在上面崩潰信息堆棧中__99-[XXX fn:queue:]_block_invoke + 384的上面是_CFRelease函數(shù)了。
既然是因為對象調(diào)用_objc_release導(dǎo)致的內(nèi)存釋放異常。那么就需要繼續(xù)追蹤是哪個對象調(diào)用了_objc_release。
根據(jù)arm系統(tǒng)的函數(shù)調(diào)用ABI規(guī)則,以及從倒數(shù)第三行<+376>的匯編代碼中可以看出是執(zhí)行了一個 x0 = x25的操作,也就是x0對象是從x25賦值而來的。這時候我們就可以利用寄存器賦值追蹤的技巧,繼續(xù)往上查看x25又是在哪里被賦值。 往上的代碼可以看出在<+312>處的指令執(zhí)行了 x25 = x0的賦值操作,而x0的結(jié)果是上一條指令調(diào)用_objc_retainAutoreleasedReturnValue函數(shù)返回的結(jié)果。而_objc_retainAutoreleasedReturnValue的入?yún)⒂质巧弦粭l指令<+300>處的[x0 testString] 方法返回的結(jié)果。到這里為止我就可以從源代碼中推斷出是[testObj testString] 返回的結(jié)果對象在釋放時導(dǎo)致了崩潰了。
那接下來你就可以仔細查查源代碼[testObj testString]的方法哪里有問題了。并最終定位出異常原因。
下面就是用寄存器追蹤技術(shù)展示的追蹤推導(dǎo)圖:

小貼士:在arm64位系統(tǒng)中,函數(shù)的第一個參數(shù)用x0寄存器保存,OC方法調(diào)用的對象也是用x0寄存器保存,函數(shù)和方法的返回結(jié)果也是用x0寄存器保存。