符號
維基百科關(guān)于符號(Symbol)定義:
A symbol in computer programming is a primitive data type whose instances have a unique human-readable form.
在計算機編程中符號是一種原始的數(shù)據(jù)類型,其實例具有獨一無二的人類可讀形式。也就是說符號是一個數(shù)據(jù)結(jié)構(gòu),它包含了名稱和類型等元數(shù)據(jù),符號對應(yīng)一個函數(shù)或者數(shù)據(jù)的地址。
符號表(Symbol Table)
符號表是內(nèi)存地址與符號的映射表,存儲了當(dāng)前文件的符號信息,靜態(tài)鏈接器ld和動態(tài)鏈接器dyld在鏈接的過程中都會讀取符號表。
打包上線的時候(Release模式)會把調(diào)試符號等裁剪掉,但是線上一旦出現(xiàn)問題上報了Crash log我們?nèi)绾沃缹?yīng)的報錯代碼位置和調(diào)用堆棧呢,這就需要把符號寫到另外一個單獨的文件里,也就是dSYM文件。dSYM(debug symbols)是iOS的符號表文件,存儲著16進制地址信息和符號的映射文件,可以幫我們將Crash堆棧信息中的地址信息符號化。
文件名樣式:MyApp.app.dSYM,它可以使我們將堆棧信息中的地址信息還原成對應(yīng)的符號,以便于問題的定位和修復(fù)。
如何生成dSYM文件

符號相關(guān)配置
Deployment Postprocessing
Deployment Postprocessing是編譯生成目標文件后是否要進行后續(xù)處理的配置項
配置為Yes,編譯生成目標文件后要進行后續(xù)處理,比如符號裁剪
配置為No,不會進行后續(xù)處理
Strip Linked Product
當(dāng)Deployment Postprocessing為Yes時,Strip Linked Product的設(shè)置才會有效。
配置為Yes,進行符號裁剪
配置為No,不進行符號裁剪

日常開發(fā)過程中,我們是需要符號信息存在的,通常Debug模式下會將Deployment Postprocessing設(shè)置為No,Release模式下設(shè)置為Yes,這樣Debug模式下一旦有問題可以及時暴露并修復(fù)。
Crash分析
一般來說發(fā)現(xiàn)Crash有開發(fā)階段和發(fā)布線上階段兩種情況。
開發(fā)階段
開發(fā)階段debug模式下,在Xcode中碰到Crash時控制臺會打印出崩潰信息,幫助我們排查問題。此時log信息大致如下
2021-06-18 20:52:01.931022+0800 MyApp[2706:35625050] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber length]: unrecognized selector sent to instance 0xbed641d619417a05'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff20421af6 __exceptionPreprocess + 242
1 libobjc.A.dylib 0x00007fff20177e78 objc_exception_throw + 48
2 CoreFoundation 0x00007fff204306f7 +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
3 CoreFoundation 0x00007fff20426036 ___forwarding___ + 1489
4 CoreFoundation 0x00007fff20428068 _CF_forwarding_prep_0 + 120
5 MyApp 0x0000000101a2cfb0 -[MIHomeINViewController loadWebPageRequestWithUrl:] + 208
6 MyApp 0x0000000101a2ce9b -[MIHomeINViewController loadWebPageRequestWithUrl:moduleDict:type:frameIndex:] + 379
7 CoreFoundation 0x00007fff204282fc __invoking___ + 140
8 CoreFoundation 0x00007fff204257b6 -[NSInvocation invoke] + 303
9 MyApp 0x0000000101d7d56f __ASPECTS_ARE_BEING_CALLED__ + 4111
10 CoreFoundation 0x00007fff20425dc0 ___forwarding___ + 859
...
)
這樣根據(jù)崩潰log信息我們能夠很快定位到崩潰問題所在的類文件、方法。
日常開發(fā)中還可以利用斷點來快速定位代碼崩潰位置。

