海神平臺iOS端崩潰日誌解析踩坑之旅

開始這篇文章的時候,我內心是拒絕的,畢竟 Google 搜索“iOS Crash解析”,內容不要太多。

自己其實也是閱文無數,但是每每到了動手解析一個Crash日誌的時候,往往還要再去翻書籤。“紙上得來終覺淺,絕知此事要躬行”。這次我們換個思路,以前都是講完原理講應用,這次我們講海神平臺在Crash解析功能開發過程中遇到的問題以及解決方案,爭取大家看完以後不用存書籤。

1. 如何獲取Crash日誌

元數據獲取的話題永不過時。

常見的日誌獲取方式有以下幾種:

  1. 將崩潰設備連接到Xcode導出Crash日誌,如果有符號表,則Crash日誌直接被解析
  2. 依賴三方應用,類似於iTools、iMazing等,可以導出Crash日誌
  3. 通過iTunes Connection獲取,此項需要用戶在客戶端授權
  4. 使用imobiledevice套件進行導出,這種方式在自動化測試中廣泛應用
  5. 使用bugly、Fabric等商業平臺收集,這種方式在發佈環境使用較多
  6. 使用開源Crash框架收集上報,這種方式在發佈環境使用較多

前4種方案在開發測試階段非常有效,但是應用發佈之後卻比較無力,因爲開發同學既無法獲取崩潰設備,也無法保證用戶授權。

方案5,當前商業平臺都沒有數據導出接口,所以無法獲取崩潰日誌。

海神平臺的定位是已發佈應用的崩潰日誌收集平臺,所以最終依賴KSCrash在發佈階段獲取客戶端Crash日誌。

1.1 KSCrash與上下文

KSCrash是著名且有效的崩潰日誌收集框架,提供抓取多種類型崩潰上下文,包括:mach、signal、C++ Exception、OC Exception等的能力,並支持多種渠道數據上報。詳細內容可以參考Github。

KSCrash抓取的上下文默認組織爲JSON格式,同時框架提供了類和方法用於將JSON轉換爲Apple Format。下面是一個JSON格式的例子:

{
    "report": {
        "id": "2BF8D28A-A2A5-4076-952C-F8EFB4A42456",
        "process_name": "LJBaseCrashReporter_Example",
        "timestamp": 1569731896698234,
        ...
    },
    "binary_images": [
        {
            "image_addr": 4377903104,
            "image_vmaddr": 4294967296,
            "image_size": 2818048,
            "name": "/var/containers/Bundle/Application/92937E4A-DFA9-4BAF-9780-2F8796A1A6C7/LJBaseCrashReporter_Example.app/LJBaseCrashReporter_Example",
            "uuid": "FE056305-553D-3ED3-AB93-7AAB60BDE692",
            "cpu_type": 16777228,
            "cpu_subtype": 0,
            "major_version": 0,
            "minor_version": 0,
            "revision_version": 0
        },
        ...
    ],
    "system": {
        "system_name": "iOS",
        "system_version": "12.4",
        "machine": "iPhone8,2",
        "model": "N66mAP",
        "CFBundleIdentifier": "com.lianjia.LJBaseCrashReporter",
        ...
    },
    "crash": {
        "error": {
            "mach": {
                "exception": 1,
                "exception_name": "EXC_BAD_ACCESS",
                "code": 1,
                "code_name": "KERN_INVALID_ADDRESS",
                "subcode": 8
            },
            "signal": {
                "signal": 11,
                "name": "SIGSEGV",
                "code": 0,
                "code_name": "SEGV_NOOP"
            },
            "address": 1,
            "type": "mach"
        },
        "threads": [
            {
                "backtrace": {
                    "contents": [
                        {
                            "object_name": "LJBaseCrashReporter_Example",
                            "object_addr": 4377903104,
                            "symbol_name": "-[LJCrashDebugMachsController tableView:didSelectRowAtIndexPath:]",
                            "symbol_addr": 4378464516,
                            "instruction_addr": 4378464680
                        },
                        ...
                    ],
                    "skipped": 0
                },
            }
        ]
    }
}

上面的例子只保留了JSON Format數據的框架結構,原始數據大約在200k左右,包含全部線程信息以及全部二進制文件信息。

