iOS 符號二三事

1、瞭解符號

一、基礎概念

  • 符號(Symbol):簡單來講,類、函數和變量的統稱;類名、函數名或變量名稱爲符號名(Symbol Name);git

  • 按類型分,符號能夠分三類:github

    • 全局符號:目標文件外可見的符號,能夠被其餘目標文件引用,或者須要其餘目標文件定義;
    • 局部符號:只在目標文件內可見的符號,指只在目標文件內可見的函數和變量;
    • 調試符號:包括行號信息的調試符號信息,行號信息中記錄了函數和變量所對應的文件和文件行號
  • 符號表(Symbol Table):符號表是內存地址與函數名、文件名、行號的映射表;每一個定義的符號有一個對應的值,叫作符號值(Symbol Value),對於變量和函數來講,符號值就是他們的地址;符號表元素以下所示:objective-c

    <起始地址> <結束地址> <函數> [<文件名:行號>]
    複製代碼
  • dSYM(debug symbols):是iOS的符號表文件,存儲16進制地址信息和符號的映射文件;文件名一般爲:xxx.app.dSYM,相似Android構建release產生的mapping文件;利用dSYM文件文件,能夠將堆棧信息中地址信息還原成對應的符號,幫助問題排查;shell

二、符號的存儲位置

  • Mach-O 是 Mac/iOS 平臺上通用的二進制格式;App可執行文件、動態庫、靜態庫等都是 Mach-O 格式;更多Mach-O的知識可看Mach-O文件周邊二三事
  • Mach-O 中能夠保存有調試信息,Mach-O採用DWARF (Debug With Arbitrary Record Foramt) 的標準調試信息格式。DWARF 中調試信息也能夠單獨用文件保存,叫 dSYM (Debug Symbol File) 文件,格式後綴名稱爲 .dSYM。
  • DWARF(debugging with attributed record formats):是一種調試信息的存儲格式,用來支持源代碼級別的調試。Release打包時候會把調試符號等裁剪掉,可是線上統計到的堆棧咱們仍然要可以知道對應的源代碼,這時候就須要把符號寫到另一個單獨的文件裏,這個文件就是dSYM。
  • 通常地,靜態庫的全局符號、局部符號以及行號信息等保存在對應的二進制文件中;文件中 Symbol Table 存儲着全局符號和局部符號DWARF 存儲着符號的行號信息。
  • 通常地,App可執行文件和動態庫的Mach-O文件和dSYM都會保存全局符號局部符號,而dSYM文件中的 DWARF存儲着行號信息。

三、符號的做用

  • 符號讓庫文件能夠被引用,讓目標文件能夠相互連接生成可執行文件;
  • 符號還能夠幫助開發者定位問題,比較常見的是:將Crash日誌中的地址信息轉化成對應的函數名以及文件名、文件行號信息
  • Release模式會將二進制中裁剪掉符號的,不過Release模式下默認有dSYM文件,能夠根據dSYM文件來作符號化。

2、Xcode符號相關配置

一、配置簡介

Xcode 編譯時有幾個選項是和符號是相關的。數組

  • Debug Information FormatDWARF OR DWARF with dSYM File。 這配置對於靜態庫會無影響;對動態庫有影響:設置爲 DWARF with dSYM File,生成動態庫時會生成相應的 dSYM 文件;若是設置爲 DWARF,則 dwarf 段即調試信息沒有地方存放將丟失。bash

  • Generate Debug Symbols:設置爲YES,編譯生成目標文件時會生成調試信息;設置爲 NO,那麼 dwarf 段不會生成,也不會有 dSYM 文件生成,而且調試過程使用的斷點也不會生效,由於地址已經沒法和對應代碼行關聯起來了。app

  • Deployment函數

    • Deployment Postprocessing:若是爲 YES在編譯生成目標文件以後要進行後續處理;若是爲 NO,則不會有後續處理;使用 Xcode Archive 進行編譯,Deloyment Postprocessing 的值恆爲YESpost

    • Strip Linked Product:若是爲 YES,則進行裁剪;若是爲 NO,則不進行裁剪;至於裁剪什麼級別的符號由 Strip Style 配置決定;若是Deployment Postprocessing爲NO,Strip Linked Product設置無效;測試

    • Strip Style(Deployment Postprocessing和Strip Linked Product都爲YES,才生效;去除的是二進制中的符號):

      • Debugging Symbols :會將調試符號從二進制中刪除掉,即去除 DWARF 信息;
      • Non-Global Symbols :會將局部符號和調試符號從二進制中刪除掉,即去除 DWARF 信息以及部分 Symbol Table 中的信息;
      • All Symbols :去除所有符號,即去除 DWARF 中的調試信息以及Symbol Table 中目標模塊定義的全局、局部符號信息。

      補充1: 動態庫和靜態庫不能去除所有符號(Strip All Symbols),要保留全局符號(選擇Non-Global Symbols),他們是庫和其餘庫連接時溝通的橋樑;失去了全局符號,動態庫和靜態庫就成爲了黑盒。

      補充2: 去除符號的操做對於 dSYM 文件中的符號信息沒有影響;對於動態庫和可執行二進制文件,能夠將符號儘量去除掉減小二進制體積的大小。須要符號進行符號化崩潰日誌時,再從 dSYM 文件中找對應符號。

  • Symbols Hidden by Default :這是全局的開關,用來設置符號的默承認見性,設置爲YES,會把全部符號都定義成」private extern」;

    • 也能夠可使用編譯器屬性__attribute__((visibility("default")))__attribute__((visibility("hidden")))來控制符號的可見性;

      __attribute__((visibility("default"))) void MyFunction1() {} //可見
      __attribute__((visibility("hidden"))) void MyFunction2() {}  //不可見
      複製代碼

