iOS crash 日誌堆疊解析

leeeisok發表於2018-04-24

日常開放中,我們難免遇到一些 crash。大部分情況下,Xcode 可以幫助我們找到問題所在,但也有些情況,Xcode 給我們反饋的是一些看不懂的地址,大大增加了我們分析問題的難度。

下面,就來介紹幾種能讓看不懂的地址,變得看的懂的方式。

symbolicatecrash

.dSYM 檔案

dSYM 是儲存十六進位制函式地址對映資訊的中轉檔案,我們除錯的 symbols 都會包含在這個檔案中。每次編譯專案的時候都會生成一個新的 dSYM 檔案,我們應該儲存每個正式釋出版本的 dSYM 檔案,以備我們更好的除錯問題。一般是在我們 Archives 時儲存對應的版本檔案的,裡面也有對應的 .dSYM.app 檔案。路徑為:

~/Library/Developer/Xcode/Archives
複製程式碼

.dSYM 檔案預設在 debug 模式下是不生成的,我們去 Build Settings -> Debug Information Format 下,將 DWARF 修改為 DWARF with dSYM File,再重新編譯下就能生成 .dSYM 檔案了,直接去專案工程的 Products 目錄下找就行。

symbols 又是什麼呢?

引用 《程式設計師的自我修養》中的解釋:

在連結中,我們將函式和變數統稱為符號(Symbol),函式名或變數名就是符號名(Symbol Name)。我們可以將符號看作是連結中的粘合劑,整個連結過程正是基於符號才能夠正確完成。

所以,所謂的 symbols 就是函式名或變數名

找到 symbolicatecrash

symbolicatecrash 是 Xcode 自帶的 crash 日誌分析工具,我們需要先找到它:

find /Applications/Xcode.app -name symbolicatecrash -type f
複製程式碼

執行完後會返回幾個路徑,我的是:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
複製程式碼

我們到這個路徑下把 symbolicatecrash 拷貝出來,放到一個資料夾下。

拿到 crash 日誌檔案

我們可以隨便寫段強制 crash 的程式碼:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
	NSArray *arr = @[];
	arr[100];
}
複製程式碼

接著用真機打個包。打好包之後,不要用 Xcode build直接用打好的包執行我們能導致 crash 的程式碼,這樣就生成好 .crash 日誌檔案了。

之後,我們去 Xcode -> Window -> Devices and Simulators 或者快捷鍵 Command + Shift + 2

iOS crash 日誌堆疊解析

找到對應時間點的 .crash 檔案,右擊 Export Log。

拿到 .app 檔案

.app 檔案可以使用真機編譯下,去 專案 Products 目錄下獲取,也可以去 Archives 目錄下獲取。

符號解析

利用 dSYM

.dSYM.crashsymbolicatecrash 放到同一個檔案下,執行命令:

./symbolicatecrash .crash檔案路徑 .dSYM檔案路徑 > 名字.crash
複製程式碼

利用 app

.app.crashsymbolicatecrash 放到同一個檔案下,執行命令:

./symbolicatecrash .crash檔案路徑 .app/appName 路徑 > 名字.crash
複製程式碼

可能會報錯誤:

Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
複製程式碼

執行下命令就行:

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
複製程式碼

然後再重新生成下新的 .crash 檔案就行。

我們可以對比下沒有符號化和符號化的檔案,下面是我自己測試的例子,iPhone5 iOS 10.2, 可能會有所不同:

Last Exception Backtrace:
0   CoreFoundation                	0x1df60df2 __exceptionPreprocess + 126
1   libobjc.A.dylib               	0x1d1c3072 objc_exception_throw + 33
2   CoreFoundation                	0x1dee62f2 -[__NSArray0 objectAtIndex:] + 105
3   DreamDemo                     	0x0008088e 0x7c000 + 18574
4   UIKit                         	0x2319eb44 forwardTouchMethod + 289
5   UIKit                         	0x2319ea10 -[UIResponder touchesBegan:withEvent:] + 29
6   UIKit                         	0x23041c58 -[UIWindow _sendTouchesForEvent:] + 1599
7   UIKit                         	0x2303ca62 -[UIWindow sendEvent:] + 2657
8   UIKit                         	0x2300d870 -[UIApplication sendEvent:] + 315
9   UIKit                         	0x237a8998 __dispatchPreprocessedEventFromEventQueue + 2615
10  UIKit                         	0x237a25de __handleEventQueue + 829
11  CoreFoundation                	0x1df1c716 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 7
12  CoreFoundation                	0x1df1c220 __CFRunLoopDoSources0 + 433
13  CoreFoundation                	0x1df1a4f6 __CFRunLoopRun + 757
14  CoreFoundation                	0x1de6952e CFRunLoopRunSpecific + 481
15  CoreFoundation                	0x1de6933c CFRunLoopRunInMode + 99
16  GraphicsServices              	0x1f640bf8 GSEventRunModal + 151
17  UIKit                         	0x230789a2 -[UIApplication _run] + 569
18  UIKit                         	0x230730cc UIApplicationMain + 145
19  DreamDemo                     	0x000834cc 0x7c000 + 29900
20  libdyld.dylib                 	0x1d633506 _dyld_process_info_notify_release + 23
複製程式碼

問題也能看出來,但是因為第三行(DreamDemo)並沒有符號化,導致我們並不確定具體呼叫位置。

再來看看符號化之後的:

Last Exception Backtrace:
0   CoreFoundation                	0x1df60df2 __exceptionPreprocess + 126
1   libobjc.A.dylib               	0x1d1c3072 objc_exception_throw + 33
2   CoreFoundation                	0x1dee62f2 -[__NSArray0 objectAtIndex:] + 105
3   DreamDemo                     	0x0008088e -[ViewController touchesBegan:withEvent:] + 18574 (ViewController.m:84)
4   UIKit                         	0x2319eb44 forwardTouchMethod + 289
5   UIKit                         	0x2319ea10 -[UIResponder touchesBegan:withEvent:] + 29
6   UIKit                         	0x23041c58 -[UIWindow _sendTouchesForEvent:] + 1599
7   UIKit                         	0x2303ca62 -[UIWindow sendEvent:] + 2657
8   UIKit                         	0x2300d870 -[UIApplication sendEvent:] + 315
9   UIKit                         	0x237a8998 __dispatchPreprocessedEventFromEventQueue + 2615
10  UIKit                         	0x237a25de __handleEventQueue + 829
11  CoreFoundation                	0x1df1c716 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 7
12  CoreFoundation                	0x1df1c220 __CFRunLoopDoSources0 + 433
13  CoreFoundation                	0x1df1a4f6 __CFRunLoopRun + 757
14  CoreFoundation                	0x1de6952e CFRunLoopRunSpecific + 481
15  CoreFoundation                	0x1de6933c CFRunLoopRunInMode + 99
16  GraphicsServices              	0x1f640bf8 GSEventRunModal + 151
17  UIKit                         	0x230789a2 -[UIApplication _run] + 569
18  UIKit                         	0x230730cc UIApplicationMain + 145
19  DreamDemo                     	0x000834cc main + 29900 (main.m:15)
20  libdyld.dylib                 	0x1d633506 _dyld_process_info_notify_release + 23
複製程式碼

可以發現,第三行被解析出來了,這樣我們就能很清晰的知道具體的頁面了。

使用命令列工具 atos

symbolicatecrash 可以幫助我們很好的分析 crash 日誌,但是有它的侷限性 --- 不夠靈活。我們需要 symbolicatecrash.crash.dSYM 三個檔案才能解析。

相對於 symbolicatecrash, atos 命令更加靈活,特別是你需要對不同渠道的 crash 檔案,寫一個自動化的分析指令碼的時候,這個方法就極其有用。

但是這種方式也有個不方便的地方:一個線上的 App,使用者使用的版本存在差異,而每個版本所對應的 .dSYM 都是不同的。必須確保 .crash.dSYM 檔案是匹配的,才能正確符號化,匹配的條件就是它們的 UUID 一致。 在這之前,先介紹下 UUID:

什麼是 UUID ?

UUID 是由一組 32 位數的十六進位制數字所構成。每一個可執行程式都有一個 build UUID 唯一標識。.crash日誌包含發生 crash 的這個應用的 build UUID 以及 crash 發生時,應用載入的所有庫檔案的 build UUID。

獲取 UUID

.crash UUID

執行命令:

grep --after-context=2 "Binary Images:" *crash
複製程式碼

輸出:

T.crash:Binary Images:
T.crash-0x7c000 - 0x87fff DreamDemo armv7  <d009f8671129397a8aab9cb2b8e506ff> /var/containers/Bundle/Application/DEEBE571-D512-4E8F-B712-ED4D19CE64F9/DreamDemo.app/DreamDemo
T.crash-0xa9000 - 0xd4fff dyld armv7s  <cd60ff3403063c0aa8e54dff11e42527> /usr/lib/dyld
複製程式碼

看到上面的輸出 d009f8671129397a8aab9cb2b8e506ff 就是 DreamDemo 專案的 UUID。

.dSYM UUID

執行命令:

dwarfdump --uuid DreamDemo.app.dSYM
複製程式碼

輸出:

UUID: D009F867-1129-397A-8AAB-9CB2B8E506FF (armv7) DreamDemo.app.dSYM/Contents/Resources/DWARF/DreamDemo
複製程式碼

.app UUID

執行命令:

dwarfdump --uuid DreamDemo.app/DreamDemo
複製程式碼

輸出:

UUID: D009F867-1129-397A-8AAB-9CB2B8E506FF (armv7) DreamDemo.app/DreamDemo
複製程式碼

可以發現這兩個檔案的 UUID 是相同的,也就是匹配的,只有滿足這種條件,才能正確的解析!

atos 解析

我們現回顧下未解析前的堆疊:

2   CoreFoundation                	0x1dee62f2 -[__NSArray0 objectAtIndex:] + 105
3   DreamDemo                     	0x0008088e 0x7c000 + 18574
4   UIKit                         	0x2319eb44 forwardTouchMethod + 289
5   UIKit                         	0x2319ea10 -[UIResponder touchesBegan:withEvent:] + 29
複製程式碼

執行命令:

xcrun atos -o DreamDemo.app.dSYM/Contents/Resources/DWARF/DreamDemo -arch armv7 -l 0x7c000
複製程式碼

接著輸入 0x0008088e 地址,終端輸出如下:

iOS crash 日誌堆疊解析

可以發現,正確的解析出來了!

除了搭配 .dSYM 檔案,我們也可以使用 .app 檔案來解析:

執行命令:

xcrun atos -o DreamDemo.app/DreamDemo -arch armv7 -l 0x7c000
複製程式碼

同樣輸入 0x0008088e 地址,效果是一樣的。

工具

直接操作 atos 命令畢竟是有點不方便,GitHub 上有個工具,可以輔助我們解析 dSYMTools ,這是個 Mac 客戶端,介面長這樣:

iOS crash 日誌堆疊解析

使用起來也很方便,我們只需要把對應的 dSYM 檔案拖進去,它會自動識別 UUID。我們對應的輸入引數地址就行:

iOS crash 日誌堆疊解析

系統庫的符號化解析

細心的人可以發現,我們上面的解析都是針對 DreamDemo ,這個自己的專案的。其實很多系統方法的堆疊之所以能解析出來,是因為已經有了系統庫的符號化檔案,存放目錄如下:

/使用者/使用者名稱稱xxx/資源庫/Developer/Xcode/iOS DeviceSupport
複製程式碼

iOS crash 日誌堆疊解析

這些庫的版本都是和 .crash 檔案中是對應的:

OS Version:          iPhone OS 10.2 (14C5077b)
複製程式碼

一旦我把這個 10.2 (14C5077b) 系統的符號化庫刪掉,.crash 檔案就會變成這樣:

Last Exception Backtrace:
0   CoreFoundation                	0x1df60df2 0x1de5f000 + 1056242
1   libobjc.A.dylib               	0x1d1c3072 0x1d1bc000 + 28786
2   CoreFoundation                	0x1dee62f2 0x1de5f000 + 553714
3   DreamDemo                     	0x000bc66e -[ViewController touchesBegan:withEvent:] + 18030 (ViewController.m:78)
4   UIKit                         	0x2319eb44 0x22ffe000 + 1706820
5   UIKit                         	0x2319ea10 0x22ffe000 + 1706512
6   UIKit                         	0x23041c58 0x22ffe000 + 277592
7   UIKit                         	0x2303ca62 0x22ffe000 + 256610
8   UIKit                         	0x2300d870 0x22ffe000 + 63600
9   UIKit                         	0x237a8998 0x22ffe000 + 8038808
10  UIKit                         	0x237a25de 0x22ffe000 + 8013278
11  CoreFoundation                	0x1df1c716 0x1de5f000 + 775958
12  CoreFoundation                	0x1df1c220 0x1de5f000 + 774688
13  CoreFoundation                	0x1df1a4f6 0x1de5f000 + 767222
14  CoreFoundation                	0x1de6952e 0x1de5f000 + 42286
15  CoreFoundation                	0x1de6933c 0x1de5f000 + 41788
16  GraphicsServices              	0x1f640bf8 0x1f637000 + 39928
17  UIKit                         	0x230789a2 0x22ffe000 + 502178
18  UIKit                         	0x230730cc 0x22ffe000 + 479436
19  DreamDemo                     	0x000bf332 main + 29490 (main.m:15)
20  libdyld.dylib                 	0x1d633506 0x1d630000 + 13574
複製程式碼

可以明顯的發現,系統庫的堆疊變成了一堆地址。

新版本,每當我們手機連上 Xcode 時,都會把當前版本的系統符號庫自動匯入到 /使用者/使用者名稱稱xxx/資源庫/Developer/Xcode/iOS DeviceSupport 目錄下。但是 iOS 版本那麼多,之前舊的系統符號庫該怎麼獲取呢?有人已經整理好了 iOS-System-Symbols,我們只需要根據 .crash 檔案的版本資訊,下載對應的系統符號化檔案到目錄下即可。

總結

  • 利用 symbolicatecrash 解析,可以將整個 .crash 日誌堆疊解析,但是由於依賴 symbolicatecrash.crash 以及 .dSYM 三個檔案,或者 .app.crashsymbolicatecrash 三個檔案,導致不太靈活。
  • 利用 atos 命令只需要 .crash.dSYM ,或者 .crash.app,知道對應的堆疊地址,就能解析,方便自動化指令碼分析,但是 crash 堆疊可能需要自己實現收集。

參考

wufawei.com/2014/03/sym…

相關文章