顯而易見,JSON Format對於Server端處理非常友好,而Apple Format對於開發人員閱讀非常友好。所以海神平臺選擇了兩全其美的方案:

  • 海神客戶端上報JSON Format數據,海神平臺後端數據流轉也保持對象結構
  • 交叉編譯Apple Format工具用於支持下載文件格式化

1.2 同步上傳遇到的問題

時效性是監控系統最靚的🏷

海神希望能夠最快知曉用戶設備上發生了什麼,所以客戶端目標始終是同步上傳Crash日誌。

iOS的thread和runloop是個好話題。當工程師熟練的創建NSURLConnection的時候,一定要感謝是runloop在背後默默的支持網絡請求。

但是當應用崩潰時,包括main thread在內的所有線程runloop都會退出,所以無論是async還是sync的方式,NSURLConnection都無法發送網絡請求。

當然,NSURLConnection已經是廢棄的網絡框架,現在提到OC網絡框架時主要指NSURLSession(https://developer.apple.com/documentation/foundation/nsurlsession)。

NSURLSession是OC新一代網絡框架,目標是取代NSURLConnection。NSURLSession由系統網絡進程管理網絡請求,實現統一的安全性、連接、帶寬和能源等管理。不過糟糕的是NSURLSession文檔非常少,導致很多具體的技術細節無從知曉。

不過,通過 控制檯 連接iOS設備,可以看到NSURLSession的守護進程,並且能夠看到該進程簡單的狀態信息。

nsurlsessiond   nsurlsessiond   Application <private> entered foreground    17:30:18.758723 +0800

NSURLSessionConfiguration是NSURLSession的配置類,通過方法:

-backgroundSessionConfigurationWithIdentifier:可以創建後臺會話(Background Session)。

詳見:https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration?language=objc

Use this method to initialize a configuration object suitable for transferring data files while the app runs in the background. A session configured with this object hands control of the transfers over to the system, which handles the transfers in a separate process. In iOS, this configuration makes it possible for transfers to continue even when the app itself is suspended or terminated.

Background Session能夠保證應用終止後完成數據傳輸,似乎解決了崩潰後無法發送網絡請求的問題。

Unlike data tasks, you can use upload tasks to upload content in the background.

NSURLSessionUploadTask是NSURLSession的上傳任務(uploadTask)類,有兩種方式創建uploadTask,在崩潰發生時同步上傳。

詳見:https://developer.apple.com/documentation/foundation/nsurlsessionuploadtask?language=objc

  • -uploadTaskWithStreamedRequest:
  • -uploadTaskWithRequest:fromData:

A URL request object that provides the URL, cache policy, request type, and so on. The body stream and body data in this request object are ignored.

但是實際測試發現,文檔和代碼有差別…上面的說明似乎沒有生效。

於是我們很容易就有了下面的代碼:

NSURL *URL = [NSURL URLWithString:url];
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:URL];
requestM.HTTPMethod = @"POST";
[requestM setHTTPBody:body];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:NSUUID.UUID.UUIDString];
NSURLSession *sharedInstance = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionUploadTask *task = [sharedInstance uploadTaskWithStreamedRequest:requestM];
[task resume];

上面的代碼在iOS 12.4上運行的非常良好。從控制檯可以看到完整的進程間通信過程:

LJBaseCrashReporter_Example CFNetwork           background session setup will wait for reply: session <private> with identifier <private>   17:18:50.724156 +0800
nsurlsessiond               nsurlsessiond       Creating session with identifier: <private> for bundle id: <private>    17:18:50.726110 +0800
nsurlsessiond               nsurlsessiond       Client <private> is a SpringBoard application   17:18:50.726390 +0800
nsurlsessiond               nsurlsessiond       Session <<private>>.<<private>> using resource timeout: 604800.000000, request timeout: 60.000000 allowsCellularAccess: 1, allowsExpensiveAccess: 1 _sourceApplicationBundleIdentifier: (null), _sourceApplicationSecondaryIdentifier: (null)   17:18:50.740688 +0800
LJBaseCrashReporter_Example CFNetwork           background session setup reply received: session <private> with identifier <private>    17:18:50.742934 +0800
nsurlsessiond               nsurlsessiond       Task <8A4DAC69-A8AF-484C-9D3B-1F77D0DB2E1F>.<1> uploadTaskWithRequest: <private> fromFile: (null)   17:18:50.746283 +0800
nsurlsessiond               nsurlsessiond       Current discretionary status for <private> is non-discretionary 17:18:50.748662 +0800
nsurlsessiond               CFNetwork           Task <8A4DAC69-A8AF-484C-9D3B-1F77D0DB2E1F>.<1> is for <<private>>.<<private>>.<1>  17:18:50.748973 +0800
nsurlsessiond               nsurlsessiond       Task <8A4DAC69-A8AF-484C-9D3B-1F77D0DB2E1F>.<1> enqueueing  17:18:50.749067 +0800
LJBaseCrashReporter_Example CFNetwork           Task <8A4DAC69-A8AF-484C-9D3B-1F77D0DB2E1F>.<1> resuming, QOS(0x15) 17:18:50.749642 +0800
LJBaseCrashReporter_Example libnetwork.dylib    Create activity <nw_activity 12:3>  17:18:50.749979 +0800
LJBaseCrashReporter_Example libnetwork.dylib    Activated <nw_activity 12:3 [1CF15E8F-791C-4987-8DA4-AEE007DDEDF1] (reporting strategy default)>    17:18:50.752752 +0800
LJBaseCrashReporter_Example CFNetwork           [Telemetry]: Activity <nw_activity 12:3 [1CF15E8F-791C-4987-8DA4-AEE007DDEDF1] (reporting strategy default)> on Task <8A4DAC69-A8AF-484C-9D3B-1F77D0DB2E1F>.<1> was not selected for reporting  17:18:50.753120 +0800

Anything Perfect! But 上面的例子在 系統兼容性 的測試上無法通過。

在較低版本的iOS系統上(實在是找不齊系統版本😂),已知會出現三種類型的異常:

類型一: iOS 9.x、iOS 10.x等系統在應用崩潰後創建Background Session導致進程卡死無法退出

這種情況發生時,如果手動退出應用的話,仍然可以通過Xcode獲取Crash日誌。忽略掉無關代碼之後,導致進程卡死的調用棧如下:

0   libsystem_kernel.dylib          semaphore_wait_trap + 8
1   libdispatch.dylib               _dispatch_semaphore_wait_slow + 244
2   CFNetwork                       -[__NSURLBackgroundSession setupBackgroundSession] + 540
3   CFNetwork                       -[__NSURLBackgroundSession initWithConfiguration:delegate:delegateQueue:] + 412
4   CFNetwork                       +[NSURLSession sessionWithConfiguration:delegate:delegateQueue:] + 560

...

11  LJBaseCrashReporter_Example     handleExceptions + 1769296 (KSCrashMonitor_MachException.c:363)
12  libsystem_pthread.dylib         _pthread_body + 156
13  libsystem_pthread.dylib         _pthread_body + 0
14  libsystem_pthread.dylib         thread_start + 4

如果熟悉GCD的話,那麼對於semaphore_wait_trap一定很熟悉,因爲無論是dispatch_once還是dispatch_semphore內部都使用了陷阱模式來實現線程wait。

從堆棧可以確定,-[__NSURLBackgroundSession setupBackgroundSession]在等待一個“響應”,並根據這個響應退出陷阱模式,而這個“響應”永遠都沒有到來。