二、App的Debug模式下配置

  • Debug Information Format 設置爲 DWARF;由於生成 dSYM 文件是一個比較耗時的過程,選擇DWARF能節省調試時間;

  • Generate Debug Symbols:設置爲 YES;這樣才能支持斷點調試;注意Debug模式下,Deployment Postprocessing 必定要NO,不然Generate Debug Symbols的設置了YES,也不支持斷點調試;

  • Deployment配置

    • Deployment Postprocessing 設置爲 NO
    • Strip Linked Product 設置爲 NO
    • Strip Style 設置爲 All Symbols (由於Strip Linked Product 是NO,Strip Style隨便配置什麼都無影響)

    如此配置後,App二進制中帶有全局符號和局部符號信息,二進制自己能夠支持不使用 dSYM 文件自解析出符號(自解析出的符號不包含行號)。

  • Symbols Hidden by Default 設置爲YES;

三、App的Release模式下配置

  • Debug Information Format 設置爲 DWARF with dSYM File;這樣生成ipa的同時,會一併生成 dSYM文件。

  • Generate Debug Symbols:設置爲 NO;這樣才能支持斷點調試;注意Debug模式下,Deployment Postprocessing 必定要NO,不然Generate Debug Symbols的設置了YES,也不支持斷點調試;

  • Deployment配置

    • Deployment Postprocessing 設置爲 YES
    • Strip Linked Product 設置爲 YES
    • Strip Style 設置爲 All Symbols (由於Strip Linked Product 是NO,Strip Style隨便配置什麼都無影響)

    如此配置,App中不帶任何符號,能夠減小安裝包大小,還能避免符號泄漏。定位問題時,能夠經過 dSYM 文件去獲取符號。

  • Symbols Hidden by Default 設置爲YES;

四、靜態庫和動態庫配置

  • 靜態庫配置:Debug Information Format默認就好,Generate Debug Symbols設置YES,Deployment Postprocessing設置爲 NO,Strip Linked Product 設置爲 NO,Strip Style 默認就行(Strip Linked Product設置爲 NO,Strip Style配置無所謂Symbols Hidden by Default設置爲NO;

    靜態庫如此配置,實際上是沒有裁剪二進制的符號的;所以,靜態庫的二進制的大小將會大大增長,可是靜態庫的大小並不影響最終安裝包二進制的大小,同時調試符號能支持安裝包或者連接的動態庫生成相應的 dSYM 文件,方便定位靜態庫中的問題。

  • 動態庫配置:Debug Information Format分Debug和Release,配置同App;Generate Debug Symbols設置YES,Symbols Hidden by Default設置爲NO;

    • ReleaseDeployment Postprocessing 設置爲 YES;Strip Linked Product 設置爲 YES; Strip Style 設置爲 Non-Global Symbols
    • Debug下Deployment Postprocessing設置爲 NO;Strip Linked Product 設置爲 NO;Strip Style 設置啥均可以(由於Strip Linked Product 是NO,Strip Style隨便配置什麼都無影響)

    若是動態庫Symbols Hidden by Default設置爲 YES,動態庫仍然能編譯經過,可是App會報一堆連接錯誤,由於符號變成了hidden。

3、符號小知識

一、weak symbol

  • symbol默認是strong的,可是能夠增長 __attribute__ ((weak))屬性將其變成weak symbol;weak symbol在連接時候比較特殊:

    • strong symbol必須有實現,不然會報錯;
    • 不能夠存在兩個同名的strong symbol
    • strong symbol能夠覆蓋weak symbol的實現
  • 應用場景:用weak symbol提供默認實現,外部能夠提供strong symbol把實現注入進來,以此來實現依賴注入

二、other linker flags配置

  • ld(靜態連接器)連接靜態庫時,只有.a中的某個.o符號被引用的時候,這個.o纔會被ld寫到最後的二進制文件中,不然會被丟掉,other linker flags提供三個選項來解決保留代碼的問題。
    • -ObjC 保留全部Objective C的代碼;
    • -force_load 保留某一個靜態庫的所有代碼;
    • -all_load 保留參與連接的所有的靜態庫代碼;
  • 不少SDK在集成進App,會要求在other linker flags裏添加*-ObjC*。

三、找不到符號的錯誤

  • 當連接的時候類找不到了,會報錯符號_OBJC_CLASS_$_CLASSNAME找不到;

  • 以前在接入 AlipaySDK遇到過(緣由是: AlipaySDK和阿里百川SDK衝突致使,須要接入UTDID framework

    Undefined symbols for architecture x86_64:
    "_OBJC_CLASS_$_UTDevice", referenced from:
    objc-class-ref in AlipaySDK
    ld: symbol(s) not found for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    複製代碼

    補充1:若是類的符號沒有被裁減掉,運行時就用_OBJC_CLASS_$_CLASSNAME做爲參數,經過dlsym來獲取類指針。

    補充2:nm app_name.app/app_name 執行返回中,小寫字母對應着本地符號,大寫字母表示全局符號;U表示undefined,即未定義的外部符號;

四、lldb符號調試

  • 運行時,使用還能夠用lldb去查詢符號相關的信息;

  • 查看符號的定義

    image lookup -t symbol_name
    複製代碼
  • 查看符號的位置

    image lookup -s symbol_name 
    複製代碼
  • 設置符號斷點

    breakpoint set -F "symbol" #也可經過Xcode的GUI能設置
    複製代碼

4、符號裁剪後...

一、概述

  • 開發階段,不會裁剪符號,因此一切都比較美好;對一個地址進行符號化比較直接:找到地址所屬的內存鏡像,而後定位該鏡像中的符號表,最後從符號表中匹配目標地址的符號。

  • 可是裁剪符號的包,如企業內測包AppStore選擇了裁剪符號的方式,甚至是裁剪所有符號(Strip Style 設置爲 All Symbols );常規符號化不能解決問題;

    符號裁剪的好處:減小了安裝包大小,還避免符號泄漏;

  • 企業內測包不一樣於AppStore包,主要用於內部測試和灰度,有時候須要收集問題的上下文信息,這些信息中包括髮生問題的代碼行數、代碼文件和函數名,甚至包括堆棧符號;

  • 裁剪符號後,[NSThread callStackSymbols] 獲取的不少堆棧地址須要符號恢復;而此時的dladdr也不能根據地址獲取符號信息;

二、獲取當前位置行號等

  • 不論符號有沒有被裁剪,均可以經過如下C語言中的預約義符獲取,具體以下:

    __FILE__      //File path
    __LINE__      //Code Line
    __FUNCTION__  //Funcation Name
      
    //demo
    printf("File = %s\nLine = %d\nFunc=%s\n", __FILE__, __LINE__, __FUNCTION__);
    複製代碼
  • 不論符號有沒有被裁剪,也能夠經過Objective-C的_cmd方法獲取當前方法名,eg以下

    printf("call %s", [NSStringFromSelector(_cmd) UTF8String]);
    複製代碼
  • 經過預約義符_cmd方法獲取當前調試符號信息,在系統API中也常有使用,如NSAssert宏的使用,源碼以下:

    #define NSAssert(condition, desc, ...)	\
        do {				\
    	__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
    	if (__builtin_expect(!(condition), 0)) {		\
                NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
                __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
    	    [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
    		object:self file:__assert_file__ \
    	    	lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
    	}				\
            __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
        } while(0)
    #endif
    複製代碼

    NSAssert適合於Objective-C方法,利用__FILE____LINE___cmd來獲取發生問題時候的代碼文件路徑、代碼行數、方法名,而後將這些交給[[NSAssertionHandler currentHandler] handleFailureInMethod:object:file:lineNumber:description:]處理;

三、恢復當前線程堆棧符號方案

  • 咱們習慣性利用[NSThread callStackSymbols]獲取當前線程調用堆棧符號信息,可是這隻在Debug模式下比較理想;在符號被裁剪的狀況下,獲取的地址須要作符號恢復;

  • 若是利用dSYM文件來符號化是能夠的;能夠參考個另類方案:根據全部的類方法、方法名、方法實現地址,將調用棧的內存地址符號化;相似Frida調用棧符號恢復,方案具體描述:

    • App啓動後x秒,獲取全部的類方法、方法名、方法實現地址
    • 執行[NSThread callStackReturnAddresses]獲取調用棧的內存地址;
    • 遍歷全部的方法地址 與 調用棧的地址比較並計算距離,若是方法地址小於目標地址且距離最小,那麼該方法就是咱們要找到的符號。
    • 最後,將調用棧上面的全部地址替換成對應的符號便可。
  • 須要說明的是,這裏的符號指的是:Objective-C的函數符號,由於若是C函數符號被strip後,是沒有辦法恢復其符號的;

  • 在符號裁剪狀況下,dladdr通常不能經過地址獲取到符號;能夠用以下代碼測試

    NSArray<NSNumber *> *addresses = [NSThread callStackReturnAddresses];	
    NSNumber *firstAddress = [addresses objectAtIndex:0];
    Dl_info info;
    int result = dladdr((const void *)[firstAddress integerValue], &info);
    if (result != 0 && info.dli_sname) {
        //Debug模式配置 
        printf("經過dladdr函數獲取symbol_name = %s", [[NSString stringWithUTF8String:info.dli_sname] UTF8String]);
    } else {
        //Release模式配置
        printf("符號裁剪後,不能經過dladdr函數獲取符號,須要[新符號恢復方案]");
    }
    複製代碼

    若是dladdr能經過地址拿到符號信息,就說明符號沒有裁剪,能夠直接用[NSThread callStackSymbols]

四、to be continued...

  • 在符號被裁剪的狀況下,利用些必要手段獲取更多的上下文信息,能很好幫助問題解決,這些上下文信息還包括:設備信息,用戶主要操做路徑,當前的ViewController等;
  • 對於Crash問題,作好符號化是很是正常的選擇;是符號化的主要應用場景;

5、Crash與符號化

一、概述

  • 分析好Crash問題,必然要作好Crash捕獲堆棧信息收集堆棧符號化三件大事;

二、Crash捕獲

  • Crash主要有兩類:Mach 異常Objective-C 異常(NSException)引發的;

  • Mach異常是最底層的內核級異常,如EXC_BAD_ACCESS(內存訪問異常);而Objective-C 層不能獲取Mach異常,可是Mach 異常到了 BSD 層會轉換爲對應的 Signal 信號,咱們能夠註冊SIGABRT, SIGBUS, SIGSEGV等信號發生時的處理函數。

    //註冊處理SIGSEGV信號
    signal(SIGSEGV,handleSignal); 
    // 註冊處理其餘信號 ....
    
    //信號處理函數
    static void handleSignal( int sig ) {
    
    }
    複製代碼
  • NSException異常是iOS庫或者各類第三方庫或Runtime驗證出錯誤而拋出的異常。如NSRangeException(數組越界異常),它們能夠被try catch捕獲(蘋果不建議用),若是未被捕獲或被@throw拋出,能夠經過註冊NSSetUncaughtExceptionHandler函數來捕獲處理。

    //註冊異常處理函數
    NSSetUncaughtExceptionHandler(&uncaught_exception_handler);
    //異常處理函數
    static void uncaught_exception_handler (NSException *exception) {
      //能夠取到 NSException 信息
      //...
      abort();
    }
    複製代碼

三、堆棧信息收集

  • 捕獲到Crash後,立刻須要收集堆棧信息;目前這些是有現成的方案支持,如PLCrashReporter、KSCrash等;
  • 甚至友盟Bugly 不只提供Crash捕獲和堆棧信息收集,還會集成分析,統計等服務,很是完善;

四、堆棧符號化

  • 目前常見的符號號手段

  • 第一種作法通常是研發本身用;第二種適用於作成標準方案,批量幫助將線上的Crash 堆棧符號還原;

五、後續

參考文章

About macOS & iOS symbol

深刻理解 Symbol

iOS Crash 捕獲及堆棧符號化思路剖析

IOS 溫習之路 」Other Linker Flags「

相關文章
相關標籤/搜索