本章介紹了 crash dump 的符號化。符號化是將機器地址映射成對擁有原始碼的程式設計師有意義的符號地址的過程。我們希望儘可能看到函數名(加上任何偏移量),而不是看到機器地址。
我們使用 icdab_planets 示例應用程式來演示崩潰。(「IOS Crash Dump Analysis Book Github Resources」 2018)
當處理真實的崩潰時,一般會涉及到很多不同的關聯數據。這些數據可以來自用戶終端,通過設置允許將崩潰報告上傳到 Apple 的終端設備,Apple 擁有的符號信息和我們本地開發環境的配置信息可以相互映射。
為了理解所有信息是如何組合在一起的,最好從一開始開始就自己完成數據轉化任務,因此一旦我們需要診斷符號化問題,我們就已有一定的技術經驗。
構建過程通常,當我們開發應用程式時,我們會將應用程式的 Debug 版本構建到我們的設備上。而當我們為測試人員、應用程式審核或應用商店構建應用時,我們構建應用程式的 Release 版本。
默認情況下,對於 Release版本,.o 文件的調試信息被存儲在一個單獨的目錄結構中。被稱作 our_app_name.DSYM。
當開發人員發現崩潰時,可以使用這些調試信息來幫助我們理解程序在哪裡出錯了。
當用戶發現我們的應用程式崩潰時,並沒有開發人員在身邊。所以,會生成一份崩潰報告。它包含出現問題的機器地址。符號化可以將這些地址轉換為有意義的原始碼來作為參考。
為了進行符號化,必須擁有對應的 DSYM 文件。
默認情況下,Xcode 被設置為只為 Release 版本生成 DSYM 文件,Debug 版本則不會生成該文件。
構建設置打開 Xcode,選擇 build settings,搜索 「Debug Information Format」,可以看到如下設置:
SettingMeaningUsually set for targetDWARF調試信息僅在 .o 文件中DebugDWARF with dSYM File除了.o 文件,也會將調試信息整理到 DSYM 文件中Release在默認設置中,如果我們在測試設備上調試 APP 時,點擊應用圖標並啟動 APP ,那麼如果發生崩潰,我們並沒有在崩潰報告中看到任何符號。這使許多人感到困惑。
這是因為二進位文件的 UUID 和 DSYM 並不匹配。
為了避免這個問題,示例程序 icdab_planets 在 Debug和 Release 兩個 Target 中全都設置為 DWARF with dSYM File 。然後我們就可以進行符號化,因為在 Mac 上會生成一個可供匹配的 DSYM。
關注開發環境的崩潰icdab_planets 程序被設計為在啟動時由於斷言而崩潰。
如果沒有設置成DWARF with dSYM File,我們會得到一個象徵性的部分符號化的崩潰報告。
從 Windows->Devices and Simulators->View Device Logs 中看到的崩潰報告看起來像這樣(為了便於演示而截斷)
Thread 0 Crashed:
0 libsystem_kernel.dylib
0x0000000183a012ec __pthread_kill + 8
1 libsystem_pthread.dylib
0x0000000183ba2288 pthread_kill$VARIANT$mp + 376
2 libsystem_c.dylib
0x000000018396fd0c abort + 140
3 libsystem_c.dylib
0x0000000183944000 basename_r + 0
4 icdab_planets
0x00000001008e45bc 0x1008e0000 + 17852
5 UIKit
0x000000018db56ee0
-[UIViewController loadViewIfRequired] + 1020
Binary Images:
0x1008e0000 - 0x1008ebfff icdab_planets arm64
<9ff56cfacd66354ea85ff5973137f011>
/var/containers/Bundle/Application/
BEF249D9-1520-40F7-93F4-8B99D913A4AC/
icdab_planets.app/icdab_planets
但是,如果設置成DWARF with dSYM File,崩潰報告則會像這樣:
Thread 0 Crashed:
0 libsystem_kernel.dylib
0x0000000183a012ec __pthread_kill + 8
1 libsystem_pthread.dylib
0x0000000183ba2288
pthread_kill$VARIANT$mp + 376
2 libsystem_c.dylib
0x000000018396fd0c abort + 140
3 libsystem_c.dylib
0x0000000183944000 basename_r + 0
4 icdab_planets
0x0000000104e145bc
-[PlanetViewController viewDidLoad] + 17852
(PlanetViewController.mm:33)
5 UIKit
0x000000018db56ee0
-[UIViewController loadViewIfRequired] + 1020
報告的第0、1、2、5行在兩種情況下是相同的,因為我們的開發環境具有正在測試的 iOS 版本的符號信息。在第二種情況下,Xcode 將查找 DSYM 文件以闡明第 4 行。它告訴我們這是在 PlanetViewController.mm 文件中的第33行。是:
assert(pluto_volume != 0.0);
DSYM 結構DSYM 文件嚴格來說是一個目錄層次結構:
icdab_planets.app.dSYM
icdab_planets.app.dSYM/Contents
icdab_planets.app.dSYM/Contents/Resources
icdab_planets.app.dSYM/Contents/Resources/DWARF
icdab_planets.app.dSYM/Contents/Resources/DWARF/icdab_planets
icdab_planets.app.dSYM/Contents/Info.plist
只是將通常放在 .o 文件中的 DWARF 數據,複製到另一個單獨的文件中。
通過查看構建日誌,我們可以看到 DSYM 是如何生成的。它實際上只是因為這個命令: dsymutil path_to_app_binary -o output_symbols_dir.dSYM
著手符號化為了幫助我們熟悉 crash dump 報告,我們可以演示實際上符號化是如何工作的。在第一段報告中,我們想要了解:
4 icdab_planets
0x00000001008e45bc 0x1008e0000 + 17852
如果我們能在崩潰時準確的知道代碼的版本,我們就可以重新編譯我們的程序,但是在 DSYM 設置打開的情況下,我們只能在在發生崩潰後獲取一個 DSYM 文件。
crash dump 告訴我們崩潰發生時程序在內存中的程序在內存中的地址信息。這告訴我們其他地址(TEXT)位置相對偏移量。
在crash dump 報告的底部,我們有一行0x1008e0000 - 0x1008ebfff icdab_planets。所以 icdab_planets 的位置從 0x1008e0000 開始。
運行命令 atos 查看你感興趣的位置信息:
# atos -arch arm64 -o
./icdab_planets.app.dSYM/Contents/Resources/DWARF/
icdab_planets -l 0x1008e0000 0x00000001008e45bc
-[PlanetViewController viewDidLoad] (in icdab_planets)
(PlanetViewController.mm:33)
崩潰報告工具基本上就是使用 atos命令來符號化崩潰報告,以及提供其他與系統相關的信息。
如果想要更加深入的了解符號化過程我們可以通過 Apple Technote 來獲取其進一步的描述。(「CrashReport Technote 2123」 2004)
逆向工程方法在上面的例子中,我們具有crash dump 的原始碼和符號,因此可以執行符號化。
但是有時在我們的項目中,包含了第三方的二進位框架,我們並沒有原始碼。如果框架提供者提供了相應的符號信息讓用戶可以進行 crash dump 分析,這當然是很好的。但是當符號信息不可用時,我們仍然可以通過一些逆向工程的手段來取得進展。
與第三方合作時,故障的診斷和排查通常需要更多的時間。我們發現良好的編寫且具體的錯誤報告可以加速很多事情。以下方法可以為你提供所需的特定信息。
我們將使用工具一章中提到的 Hopper 工具來演示我們的方法。
啟動 hopper,選擇 File->Read Executable to Disassemble。我們使用 examples/assert_crash_ios/icdab_planets中的二進位文件作為示例。
我們需要 「rebase」 反彙編程序,以便它在崩潰時顯示的地址與程序的地址相同。選擇 Modify->Change File Base Address。為了保持一致,輸入 0x1008E0000。
現在我們可以看到崩潰代碼了。地址 0x00000001008e45bc 實際上是設備在跟蹤堆棧中執行函數調用後將 return 到的地址。儘管如此,它仍被記錄在此。選擇 Navigate-> Go To Address and Symbol 並輸入 0x00000001008e45bc 。
我們看到的總體會如下所示
放大這一行,我們能看到
這確實顯示了 assert 方法的返回地址。再往上看,我們看到判斷了 Pluto 的體積不能為零。這只是 Hopper 非常基本的使用示例。接下來我們將使用 Hopper 演示其最有趣的功能——將彙編代碼生成偽代碼。這降低了理解崩潰的心理負擔。如今,大多數開發人員很少查看彙編代碼,所以就這個功能就值得我們為該軟體付出代價。
現在至少對於當前的問題嗎,我們可以編寫一個錯誤報告,指明由於 Pluto 的體積為零,導致代碼崩潰了。對框架的提供者來說,這就足以解決問題了。
在更複雜的情況下,想像我們使用了一個發生崩潰的圖片轉換庫。圖片可能有多種像素格式。使用 assert 可以讓我們注意到某些特定的像素格式。因此,我們可以嘗試其他的像素格式。
另一個例子是 security 庫。安全代碼通常會返回通用錯誤代碼,而不是特定的故障代碼,以便將來進行代碼增強並避免洩漏內部細節(安全風險)。安全庫中的 crash dump 程序可能會指出安全問題的類型,並幫助我們更早地更正傳遞到庫中的某些數據結構。