發(fā)布線上階段
前面講過了,一旦到了發(fā)布線上(Release)階段,符號會被裁剪掉,為什么要裁剪呢
減少包體積大小
避免被逆向分析(符號裁剪不能保證不被逆向分析,符號化就是逆向工程的研究重點研究內(nèi)容之一)
這就意味著Release包發(fā)生Crash后,拿到的Crash log是未經(jīng)符號化的.
一般Crash日志來源有以下兩種:
-
蘋果收集的Crash日志
Xcode -> Window -> Organize -> Crashes中查看
用戶手機設(shè)置 -> 隱私 -> 分析與改進 -> 分析數(shù)據(jù)里查看
-
應(yīng)用內(nèi)收集
Crash收集SDK,比如項目中所用的OMGCrashReportsSDK,第三方的KSCrash等,上報到自建分析平臺
接入APM產(chǎn)品,比如Bugly、Fabric等
這個階段拿到的Crash日志大部分都是以下樣式:
Incident Identifier: *******
CrashReporter Key: *******
Hardware Model: iPhone6,2
Process: MyApp [1148]
Path: /var/containers/Bundle/Application/*******/MyApp.app/MyApp
Identifier: *******
Code Type: ARM-64
Parent Process: ? [1]
Date/Time: 2021-05-11T11:24:28Z
Launch Time: 2021-05-11T11:21:59Z
OS Version: iOS 12.5.2 (16H30)
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000001
Crashed Thread: 0
Thread 0 Crashed:
0 libGPUSupportMercury.dylib 0x23c67efe4 0x23c67d000 + 8164
1 libGPUSupportMercury.dylib 0x23c67ffac 0x23c67d000 + 12204
2 xxxxxxxx 0x240ce8404 0x240cc2000 + 156676
3 GLEngine 0x241d92234 0x241cb1000 + 922164
4 OpenGLES 0x22393eaa4 0x223937000 + 31396
5 MyApp 0x10369c584 0x101044000 + 40207748
6 MyApp 0x10368f9a0 0x101044000 + 40155552
7 MyApp 0x10370e7e0 0x101044000 + 40675296
8 MyApp 0x1036f4364 0x101044000 + 40567652
9 MyApp 0x10373aa64 0x101044000 + 40856164
10 MyApp 0x1036f2a54 0x101044000 + 40561236
11 MyApp 0x1036f1b78 0x101044000 + 40557432
12 QuartzCore 0x224b43ff0 0x224b32000 + 73712
...
Crash log分析
既然線上發(fā)布階段Crash log是未經(jīng)符號化的,那么一旦發(fā)生Crash,如何進行Crash分析呢?
先來看下Crash log的結(jié)構(gòu)以及每個字段包含的信息是什么,這些內(nèi)容可以幫助我們診斷崩潰來源的信息。