Google上並沒有多少關於NSURLSession跨進程通信的說明,簡書上有一篇Blog(https://www.jianshu.com/p/4b51c85c82b3)描述了一個socket通訊管道破裂導致崩潰的場景,允許我們大膽猜測NSURLSession大概也因爲類似的原因導致永遠無法從陷阱模式退出。

類型二: iOS 11.x等系統在Swift應用崩潰後創建Background Session導致進程卡死無法退出

雖然現象是相同的,但是背後的原因卻並不相同。此類場景下導致進程卡死的調用棧如下:

0   libsystem_kernel.dylib          0x000000020bb6cf2c __psynch_mutexwait + 8
1   libsystem_pthread.dylib         0x000000020bbe8a84 _pthread_mutex_firstfit_lock_wait + 92
2   libsystem_pthread.dylib         0x000000020bbe89f4 _pthread_mutex_firstfit_lock_slow$VARIANT$mp + 272
3   libdyld.dylib                   0x000000020ba23760 dyldGlobalLockAcquire+ 14176 () + 20
4   dyld                            0x0000000107850a40 dlopen_internal + 296
5   libdyld.dylib                   0x000000020ba24908 dlopen + 176
6   CFNetwork                       0x000000020c64b9fc initMKBDeviceUnlockedSinceBoot+ 883196 () + 44
7   CFNetwork                       0x000000020c589eb4 -[__NSURLBackgroundSession setupBackgroundSession] + 76
8   CFNetwork                       0x000000020c5965c4 -[__NSURLBackgroundSession initWithConfiguration:delegate:delegateQueue:] + 492
9   CFNetwork                       0x000000020c57e314 +[NSURLSession sessionWithConfiguration:delegate:delegateQueue:] + 644
...

18  CoreFoundation                  0x000000020bfd05b8 __handleUncaughtException + 692
19  libobjc.A.dylib                 0x000000020b1aadf4 _objc_terminate+ 24052 () + 112
20  open_dev                        0x0000000104d92404 0x1048b0000 + 5121028
21  libc++abi.dylib                 0x000000020b19f838 std::__terminate(void (*)+ 55352 ()) + 16
22  libc++abi.dylib                 0x000000020b19f434 __cxa_rethrow + 144
23  libobjc.A.dylib                 0x000000020b1aabc8 objc_exception_rethrow + 44
24  CoreFoundation                  0x000000020bf5c11c CFRunLoopRunSpecific + 544
25  GraphicsServices                0x000000020e15c79c GSEventRunModal + 104
26  UIKitCore                       0x0000000238506978 UIApplicationMain + 212
27  open_dev                        0x0000000104935258 main + 545368
28  libdyld.dylib                   0x000000020ba218e0 start + 4

Swift依賴的網絡框架和OC似乎並不相同,導致在崩潰時還需要調用libdyld.dylib來啓動額外的庫以支持Background Session。與上一節不同的是,這類卡死是因爲pthread_mutex無法釋放導致的。

類型三: iOS8.x Background Session無效

貝殼現在最低支持iOS 9,所以這裏就不寫了😝😝😝

1.3 海神的同步上傳方案

由於存在以上各種問題,海神客戶端放棄了基於OC Runtime的網絡框架,使用Standard C實現同步網絡請求。

站在巨人的肩膀上總是更輕鬆,海神對curl進行裁剪和移植,作爲客戶端同步上傳的網絡基礎庫。實現的代碼如下:

curl_global_init(CURL_GLOBAL_ALL);

CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1);

//設置請求頭
struct curl_slist *headers = curl_slist_append(NULL, "Content-Encoding:gzip");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

//設置請求體
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_size);

curl_easy_perform(curl);

2. 如何獲取系統符號表

系統符號表和項目符號表類似,都是記錄可執行文件的符號信息。通常我們將設備連接到Xcode後,Xcode會自動從設備中導出系統符號表,也就是顯示“Preparing debugger support for Device”的過程。在Mac上iOS系統符號表默認的存儲路徑:

~/Library/Developer/Xcode/iOS DeviceSupport

海神平臺創建之初,目標是支持iOS 8.0開始所有發佈版本的日誌解析。我們都知道:

Crash原始日誌 + 項目符號表 + 系統符號表 = Crash解析日誌

項目符號表可以通過持續集成平臺獲取到,Crash原始日誌在上一節中也已實現,剩下的工作就是找到系統符號表。

2.1 iOS System Symbols項目

系統符號表和iOS系統是一一對應的。標準的系統版本號包括髮布版本和Build版本,同一個版本號又包含多個架構類型。以iOS 12.1爲例:

Build Version Arch
16B92 arm64、arm64e
16B93 arm64、arm64e

基本上所有公司都沒有收集系統符號表的傳統,貝殼也是😞。由於測試機數量有限,而iOS系統又持續升級,再加上很多“清理Mac存儲空間”的坑爹文章,導致海神平臺缺失不少版本和架構的符號表。

iOS-System-Symbols是Zuikyo發起的系統符號表收集項目(具體見GitHub),基本集齊了7.0-12.x的所有版本。項目還在持續更新,感謝Zuikyo的工作,海神平臺當前就是使用這套系統符號表作爲基礎。

2.2 系統符號表的及時性

iOS新版本通常會有新功能和優化體驗,而且隨着更新習慣養成和網速的不斷提升,更新成本也越來越低。Apple官方數據表明,iOS用戶越來越願意升級新版本。

