背景
有贊在基礎保障平臺的實踐中完成了 Crash平臺 的建設,但是iOS的崩潰日誌未經符號化,排查問題比較困難。爲了降低iOS App的crash率,快速排查線上crash,疑難crash的跟蹤處理,符號化崩潰日誌顯得尤爲重要!
一、crash日誌的收集與分析
1.1 如何收集crash日誌
1.手機上直接看,在隱私-分析與改進 -分析數據,可以找到所有崩潰日誌,未符號化。
2.連接電腦,通過“音樂”同步到本地 ~/Library/Logs/CrashReporter/MobileDevice/xxx的 iPhone. 缺點:日誌沒有符號化,需要自己手動符號化
3.連接電腦,打開Xcode-window-Diveces and Simulators。
Xcode會嘗試在本地查找符號表文件,自動符號化。
以上3種方法都侷限於拿得到設備的情況。
4.查看別人手機上的crash日誌 Xcode-Window-Organizer。
這種方式找符號表會有2種途徑
- 上傳AppStore的時候會讓你勾選上傳符號表「Include App symbols for your Application…」,如果上傳了,蘋果自動幫你在雲端做解析。
- 如果沒有上傳,Xcode嘗試在本地找符號表文件進行符號化。
缺點:這種方式也只能收集在手機設置中打開了上傳crash開關,以及TestFlight用戶的crash日誌。企業分發或 AdHoc 安裝,需要自行獲取崩潰日誌。信息不全,線程信息不夠。
5.自己收集crash日誌,比如接入KSCrash、plcrashreporter等,但是要自己做符號化。
1.2 crash日誌的結構
日誌可以分成4個部分,基本信息,崩潰的原因,所有線程調用,Binary Images (二進制文件列表)。
1.2.1 基本信息
1.2.2 崩潰原因
線程
Binary Images
二、如何進行crash日誌符號化
crash日誌符號化通常是通過 atos
和 symbolicatecrash
這兩個工具來完成。
2.1 atos
atos
是蘋果提供的符號化工具,在Mac OS系統下默認安裝,他的缺點是隻能一個地址一個地址逐個翻譯。我們看下這個工具的使用說明:
使用方法:
atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
需要傳入這幾個信息:arch 架構、dSYM路徑、binary image 載入內存的初始地址、崩潰的地址。
參數內容可以從crash日誌中取得,如下圖所示:
example
$ atos -arch arm64 -o TheElements.App.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
-[AtomicElementViewController myTransitionDidStop:finished:context:]
2.2 symbolicatecrash
symbolicatecrash
是 Xcode
自帶的一個程序,他是對 atos
的封裝,可以翻譯整個crash文件,有贊就是選擇這個工具來進行 crash
符號化的。
具體的路徑可以通過以下命令搜索出來:
find /Applications/Xcode.App -name symbolicatecrash -type f
使用方法:
export DEVELOPER_DIR="/Applications/Xcode.App/Contents/Developer"
<path of symbolicatecrash>/symbolicatecrash <Path to dSYM file crash log>
例子:
symbolicatecrash log.crash > result.log
// dSYM可以跟多個
symbolicatecrash log.crash -d TheElement.App.dSYM >result.log
下文會對此工具做一個詳細的原理分析。
三、symbolicatecrash符號化原理分析
通過網上找的教程來看,一般是把對應版本的crash日誌,dSYM文件,App文件都放進一個目錄,然後執行一下命令來進行符號化:
symbolicatecrash log.crash -d TheElement.App.dSYM >result.log
但是我有幾個疑問:
- 如果App打包出來多個dSYM怎麼辦?
- 發現把目錄中的App文件刪了,dSYM刪了(源文件還在),執行命令的時候也沒傳他們,竟然也可以符號化,這怎麼做到的?
- 怎麼樣知道crash日誌,dSYM,App是正確的,可以正確做符號化,如果發現某個crash日誌沒有被正確符號化,怎麼查這個問題?
- 把dSYM丟了,相同代碼再去編譯一次把dSYM拿出來可以用嗎?
- 我們執行完後發現系統庫也都符號化了,系統的dSYM在哪裏,難道已經包含在App的dSYM中嗎?
- 崩潰日誌最下面的Binary Images是幹嘛的?
針對以上這些問題,我們來做下源碼分析一探究竟。
3.1 symbolicatecrash 源碼分析
官方沒有開源,但是網上有類似的實現,是用perl實現的一個腳本。
首先,一個基本原則是需要確保你的電腦上有每個 image
對應的 uuid
的符號表文件,這樣crash文件才能被正確解析和符號化出來。
然後我們看下符號化一個crash文件的流程:
3.1.1 解析所有的Binary Image
這是crash日誌中的Binary Image格式
0x1cd997000 - 0x1cea7bfff UIKitCore arm64 <40a93e939f8635c1905c7b947c7c2305> /System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
轉換爲如下格式
'UIKitCore' =>
{
'extent' => '0x1cea7bfff',
'plus' => '',
'bundlename' => 'UIKitCore',
'uuid' => '40a93e939f8635c1905c7b947c7c2305',
'base' => '0x1cd997000',
'path' => '/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore',
'arch' => 'arm64',
'nextID' => ''
}
把每一個Binary Image都存儲爲以上形式的對象。
Binary Image的作用是建立UIKitCore與uuid的關係,當需要符號化一個UIKitCore的地址時,會找到對應的uuid,並從文件系統中查找到這個符號表。這也解釋了上面第6個問題。
3.1.2 解析所有線程
8 TheElement 0x00000001044dcfc0 0x104058000 + 4739008
轉換爲如下格式
'0x00000001044dcfc0 0x104058000 + 4739008' =>
{
'raw_address' => '0x00000001044dcfc0',
'bundle' => 'TheElement',
'address' => '0x00000001044dcfc0'
}
把所有堆棧存儲爲以上形式的對象。
3.1.3 翻譯Last Exception Backtrace
這是crash日誌中的Last Exception Backtrace
Last Exception Backtrace:
(0x1a1a9127c 0x1a0c6b9f8 0x1a19adab8 0x1a1a96ac4 0x1a1a9875c 0x10566d498 0x10423ab84 0x1ce255040 0x1cdcfe1c8 0x1cdcfe4e8 0x1cdcfd554 0x1ce28c304 0x1ce28d52c 0x1ce26d59c 0x10437fd20 0x1ce333714 0x1ce335e40 0x1ce32f070 0x1a1a23018 0x1a1a22f98 0x1a1a22880 0x1a1a1d7bc 0x1a1a1d0b0 0x1a3c1d79c 0x1ce253978 0x104283158 0x1a14e28e0)
翻譯爲:
0 libsystem_kernel.dylib 0x00000001a162e0dc 0x1a160b000 + 143580
1 libsystem_pthread.dylib 0x00000001a16a7094 0x1a16a5000 + 8340
2 libsystem_c.dylib 0x00000001a1587f4c 0x1a152d000 + 372556
3 libsystem_c.dylib 0x00000001a1587eb4 0x1a152d000 + 372404
4 libc++abi.dylib 0x00000001a0c54788 0x1a0c53000 + 6024
5 libc++abi.dylib 0x00000001a0c54934 0x1a0c53000 + 6452
6 libobjc.A.dylib 0x00000001a0c6be00 0x1a0c66000 + 24064
7 TheElement 0x0000000104babb18 0x104058000 + 11877144
8 TheElement 0x00000001044dcfc0 0x104058000 + 4739008
9 libc++abi.dylib 0x00000001a0c60838 0x1a0c53000 + 55352
10 libc++abi.dylib 0x00000001a0c60434 0x1a0c53000 + 54324
11 libobjc.A.dylib 0x00000001a0c6bbc8 0x1a0c66000 + 23496
12 CoreFoundation 0x00000001a1a1d11c 0x1a1979000 + 672028
13 GraphicsServices 0x00000001a3c1d79c 0x1a3c13000 + 42908
14 UIKitCore 0x00000001ce253978 0x1cd997000 + 9161080
15 TheElement 0x0000000104283158 0x104058000 + 2273624
16 libdyld.dylib 0x00000001a14e28e0 0x1a14e1000 + 6368
這裏爲什麼可以翻譯,因爲第一步已經把所有Binary Image存儲起來,上面的每一個地址,都可以找到對應的Binary Image,從而獲得Binary Image的名稱,基地址,以及偏移量。
3.1.4 刪除不需要的image
因爲crash日誌把App用到的所有Binary Image都列舉出來了,而崩潰堆棧中只用到了一小部分,所以這裏把沒有用到的Binary Image刪除。後續要遍歷所有images,去找到每個二進制對應的dSYM,這樣做提高了效率。
3.1.5 查找Binary Image的符號表
符號表的類型
- App編譯出來的dSYM ( 一般輸入命令時指定在哪裏,如果沒有會自動去查找)
- 系統庫的符號表 (自動查找),這也解釋了第五個問題,系統符號表和APP符號表是分開的。在 ~/Library/Developer/Xcode/iOS DeviceSupport/os/Symbols 這個路徑再拼上image中的path,就是完整路徑 比如 ~/Library/Developer/Xcode/iOS DeviceSupport/os/Symbols/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
- 從search path中找 (包括命令行輸入的幾個目錄 和 系統符號表所在目錄)
- mdfind搜索uuid相同的符號表,這就解釋了上面第1個和第2個問題,會使用uuid去查找,所以命令行中不傳也沒關係。
- 如果還沒找到 返回空 並刪除這個image,與這個image相關的都不能被符號化
判斷匹配的條件
- lipo -info 判斷架構是否一致
- otool 命令打出來macho信息,找到uuid 並 判斷是否一致,這解答了上面第3個和第4個問題,只有uuid相同,纔可以被符號化出來。相同代碼重新打一個包出來也不能符號化,因爲uuid不同。
3.1.6 執行atos進行符號化
- 遍歷所有線程
- 取到每一條的bundle 還有地址 在images中找到符號表路徑
- 執行命令 並記錄符號化後的內容
'0x00000001044dcfc0 0x104058000 + 4739008' =>
{
'symbolled' => 'CPPExceptionTerminate() (SentryCrashMonitor_CPPException.cpp:179)',
'raw_address' => '0x00000001044dcfc0',
'bundle' => 'TheElement',
'address' => '0x00000001044dcfc0'
}
3.1.7 字符串替換 生成最終的報告
逐行開始替換
比如將’0x00000001044dcfc0 0x104058000 + 4739008’替換爲’CPPExceptionTerminate() (SentryCrashMonitor_CPPException.cpp:179)’
四、有贊符號化方案
通過上面的原理分析,我們基本掌握了 crash
符號化的步驟,下面介紹下我們有贊是如何做符號化的。
4.1 dSYM符號表保存
首先,進行符號化必不可少的一個文件就是 dSYM
符號表,我們需要保存每次正式發佈的App版本對應的符號表文件。如下圖所示:
- 打包機(gitlab runner):有贊目前有自己的持續構建平臺
MBD
,業務方在MBD
上發起打包構建任務後系統會根據算法分配到不同的打包機上。更多關於有贊移動CI/CD
我們在之前做過一次技術沙龍,詳細內容見這裏。 - 項目打包完成後會執行一個保存符號表的腳本,會保存符號表到本地,並且上傳到雲端做備份。備份完成後調用MBD接口,上報符號表uuid,bundleId,版本號,build號,打包機唯一標識。
- 由於有多臺打包機導致每次打包產出的符號表分佈在不同的打包機上,我們需要建立dSYM文件與打包機的關係。第一步中的保存符號表腳本會上報信息到MBD,MBD把dSYM符號表uuid和打包機唯一標識做一個映射關係。
- 當發生一個crash時,crash日誌中包含符號表uuid,通過uuid查表,就能定位到執行構建的打包機。
4.2 crash上報
dSYM符號表已經保存下來了,接下來就是crash的上報和解析,crash上報大致流程見下圖:
- crash信息通過SDK上報到埋點平臺,我們通過Flink監聽到crash信息的上報,並把它寫入數據庫。
- Flink是實時計算平臺提供的用來實時消費上報的數據的程序,支持大併發量的數據。
更多關於crash平臺的建設我們近期也發表過一篇文章,詳情見 這裏。
4.3 crash文件符號化
步驟二中已經上報了crash信息並展示在了我們的內部平臺中,接下來我們需要對此crash文件結合對應的dSYM進行符號化解析,具體流程如下:
- 在 Crash前端頁面,點擊符號化按鈕會發起 MBD 的一次符號化構建,並將 crash 的信息傳遞給 MBD。
- MBD把crash的uuid拿出來,根據uuid去查 dSYM文件所在的 打包機,並把任務給到這個打包機。
- 打包機運行腳本,這個腳本的作用是使用symbolicatecrash程序符號化crash日誌,並把符號化後的結果通知到MBD。
- MBD 把符號化結果寫入數據庫,並通知Crash後端。
- Crash前端頁面收到通知後刷新頁面,展示符號化後的結果。
至此,我們完成了crash文件的符號化解析工作,但是使用過程中暴露出了一些問題:
- 目前每次打包都會產生dSYM文件並直接保存在打包機上,MBD每天的打包任務有很多,導致佔用空間浪費資源。我們計劃只維護符號表的cdn鏈接,用到時再去下載符號表。
- 這種方案下線一臺打包機後,會造成一部分crash日誌無法符號化,目前我們正在優化,計劃統一把符號表放到一臺打包機上,這樣就能解決這個問題。
- 系統符號表的維護也是一個問題,我們需要在每臺打包機上都要加上系統符號表,而且每次蘋果發佈新版都需要拿新的系統符號表過來,維護起來挺麻煩的。目前的解決方案是人工放到打包機上。
總結
至此,我們瞭解瞭如何收集crash日誌,明白了crash日誌中每個部分的意思,符號化的工具,以及如何對crash日誌進行符號化。已經可以解答出來上面提出的問題,對符號化的原理有了非常清晰的認識。
我們的符號化方案對於有贊多臺打包機環境而言,非常合適,下線一臺或者新增一臺打包機,可以無縫支持。另外,整套方案非常輕量,能夠快速集成符號化功能,符號化鏈路清晰。
Crash平臺擁有符號化crash日誌的能力後,極大的提高了大家排查、解決線上問題的效率,提升了App的穩定性。
本文轉載自公衆號有贊coder(ID:youzan_coder)。
原文鏈接: