iOS疑難Crash的寄存器賦值追蹤排查技術(shù)

我們會借助一些崩潰日志收集庫來定位和排查線上的崩潰信息,但是有些崩潰堆棧所提供的信息有限又不是必現(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)的線上崩潰。

所以要想查明原因需要到匯編代碼級別定位崩潰原因?。?步驟如下:

  1. 先下載可執(zhí)行文件到本地或者從CI發(fā)布部門獲取可執(zhí)行的app包并解壓。

  2. 用系統(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é)論是如何得出的?

  1. 線上的程序運行時程序時會在一個隨機的基地址上加載,從崩潰的原始堆棧中最下面部分可以看到程序映像的基地址就是 0x1000b8000。因此:
0x0000000103b9e5b8 - 0x1000b8000 = 0x0000000103ae65b8
  1. 又因為一般程序崩潰的地址都有3個特征:
    a. 崩潰堆棧層級中的非頂層地址都是函數(shù)調(diào)用指令的下一條地址也就是LR的值,所以真實的崩潰指令處是第1步算出的結(jié)果再減去4也就是實際崩潰的地址是:0x0000000103ae65b4

    b. 如果崩潰信息出現(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寄存器保存。

?著作權(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)容