iOS-System-Symbols項目更新總是在新版本發佈一段時間之後,完全依賴此項目會導致新版本系統產生Crash時,海神平臺還沒有該系統符號表。於是解析流程被中斷,Crash數量和率都出現異常。

是時候開始自動監控iOS新版本並自動生成和部署系統符號表了!

爲什麼連接設備能夠導出系統符號表?其實系統符號表和iOS固件是密不可分的,Apple沒有提供下載系統符號表的方式,但是我們完全可以通過固件提取指定版本的符號表。

The iPhone Wiki(https://www.theiphonewiki.com/)由著名的黑客geohot創建,用於收集有關iOS操作系統(及其變體,tvOS和watchOS)以及運行該軟件的設備(iPhone,iPod touch,iPad,Apple TV和Apple Watch)的所有公共知識。此項目中Firmware(https://www.theiphonewiki.com/wiki/Firmware)類別包括了幾乎所有固件,並且具有非常好的時效性。

The iPhone Wiki一定要多逛一逛🏷

仍然以iOS 12.1爲例下載iPhone 5s和iPhone XS Max的固件。之所以下載兩個固件文件,是因爲iOS 12.1支持arm64和arm64e兩種架構。當iOS系統支持多種架構類型時,需要下載全部架構的固件分別提取系統符號表,然後合併成生成多架構系統符號表。下載好的固件:

  • iPhone_4.0_64bit_12.1_16B92_Restore.ipsw
  • iPhone11,4,iPhone11,6_12.1_16B92_Restore.ipsw

雖然文件後綴爲“.ipsw”,但其本質上就是一個壓縮文件,可以通過修改後綴爲“.zip”,然後直接解壓縮得到文件目錄:

多個.dmg文件中,佔用空間最大的包含需要的系統庫。iOS 10之前的版本此文件是經過加密的,所以誕生了很多VFDecrypt工具,但是之後的版本不再加密。加載映像以後得到文件目錄:

打開/System/Library/Caches/com.apple.dyld/目錄:

dyld_shared_cache_xxx文件是所有系統庫的壓縮包,其中xxx表示具體架構。想要打開cache文件需要dyld的解壓支持。

dyld是MachO文件的開源加載庫,在iOS中主要作用於應用main函數之前的加載流程。dyld在Github上的倉庫比較老,我個人更喜歡在Apple Open Source下載iOS和OSX的開源代碼。 強烈建議

使用551.x版本(https://opensource.apple.com/tarballs/dyld/dyld-551.4.tar.gz),儘管最新版本是655.x,但其需要的編譯條件比較多,想要編譯成功略費勁。解壓得到文件目錄:

在launch-cache目錄下,能夠找到dsc_extractor.cpp、dsc_iterator.cpp,這兩個文件就是解壓cache的工具庫。打開dsc_extractor.cpp,將“#if 0”修改爲“#if 1”,然後使用clang就可以將源碼編譯爲二進制文件,命令如下:

clang dsc_extractor.cpp dsc_iterator.cpp -lc++ -o dsc_extractor

得到解壓工具後,就可以開始解壓dyld_shared_cache_xxx文件了。dsc_extractor接收兩個參數,第一個是待解壓文件地址,第二個是解壓目標目錄。命令如下:

./dsc_extractor dyld_shared_cache_arm64 arm64
./dsc_extractor dyld_shared_cache_arm64e arm64e

輸出:

...
dyld_shared_cache_extract_dylibs_progress() => 0

得到兩種結構的符號表之後,就要開始合併。合併是通過lipo實現的,lipo是一個OS X中處理通用可執行文件的工具,支持查看架構,拆分、合併文件。命令如下:

lipo -create arm64/xx/xxx arm64e/xx/xxx -output '12.1 (16B92)/xx/xxx'

xx/xxx表示xx目錄下的xxx文件。遍歷文件目錄,各種語言提供的工具比較多,這裏就不再贅述。如果想在OS X上查看文件夾下完整目錄,可以試試tree。命令如下:

brew install tree
tree xxx

合併好的文件可以通過lipo查看架構,命令如下:

lipo -info '12.1 (16B92)/xx/xxx'

輸出:

Architectures in the fat file: /12.1 (16B92)/xx/xxx are: arm64 arm64e

最後一步需要調整目錄結構。可以查看Xcode導出的系統符號表的結構:

在System和usr的外層增加目錄Symbols,系統符號表就做好了。

海神平臺當前是通過Python定時腳本抓取和分析固件信息。發現新版本系統後,自動下載、解壓、合併和上傳系統符號表。

3. symbolicatecrash解析Crash日誌的問題

3.1 symbolicatecrash是什麼

symbolicatecrash是Xcode提供的傻瓜式解析腳本,使用Perl語言開發,用於將Apple Format原始日誌中的地址解析成可讀符號。

新版本的Xcode,symbolicatecrash文件位置爲:

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

如果不能確定symbolicatecrash的位置,可以使用以下命令查找:

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

輸出:

/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

一般我們選擇使用SharedFrameworks相關目錄下的腳本。symbolicatecrash的運行依賴Xcode工具鏈,所以需要在Shell環境提供DEVELOPER_DIR:

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer

友情提示一下:做飯需要米,解析Crash需要符號表。所以請先準備好系統符號表和項目符號表。

symbolicatecrash要求系統符號表放在指定位置:

/Users/LiXiangYu/Library/Developer/Xcode/iOS DeviceSupport

項目符號表放在什麼位置就比較隨意了,不過 建議 和Crash日誌放在同一目錄下。

現在可以開始解析Crash日誌了。symbolicatecrash只需要原始Crash日誌的路徑即可實現解析,不過解析內容會被打印在控制檯,通過重定向可以很方便的輸出到文件中。

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash ~/Desktop/origin.crash > ~/Desktop/symboled.crash

如果你和你的Shell是 好朋友 的話,上面的命令會短很多:

symbolicatecrash ~/Desktop/origin.crash > ~/Desktop/symboled.crash

3.2 symbolicatecrash的錯誤

行至上一小節,簡直完美對不對?但理想是豐滿的,現實是骨感的。解析的時候,很可能會遇到這種錯誤:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/size: /Users/LiXiangYu/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Frameworks/*.framework/* (for architecture *)  truncated or malformed object (dataoff field of LC_SEGMENT_SPLIT_INFO command 12 extends past the end of the file)

還有這種錯誤:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/objdump: /Users/LiXiangYu/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Frameworks/*.framework/*: truncated or malformed object (dataoff field of LC_SEGMENT_SPLIT_INFO command 14 extends past the end of the file)

And More…

系統符號表有問題?!祖傳的系統符號表怎麼說不行就不行了呢?明明測試機連上Xcode用的好好的呢?

雖然錯誤信息非常多,但是所有的錯誤信息都包含了同一個關鍵字:LC_SEGMENT_SPLIT_INFO。如果熟悉MachO文件格式,一定會想到這是可執行文件中的一個加載命令(Load Command)。

通過MachOView分別打開有問題的符號表和相同版本沒有問題的符號表,查看MachO的文件結構。

有問題時:

無問題時:

可以看到有問題的符號表中確實多一個LC_SEGMENT_SPLIT_INFO加載命令:

我們再查看MachO的頭文件信息:

會發現LC_SEGMENT_SPLIT_INFO中指定的Data Offset遠大於Fat Header中的Offset + Size。如果在MachoView中嘗試打開:

MachoView會直接崩潰。

大概清楚錯誤的原因了。既然信息是symbolicatecrash打印出來的,那就再看下它的實現,驗證下我們的猜測。Perl是不可能學的,這輩子都不可能學的。幸好這並不影響接下來的分析。

symbolicatecrash大概有1.5k行,雖然文件比較長,但是邏輯很清晰,可以很清楚的看到symbolicatecrash如何一步一步實現Crash日誌解析。這裏不再詳細描述解析的過程,但有幾個關鍵點需要列舉:

  • 無需指定項目符號表的位置,因爲內部使用 mdfind 進行全局文件查找,需要校驗符號表
  • Apple Format文件通過字符串操作,分解爲各種元數據用於查詢
  • 聚合相同堆棧,優化解析速度
  • 核心是使用 atos 進行地址到符號的解析

fetch_symbolled_binaries 函數中,我們發現了很有意思的東西:

# <rdar://problem/21493669> 13T5280f: My crash logs aren't symbolicating
# System libraries were not being symbolicated because /usr/bin/size is always failing.
# That's <rdar://problem/21604022> /usr/bin/size doesn't like LC_SEGMENT_SPLIT_INFO command 12
#
# Until that's fixed, just hope for the best and assume no sliding. I've been informed that since
# this scripts always deals with post-mortem crash files instead of running processes, sliding shouldn't
# happen in practice. Nevertheless, we should probably add this sanity check back in once we 21604022
# gets resolved.

$real_base = $$lib{base}

# call to size failed.  Don't use this image in symbolication; don't die
# delete $$images{$b};
#print STDERR "Error in symbol file for $symbol\n"; # and log it
# next;

rdar://problem/21604022 已經不可訪問,所以歷史原因亦無從追溯了。但是從備註中基本可以確定,這個是Apple留下的一個bug…

後續Google也沒有找到更多有價值的內容,問題似乎無解了,但是這沒有阻止海神的腳步。通過前面分析symbolicatecrash的實現,對於Crash的解析我們還是得到了一些啓發。

3.3 海神的解析方案

3.3.1 更快的查找速度

作爲平臺後端,每次通過mdfind來查找文件是不能接受的。海神平臺後端直接存儲了系統符號表文件路徑,通過符號表UUID查找路徑實現直接命中符號文件。

校驗符號表操作,對於symbolicatecrash這種獨立工具來講是合理的,保證每次解析流程的準確性。但是對於海神平臺就不必要了,因爲符號表會穩定存儲在目標機器上,不會出現“變質”情況,這樣又節約一些時間。

3.3.2 聚合JSON格式數據

其實我並不理解symbolicatecrash爲什麼使用字符串操作這種Stupid的方式來解析Crash日誌,也許又是因爲什麼“歷史原因”吧。┓( ´∀` )┏

和Perl語言調用系統工具相同,Java調用系統工具也需要進行跨進程通信。由於創建進程會消耗大量系統資源,所以減少解析次數對提高解析速度非常有效。

在上面1.1小節【KSCrash與上下文】中已經討論過,海神客戶端與Server端通過JSON格式傳輸數據。使用JSON格式主要是便於Java直接將數據映射爲對象,而對象方便進行相同庫不同符號地址的聚合。

下面是一個棧幀的組成結構:

{
    "object_name": "LJBaseCrashReporter_Example",
    "object_addr": 4377903104,
    "symbol_addr": 4378464516,
    "instruction_addr": 4378464680
}
  • object_name:

    可執行文件名稱

  • object_addr:

    可執行文件加載地址

  • symbol_addr:

    函數地址

  • instruction_addr:

    調用地址

3.3.3 atos解析

可以發現,symbolicatecrash的大部分工作已經被優化掉或者由Java層承擔,核心的解析操作就可以通過調用atos命令來實現了。

atos是OS X系統的地址符號化工具,它接收可執行文件路徑、可執行文件加載地址和符號地址,需要特別注意,地址值需要轉換爲十六進制。命令如下:

atos -o 符號文件地址 -arch 架構 -l 可執行文件加載地址 符號地址1 符號地址2 ...

輸出:

符號信息1
符號信息2
...

以3.3.2小節【聚合JSON格式數據】中的樣例棧幀爲例:

atos -o LJBaseCrashReporter_Example.app.dSYM/Contents/Resources/DWARF/LJBaseCrashReporter_Example -arch arm64 -l 0x104F18000 0x104FA11A8

輸出:

-[LJCrashDebugMachsController tableView:didSelectRowAtIndexPath:] (in LJBaseCrashReporter_Example) (LJCrashDebugMachsController.m:135)

獲取的輸出中包括符號、庫、文件和行號。系統庫和商業平臺SDK經常會閹割符號表,導致atos輸出信息不完整,拆分時需要兼容。

4. 總結

以上問題解決後,海神平臺從獲取到解析Crash日誌的流程就基本完成了。

海神平臺目前作爲貝殼移動端基礎設施之一,已經融進了貝殼的整個監控體系。隨着公司業務的快速發展和新業務的出現,穩定性建設方向也出現了新的監控訴求。爲此,海神也在通過不斷的迭代來覆蓋這些新場景,爲業務的穩定發展提供有力保障。

本文轉載自公衆號貝殼產品技術(ID:beikeTC)。

原文鏈接

https://mp.weixin.qq.com/s/8rQE_bnSsswd-wTNcOevFg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章