一份Crash log打開后結(jié)構(gòu)劃分大致如圖所示。對我們來說需要重點關(guān)注以下三個部分:
-
Header(標題),用來描述發(fā)生崩潰的環(huán)境
Incident Identifier: // 報告唯一標識符 CrashReporter Key: // 匿名的每個設(shè)備的標識符 Hardware Model: // 運行應(yīng)用程序的設(shè)備型號 Process: // 崩潰進程的可執(zhí)行文件名 Path: // 可執(zhí)行文件在磁盤上的位置 Identifier: // 崩潰的進程 Code Type: // 崩潰進程的CPU架構(gòu) Parent Process: // 啟動崩潰進程的名稱和進程ID Date/Time: // 崩潰的日期和時間 Launch Time: // 應(yīng)用程序啟動的日期和時間 OS Version: // 發(fā)生崩潰的系統(tǒng)版本號 -
Exception Information
Exception Type: // 終止進程的異常的名稱 Exception Codes: // 異常編碼信息這塊信息會告訴我們進程終止的原因是什么,但是它無法完全解釋應(yīng)用程序終止的原因,因為這塊提供的信息是有限的。
-
Exception Backtrace
Crashed Thread: 0 Thread 0 Crashed: 0 libGPUSupportMercury.dylib 0x23c67efe4 0x23c67d000 + 8164 1 libGPUSupportMercury.dylib 0x23c67ffac 0x23c67d000 + 12204 2 xxxxxxxx 0x240ce8404 0x240cc2000 + 156676 3 GLEngine 0x241d92234 0x241cb1000 + 922164 4 OpenGLES 0x22393eaa4 0x223937000 + 31396 5 MyApp 0x10369c584 0x101044000 + 40207748 6 MyApp 0x10368f9a0 0x101044000 + 40155552 7 MyApp 0x10370e7e0 0x101044000 + 40675296 8 MyApp 0x1036f4364 0x101044000 + 40567652 9 MyApp 0x10373aa64 0x101044000 + 40856164 10 MyApp 0x1036f2a54 0x101044000 + 40561236 11 MyApp 0x1036f1b78 0x101044000 + 40557432 12 QuartzCore 0x224b43ff0 0x224b32000 + 73712Exception Backtrace記錄了程序終止時在線程上運行的代碼,回溯的代碼調(diào)用堆棧和Xcode調(diào)試暫時或崩潰時看到的類似,區(qū)別在于調(diào)用信息都是16進制地址。
第一列數(shù)字序號代表堆棧幀號,堆棧幀調(diào)用順序排列,第0幀是最后在執(zhí)行的方法,第1幀是調(diào)用第0幀方法的方法,以此類推,也就是反向調(diào)用的順序。
第二列(MyApp)代表正在執(zhí)行方法的二進制文件名
第三列(0x10369c584)是正在執(zhí)行的機器指令的地址
第四列(0x101044000)是要二進制鏡像入口地址,在符號化后會顯示為要執(zhí)行的方法名(SEL)
第五列(+ 40207748)是從方法入口點到方法中當(dāng)前指令的字節(jié)偏移量
Crash符號化
-
Xcode本身也提供了工具來幫助開發(fā)者完成符號化的工作。
-
symbolicatecrash,這是一個將堆棧地址符號化的腳本,執(zhí)行命令:./symbolicatecrash MyApp.crash MyApp.app.dSYM > MyApp.log。這種方式局限性較大
只能分析官方格式的Crash log,Crash log需要從具體設(shè)備中取出,存在一定的限制性
會出現(xiàn)符號化失敗的情況
-
atos,這個命令的特點是可以對單行堆棧進行符號化操作// 命令格式 atos -arch <BinaryArchitecture> -o <PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName> -l <LoadAddress> <AddressesToSymbolicate> // 示例 atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x104a10000 0x00000001066f56f4 // 輸出 -[HomeViewController configureViewTabBarViewWhenApplicationActive] (in MyApp) (HomeViewController.m:370)atos命令可以把地址里的16進制信息替換成等價的符號。如果調(diào)試符號信息是完備的,則atos的輸出信息將會包含文件名和對應(yīng)的資源行數(shù)。atos命令可以被用來單獨符號化那些未符號化或者部分符號化過的crash log中的堆棧信息里的地址。
-
-
Crash收集SDK收集Crash log并上傳自建分析平臺,打包時腳本上傳dSYM文件,在自建平臺完成log解析
crash_05.png如圖所示,平臺在完成Crash log解析后,調(diào)用堆棧、崩潰發(fā)生App的信息、用戶信息等都完整的解析出來,對于開發(fā)同學(xué)來說是最友好的,能夠更加全面的對問題進行分析和解決。
特殊需求
上面的Crash分析說的都是針對普通情況下的,面對業(yè)務(wù)中的特殊場景和特殊需求,仍然有著特殊的Crash分析和解決方案。
比如在我們的業(yè)務(wù)中,App啟動時會根據(jù)業(yè)務(wù)的不同需求來加載AB兩套服務(wù),當(dāng)然A、B服務(wù)加載哪一個都能正常的保證App業(yè)務(wù)流程的正常使用。如果在App的啟動過程中A服務(wù)發(fā)生崩潰,不管是A服務(wù)自己SDK不穩(wěn)定導(dǎo)致,還是我們的使用存在問題,受影響的都會是用戶。
特殊場景需求:
能夠自動識別出是A服務(wù)的必現(xiàn)Crash,如果是偶現(xiàn)的crash不在兜底范圍之內(nèi)
確認是A服務(wù)導(dǎo)致Crash后,App下次啟動時切換為B服務(wù),等A服務(wù)修復(fù)完Crash之后能夠自動從兜底的底圖恢復(fù)到A服務(wù)
這個需求是要在線上發(fā)布的App中完成Crash的收集和分析處理,前面也講到了Release的App都會將符號裁剪掉,這種信息是無法做Crash分析處理的,但是線上Release包也沒有dSYM文件,這種況下如不借助dSYM文件一般是無法符號化的,那么有沒有其他思路進行符號化呢
符號化思路
從上面的Crash log可以了解到,調(diào)用堆棧的第三列是正在執(zhí)行的機器指令地址,那么這個地址(目標地址)再往前推進肯定就是當(dāng)前正在調(diào)用的方法地址,如果能夠拿到所有的方法地址,然后拿買一個方法地址和目標地址進行比較,與目標地址距離最近的那個地址所對應(yīng)的方法就是當(dāng)前正在調(diào)用的方法,也就是我們要得到的符號。
// 示例
5 MyApp 0x10369c584 0x101044000 + 40207748
取0x10369c584和項目中拿到的每一個方法地址作比較,與0x10369c584差值最小的地址所對應(yīng)的方法和其所屬的類就是最終的目標符號信息。
取0x10369c584和項目中拿到的每一個方法地址作比較,與0x10369c584差值最小的地址所對應(yīng)的方法和其所屬的類就是最終的目標符號信息。</pre>
那么如何拿到所有的方法地址?
自己去解析內(nèi)存中加載的Mach-O文件,根據(jù)Mach-O文件格式先找到Class信息,然后找到對應(yīng)的Method信息,Method中保存了方法IMP(方法地址)和SEL(方法名)。但是這種方案涉及到了逆向工程的一些東西,過于復(fù)雜。
-
App在點擊App啟動到
main()之前,會使用dyld初始化運行環(huán)境,加載程序相關(guān)依賴庫,并對其鏈接和初始化,然后runtime會項目中所有類進行類結(jié)構(gòu)初始化,然后調(diào)用所有l(wèi)oad方法,最后dyld返回main函數(shù)地址,然后main函數(shù)被調(diào)用。這個過程會把程序中的所有方法加載到內(nèi)存中,我們在main()之后就可以使用objc提供的一系列方法拿到所有的Class和其對應(yīng)的Method。unsigned int classCount; const char **classNames; // 獲取指定類所在的動態(tài)庫 const char * _Nonnull image = class_getImageName(self.class); // 獲取指定庫或框架中所有類的名稱 classNames = objc_copyClassNamesForImage(image, &classCount);
綜合來看,對于當(dāng)前需求第二種方案會更加方便實現(xiàn)一些,那么現(xiàn)在來看第二種方案的技術(shù)實現(xiàn)。
技術(shù)實現(xiàn)
-
核心流程圖
crash_07.png -
代碼實現(xiàn)
+ (NSDictionary *)crashParseForKeyInfo:(NSArray<NSString *> *)stackSymbols { unsigned int classCount; const char **classNames; // 獲取指定類所在的動態(tài)庫 const char * _Nonnull image = class_getImageName(self.class); // 獲取指定庫或框架中所有類的名稱 classNames = objc_copyClassNamesForImage(image, &classCount); NSDictionary *methodAddressDict = [self getMethodAdressDict:classNames classCount:classCount]; ... NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary]; [stackSymbols enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { ... __block NSString *crashMethodAddress = nil; [methodAddressDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary *obj, BOOL * _Nonnull stop) { ... if (crashMethodAddressNumber >= methodAddressNumber) { tmpSpan = crashMethodAddressNumber - methodAddressNumber; if (tmpSpan > kAdressSpanKey) { return; } if (tmpSpan >= minSpan) { return; } minSpan = tmpSpan; crashMethodAddress = key; } }]; NSDictionary *crashInfoDict = methodAddressDict[crashMethodAddress]; tmpDict[kCrashClassNameKey] = crashInfoDict[kCrashClassNameKey]; tmpDict[kCrashMethodNameKey] = crashInfoDict[kCrashMethodNameKey]; *stop = YES; }]; return [tmpDict copy]; } // 獲取當(dāng)前項目所有類的className、methodName + (NSDictionary *)getMethodAdressDict:(const char **)classNames classCount:(unsigned int)count { NSMutableDictionary *tmpAddressDict = [NSMutableDictionary dictionary]; for (unsigned int i = 0; i < count; i++) { const char *className = classNames[i]; NSString *classNameStr = [NSString stringWithUTF8String:className]; // 根據(jù)字段串反射為類對象 Class cls = NSClassFromString(classNameStr); // 獲取當(dāng)前類對象的所有實例方法 [tmpAddressDict addEntriesFromDictionary:[self getClassInfo:cls]]; // 獲取當(dāng)前類對象的所有類方法 [tmpAddressDict addEntriesFromDictionary:[self getClassInfo:object_getClass(cls)]]; } return [tmpAddressDict copy]; } // 獲取當(dāng)前類或元類的信息 + (NSDictionary *)getClassInfo:(Class)cls { unsigned int methodCount; Method *methodList = class_copyMethodList(cls, &methodCount); NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary]; for (unsigned int j = 0; j < methodCount; j++) { Method method = methodList[j]; // 獲取方法IMP IMP imp = method_getImplementation(method); // 獲取方法SEL SEL selector = method_getName(method); NSString *methodAddress = [NSString stringWithFormat:@"%p", imp]; NSMutableDictionary *tmpInfoDict = [NSMutableDictionary dictionary]; NSString *className = NSStringFromClass(cls) ? NSStringFromClass(cls) : @""; NSString *methodName = NSStringFromSelector(selector) ? NSStringFromSelector(selector) : @""; tmpInfoDict[kCrashClassNameKey] = className; tmpInfoDict[kCrashMethodNameKey] = methodName; tmpDict[methodAddress] = [tmpInfoDict copy]; } free(methodList); return [tmpDict copy]; } -
效果
線上stackSymbols crash日志
stackSymbols: 0 CoreFoundation 0x000000018c7d6768 4FBDF167-161A-324C-A233-D516922C67E5 + 1218408; 1 libobjc.A.dylib 0x00000001a129d7a8 objc_exception_throw + 60; 2 CoreFoundation 0x000000018c6d55f8 4FBDF167-161A-324C-A233-D516922C67E5 + 165368; 3 MyApp 0x0000000102f37cd8 MyApp + 5536984; 4 UIKitCore 0x000000018f3d71d0 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 14537168; 5 UIKitCore 0x000000018f3d6d78 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 14536056; 6 UIKitCore 0x000000018f3d7538 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 14538040; 7 UIKitCore 0x000000018f6a1c98 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 17464472; 8 UIKitCore 0x000000018f1d46c4 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12428996; 9 UIKitCore 0x000000018f1c303c 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12357692; 10 UIKitCore 0x000000018f1f6f10 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12570384; 11 CoreFoundation 0x000000018c74f5e0 4FBDF167-161A-324C-A233-D516922C67E5 + 665056; 12 CoreFoundation 0x000000018c749704 4FBDF167-161A-324C-A233-D516922C67E5 + 640772; 13 CoreFoundation 0x000000018c749cb0 4FBDF167-161A-324C-A233-D516922C67E5 + 642224; 14 CoreFoundation 0x000000018c749360 CFRunLoopRunSpecific + 600; 15 GraphicsServices 0x00000001a3d87734 GSEventRunModal + 164; 16 UIKitCore 0x000000018f1c4584 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12363140; 17 UIKitCore 0x000000018f1c9df4 UIApplicationMain + 168; 18 MyApp 0x00000001029f40c8 MyApp + 16584; 19 libdyld.dylib 0x000000018c405cf8 E574A365-9878-348A-8E84-91E163CFC128 + 7416線上堆棧符號化結(jié)果:
symbolsString: className:HomeViewController; methodName:getName:; className:AppDelegate; methodName:application:didFinishLaunchingWithOptions: -
方案缺點
受限于objc相關(guān)方法的局限性,目前堆棧符號化僅僅是針對OC代碼的,C和C++代碼無法完成符號化。
需要用項目所有方法(目前項目大約19萬條左右)進行遍歷,需要消耗較長時間完成符號化工作,根據(jù)需求特點做完優(yōu)化后耗時大約1s左右。
但上述方案僅僅是在App啟動即發(fā)生Crash時才會進入上述符號化流程,故不會影響正常啟動。
問題
多個Crash信息收集功能并存
當(dāng)工程大到需要多個團隊共同開發(fā)一個App的時候,就可能會出現(xiàn)多個Crash收集功能并存的問題,如果處理不好后邊加入的收集功能可能就會影響已經(jīng)存在的Crash收集模塊的正常使用。
比如多方均通過NSSetUncaughtExceptionHandler注冊異常處理,如果想要讓大家的收集功能都能發(fā)揮各自的作用,可以采用下面的解決方案。
+ (void)registerHandlerWithMonitorString:(NSString *)key {
if (NSGetUncaughtExceptionHandler() != MyExceptionHandler) {
OldHandler = NSGetUncaughtExceptionHandler();
}
NSSetUncaughtExceptionHandler(&MyExceptionHandler);
}
void MyExceptionHandler(NSException *exception) {
NSArray *callStack = exception.callStackSymbols;
// 處理crash信息
// 調(diào)用之前已經(jīng)注冊的handler
if (OldHandler) {
OldHandler(exception);
}
}
在注冊時將之前別人注冊的handler取出備份,在自己的MyExceptionHandler中處理完后將別人的handler注冊傳遞回去,這樣大家都能完成自己的工作,皆大歡喜~
附錄
如何debug環(huán)境在Xcode中調(diào)試signal異常呢,因為Xcode屏蔽了signal回調(diào),所以需要lldb命令來幫助我們拿到回調(diào)信息。在執(zhí)行abort()代碼處打斷點,執(zhí)行到這行代碼時控制臺輸入:
pro hand -p true -s false SIGABRT
其中,SIGABRT可替換為其他signal異常類型?;剀嚭罂刂婆_輸出:
NAME PASS STOP NOTIFY
=========== ===== ===== ======
SIGABRT true false true
表示已跳出Xcode屏蔽,可以拿到signal異?;卣{(diào)了。

