Categories
程式開發

有贊crash平台符號化實踐


背景

有贊在基礎保障平台的實踐中完成了 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。

有贊crash平台符號化實踐 1

Xcode會嘗試在本地查找符號表文件,自動符號化。

以上3種方法都局限於拿得到設備的情況。

4.查看別人手機上的crash日誌Xcode-Window-Organizer。

有贊crash平台符號化實踐 2

這種方式找符號表會有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 基本信息

有贊crash平台符號化實踐 3

1.2.2 崩潰原因

有贊crash平台符號化實踐 4

線程

有贊crash平台符號化實踐 5

二進製圖像

有贊crash平台符號化實踐 6

二、如何進行crash日誌符號化

crash日誌符號化通常是通過 atossymbolicatecrash 這兩個工具來完成。

2.1行為

atos 是蘋果提供的符號化工具,在Mac OS系統下默認安裝,他的缺點是只能一個地址一個地址逐個翻譯。 我們看下這個工具的使用說明:

有贊crash平台符號化實踐 7

使用方法:

atos -arch  -o /Contents/Resources/DWARF/ -l  

需要傳入這幾個信息:arch 架構、dSYM路徑、binary image 載入內存的初始地址、崩潰的地址。

參數內容可以從crash日誌中取得,如下圖所示:

有贊crash平台符號化實踐 8

$ atos -arch arm64 -o TheElements.App.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
  -[AtomicElementViewController myTransitionDidStop:finished:context:]

2.2 symbolicatecrash

symbolicatecrashXcode 自帶的一個程序,他是對 atos 的封裝,可以翻譯整個crash文件,有贊就是選擇這個工具來進行 crash 符號化的。

具體的路徑可以通過以下命令搜索出來:

find /Applications/Xcode.App -name symbolicatecrash -type f

使用方法:

export DEVELOPER_DIR="/Applications/Xcode.App/Contents/Developer"
  /symbolicatecrash 

  例子:
  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

但是我有幾個疑問:

  1. 如果App打包出來多個dSYM怎麼辦?
  2. 發現把目錄中的App文件刪了,dSYM刪了(源文件還在),執行命令的時候也沒傳他們,竟然也可以符號化,這怎麼做到的?
  3. 怎麼樣知道crash日誌,dSYM,App是正確的,可以正確做符號化,如果發現某個crash日誌沒有被正確符號化,怎麼查這個問題?
  4. 把dSYM丟了,相同代碼再去編譯一次把dSYM拿出來可以用嗎?
  5. 我們執行完後發現系統庫也都符號化了,系統的dSYM在哪裡,難道已經包含在App的dSYM中嗎?
  6. 崩潰日誌最下面的Binary Images是乾嘛的?

針對以上這些問題,我們來做下源碼分析一探究竟。

3.1 symbolicatecrash 源碼分析

官方沒有開源,但是網上有類似的實現,是用perl實現的一個腳本。

首先,一個基本原則是需要確保你的電腦上有每個 image 對應的 uuid 的符號表文件,這樣crash文件才能被正確解析和符號化出來。

然後我們看下符號化一個crash文件的流程:

3.1.1 解析所有的Binary Image

这是crash日志中的Binary Image格式
0x1cd997000 - 0x1cea7bfff UIKitCore arm64   /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翻譯最新異常回溯

这是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
  1. 從search path中找(包括命令行輸入的幾個目錄和系統符號表所在目錄)
  2. mdfind搜索uuid相同的符號表,這就解釋了上面第1個和第2個問題,會使用uuid去查找,所以命令行中不傳也沒關係。
  3. 如果還沒找到返回空並刪除這個image,與這個image相關的都不能被符號化
判斷匹配的條件
  1. lipo -info 判斷架構是否一致
  2. otool 命令打出來macho信息,找到uuid 並判斷是否一致,這解答了上面第3個和第4個問題,只有uuid相同,才可以被符號化出來。 相同代碼重新打一個包出來也不能符號化,因為uuid不同。

3.1.6 執行atos進行符號化

  1. 遍歷所有線程
  2. 取到每一條的bundle 還有地址在images中找到符號表路徑
  3. 執行命令並記錄符號化後的內容
 '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版本對應的符號表文件。 如下圖所示:

有贊crash平台符號化實踐 9

  1. 打包機(gitlab runner):有贊目前有自己的持續構建平台 MBD,業務方在 MBD 上發起打包構建任務後系統會根據算法分配到不同的打包機上。 更多關於有贊移動 CI/CD 我們在之前做過一次技術沙龍,詳細內容見這裡
  2. 項目打包完成後會執行一個保存符號表的腳本,會保存符號表到本地,並且上傳到雲端做備份。 備份完成後調用MBD接口,上報符號表uuid,bundleId,版本號,build號,打包機唯一標識。
  3. 由於有多台打包機導致每次打包產出的符號表分佈在不同的打包機上,我們需要建立dSYM文件與打包機的關係。 第一步中的保存符號表腳本會上報信息到MBD,MBD把dSYM符號表uuid和打包機唯一標識做一個映射關係。
  4. 當發生一個crash時,crash日誌中包含符號表uuid,通過uuid查表,就能定位到執行構建的打包機。

4.2 crash上報

dSYM符號表已經保存下來了,接下來就是crash的上報和解析,crash上報大致流程見下圖:

有贊crash平台符號化實踐 10

  1. crash信息通過SDK上報到埋點平台,我們通過Flink監聽到crash信息的上報,並把它寫入數據庫。
  2. Flink是實時計算平台提供的用來實時消費上報的數據的程序,支持大並發量的數據。

更多關於crash平台的建設我們近期也發表過一篇文章,詳情見 這裡

4.3 crash文件符號化

步驟二中已經上報了crash信息並展示在了我們的內部平台中,接下來我們需要對此crash文件結合對應的dSYM進行符號化解析,具體流程如下:

有贊crash平台符號化實踐 11

  1. 在Crash前端頁面,點擊符號化按鈕會發起MBD 的一次符號化構建,並將crash 的信息傳遞給MBD。
  2. MBD把crash的uuid拿出來,根據uuid去查dSYM文件所在的打包機,並把任務給到這個打包機。
  3. 打包機運行腳本,這個腳本的作用是使用symbolicatecrash程序符號化crash日誌,並把符號化後的結果通知到MBD。
  4. MBD 把符號化結果寫入數據庫,並通知Crash後端。
  5. Crash前端頁面收到通知後刷新頁面,展示符號化後的結果。

至此,我們完成了crash文件的符號化解析工作,但是使用過程中暴露出了一些問題:

  1. 目前每次打包都會產生dSYM文件並直接保存在打包機上,MBD每天的打包任務有很多,導致佔用空間浪費資源。 我們計劃只維護符號表的cdn鏈接,用到時再去下載符號表。
  2. 這種方案下線一台打包機後,會造成一部分crash日誌無法符號化,目前我們正在優化,計劃統一把符號表放到一台打包機上,這樣就能解決這個問題。
  3. 系統符號表的維護也是一個問題,我們需要在每台打包機上都要加上系統符號表,而且每次蘋果發布新版都需要拿新的系統符號表過來,維護起來挺麻煩的。 目前的解決方案是人工放到打包機上。

總結

至此,我們了解瞭如何收集crash日誌,明白了crash日誌中每個部分的意思,符號化的工具,以及如何對crash日誌進行符號化。 已經可以解答出來上面提出的問題,對符號化的原理有了非常清晰的認識。

我們的符號化方案對於有贊多台打包機環境而言,非常合適,下線一台或者新增一台打包機,可以無縫支持。 另外,整套方案非常輕量,能夠快速集成符號化功能,符號化鏈路清晰。

Crash平台擁有符號化crash日誌的能力後,極大的提高了大家排查、解決線上問題的效率,提升了App的穩定性。

本文轉載自公眾號有贊coder(ID:youzan_coder)。

原文鏈接

有贊crash平台符號化實踐