iOS中符號的那些事兒

本文介紹了iOS開發中常見的符號及堆棧符號化等內容。html

dSYM 與 DWARF

對於dSYM,iOS開發應該都比較熟悉了。ios

編譯器在編譯過程(即把源代碼轉換成機器碼)中,會生成一份對應的Debug符號表。Debug符號表是一個映射表,它把每個編譯好的二進制中的機器指令映射到生成它們的每一行源代碼中。這些Debug符號表要麼被存儲在編譯好的二進制中,要麼單獨存儲在Debug Symbol文件中(也就是dSYM文件):通常來講,debug模式構建的App會把Debug符號表存儲在編譯好的二進制中,而release模式構建的App會把Debug符號表存儲在dSYM文件中以節省二進制體積。c++

在每一次的編譯中,Debug符號表和App的二進制經過構建時的UUID相互關聯。每次構建時都會生成新的惟一標識UUID,不論源碼是否相同。僅有UUID保持一致的dSYM文件,才能用於解析其堆棧信息。git

DWARF,即 Debug With Arbitrary Record Format ,是一個標準調試信息格式,即調試信息。單獨保存下來就是dSYM文件,即 Debug Symbol File 。使用MachOView打開一個二進制文件,就能看到不少DWARF的section,如 __DWARF,__debug_str, __DWARF,__debug_info, __DWARF,__debug_names 等。github

線上的App沒有dSYM,因此對於一些線上的崩潰,須要對應正確的dSYM才能進行堆棧符號化。如 Firebase 和 Bugly 平臺都須要上傳dSYM文件才能符號化堆棧信息。shell

/xxxxxx/Pods/Crashlytics/iOS/Crashlytics.framework/upload-symbols -a 75ef2a0601e7b1071aed828d01b73ebdda95f3b9 -p ios ./MyApp.dSYM
複製代碼

其中,-a參數即指定了UUID。swift

Symbol

變量、函數都是符號。連接就是將各個mach-o文件收集並連接在一塊兒的過程,連接的過程就須要讀取符號表。而使用Xcode進行調試的時候,也會經過符號表將符號和源文件映射起來。xcode

如二進制main中用到了二進制A中的函數a,即main經過符號在A中找到該函數的實現。二進制A維護本身的符號表。使用nm工具能夠查看二進制中的符號信息。bash

struct nlist_64 存儲了符號的數據結構。而符號的name不在符號表中,而在 String Table 中,由於全部的字符串都存儲在那裏。須要根據 n_strx 找到符號的name位於 String Table 中的下標位置,才能找到正確的符號名,即字符串。數據結構

struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */ // 符號的name在String Table中的下標。
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};
複製代碼

注意這個n_strx字段,即爲符號的名字在String Table中的下標。

Symbol Table

符號表存儲了符號信息。ld和dyld都會在link的時候讀取符號表,

String Table

二進制中的全部字符串都存儲在 String Table 中。

使用strings命令能夠查看二進制中的能夠打印出來的字符串,String Table裏邊的字符串固然也在其中了。

strings - find the printable strings in a object, or other binary, file
複製代碼

Dynamic Symbol Table

動態符號表,Dynamic Symbol Table ,其中 僅存儲了符號位於Symbol Table中的下標 ,而非符號數據結構,由於符號的結構僅存儲在 Symbol Table 而已。

使用 otool 命令能夠查看動態符號表中的符號位於符號表中的下標。所以動態符號也叫作 Indirect symbols

➜  swift-hello git:(master) ✗ otool -I swift-hello.out  
swift-hello.out:
Indirect symbols for (__TEXT,__stubs) 9 entries address index 0x0000000100000eec 10 0x0000000100000ef2 11 0x0000000100000ef8 15 0x0000000100000efe 16 0x0000000100000f04 17 0x0000000100000f0a 18 0x0000000100000f10 19 0x0000000100000f16 21 0x0000000100000f1c 22 Indirect symbols for (__DATA_CONST,__got) 5 entries address index 0x0000000100001000 12 0x0000000100001008 13 0x0000000100001010 14 0x0000000100001018 20 0x0000000100001020 23 Indirect symbols for (__DATA,__la_symbol_ptr) 9 entries address index 0x0000000100002000 10 0x0000000100002008 11 0x0000000100002010 15 0x0000000100002018 16 0x0000000100002020 17 0x0000000100002028 18 0x0000000100002030 19 0x0000000100002038 21 0x0000000100002040 22 複製代碼

__la_symbol_ptr

上邊的otool命令輸出中,有 Indirect symbols for (__DATA,__la_symbol_ptr) 9 entries__la_symbol_ptr 是懶加載的符號指針,即第一次使用到的時候才加載。

section_64的結構中有個reserved字段,若該section是 __DATA,__la_symbol_ptr ,則該reserved1字段存儲的就是該 __la_symbol_ptr 在Dynamic Symbol Table中的偏移量,也能夠理解爲下標。

struct section_64 { /* for 64-bit architectures */
  char    sectname[16]; /* name of this section */
  char    segname[16];  /* segment this section goes in */
  uint64_t  addr;   /* memory address of this section */
  uint64_t  size;   /* size in bytes of this section */
  uint32_t  offset;   /* file offset of this section */
  uint32_t  align;    /* section alignment (power of 2) */
  uint32_t  reloff;   /* file offset of relocation entries */
  uint32_t  nreloc;   /* number of relocation entries */
  uint32_t  flags;    /* flags (section type and attributes)*/
  uint32_t  reserved1;  /* reserved (for offset or index) */
  uint32_t  reserved2;  /* reserved (for count or sizeof) */
  uint32_t  reserved3;  /* reserved */
};
複製代碼

查找 __la_symbol_ptr 的符號流程以下:

  1. 若是LC是 __DATA,__la_symbol_ptr ,則讀取其中的 reserved1 字段,即獲得了該 __la_symbol_ptrDynamic Symbol Table 中的起始地址或者下標。
  2. __la_symbol_ptr 進行遍歷,就獲得其中每一個symbol對應於 Dynamic Symbol Table 中的下標。即當前遍歷下標 idx + reserverd1
  3. 經過 Dynamic Symbol Table ,找到符號對應於 Symbol Table 中的下標。
  4. 經過 Symbol Table ,找到符號名對應於 String Table 中的下標(即 nlist_64 中的 n_strx 字段),即獲得符號名了。
  5. 最終,都是須要到 String Table 中,經過符號對應的下標,才能查找到符號名的。

__non_la_symbol_ptr

__non_la_symbol_ptr 也是相似的原理,非懶加載。

二進制加載的時候,對於使用到的符號,先經過一系列的關係查找到 lazy symbolnon lazy symbol ,將函數符號定位到其函數實現,兩者綁定起來的過程就是符號綁定。

符號命名規則

這裏主要參考nm的命令幫助,以及大神的博客 深刻理解Symbol

  1. C語言的符號,直接在函數名前加下劃線便可。如 myFunc 函數的符號爲 _myFunc
  2. C++支持命名空間、函數重載等,爲了不衝突,因此對符號作了Symbol Mangling操做。如 __ZN11MyNameSpace7MyClass6myFuncEd 中,***_ZN*** 是開頭部分,後邊緊接着 命名空間的長度及命名空間,類名的長度及類名,函數名的長度及函數名 ,以 E 結尾,最後則是參數類型,如i爲int,d爲double。
  3. Objective-C的符號相似於:***_OBJC_CLASS_$_MyViewController*** ,***_OBJC_CLASS_$_MyObject*** 等。
  4. Swift的符號名,有點相似於C++的規則。如函數sayHello對應的符號名爲 _s4main8sayHelloyyF*** 。以 ***_s 或者 _$ss 開頭,緊接着是 4main 表示二進制名稱?待查證。再接着的就是 8sayHello 即函數名的長度及函數名。最後的 yyF 不清楚。。。???

nm命令

nm命令用於顯示二進制的符號表。該命令有兩個版本,咱們經常使用的nm其實是 llvm-nm

nm顯示的符號表,即每一個二進制文件的 nlist 結構中的符號表。

As  of Xcode 8.0 the default nm(1) tool is llvm-nm(1).  For the most part nm(1) and llvm-nm(1) have the same options; notable exceptions include -f, -s, and -L as described below. This document explains options common between the two commands as well as some historically relevant options  supported  by  nm-classic(1). More help on options for llvm-nm(1) is provided when running it with the --help option.

nm  displays the name list (symbol table of nlist structures) of each object file in the argument list.  In some cases, as with an object that has had strip(1) with its -T option used on the object, that can be different than the dyld information.  For that information use dyldinfo(1).

If an argument is an archive, a listing for each object file in the archive will be produced.  File can be of the form libx.a(x.o), in which case only  symbols from  that  member  of  the  object  file  are listed.  (The parentheses have to be quoted to get by the shell.)  If no file is given, the symbols in a.out are listed.

Each symbol name is preceded by its value (blanks if undefined).  Unless the -m option is specified, this value is followed by one of the following characters, representing  the symbol type: U (undefined), A (absolute), T (text section symbol), D (data section symbol), B (bss section symbol), C (common symbol), - (for debugger symbol table entries; see -a below), S (symbol in a section other than those above), or I (indirect symbol).  If the symbol is  local  (non-external), the  symbol's type is instead represented by the corresponding lowercase letter.  A lower case u in a dynamic shared library indicates a undefined reference to a private external in another module in the same library.

If the symbol is a Objective-C method, the symbol name is +-[Class_name(category_name) method:name:], where `+' is for class methods, `-' is for instance methods, and (category_name) is present only when the method is in a category.
複製代碼

使用nm命令能夠查看mach-o文件的符號信息,如:

➜  codes git:(master) ✗ nm main
0000000000000000 T _main
                 U _printf
複製代碼

大小字母表示全局符號,小寫表示本地符號。這裏的U表示undefined,即未定義的外部符號。

對於Swift代碼生成的二進制文件,nm執行的輸出以下:

➜  swift-hello git:(master) ✗ nm swift-hello.out 
0000000100002050 b _$s4main4name33_9D2E62AE399B1FA0EBB6EEB3A775C624LLSSvp
0000000100000c40 t _$s4main8sayHelloyyF
                 U _$sSS19stringInterpolationSSs013DefaultStringB0V_tcfC
                 U _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
                 U _$sSSN
0000000100000e70 t _$sSSWOh
                 U _$sSSs20TextOutputStreamablesWP
                 U _$sSSs23CustomStringConvertiblesWP
                 U _$ss26DefaultStringInterpolationV06appendC0yyxs06CustomB11ConvertibleRzs20TextOutputStreamableRzlF
                 U _$ss26DefaultStringInterpolationV13appendLiteralyySSF
                 U _$ss26DefaultStringInterpolationV15literalCapacity18interpolationCountABSi_SitcfC
0000000100000e90 t _$ss26DefaultStringInterpolationVWOh
                 U _$ss27_allocateUninitializedArrayySayxG_BptBwlF
                 U _$ss5print_9separator10terminatoryypd_S2StF
0000000100000eb0 t _$ss5print_9separator10terminatoryypd_S2StFfA0_
0000000100000ed0 t _$ss5print_9separator10terminatoryypd_S2StFfA1_
                 U _$sypN
0000000100000fa0 s ___swift_reflection_version
0000000100002048 d __dyld_private
0000000100000000 T __mh_execute_header
0000000100000bf0 T _main
                 U _swift_bridgeObjectRelease
                 U _swift_bridgeObjectRetain
                 U dyld_stub_binder
複製代碼

能夠看出,相對比較複雜,但也是符合上邊講到的命名規則的。

nm -g 能夠僅查看全局符號(external symbol)。

符號的可見性

By default, Xcode just leaves every symbol in a library visible, unless it is obviously private (like static functions or inlined ones, or in Swift ones declared internal or private). But there is a setting to reverse that: 「Symbols Hidden by Default」 (Clang flag -fvisibility=hidden).

項目中的符號默認都是可見的。可使用 -fvisibility=hidden 使得符號被隱藏。也可使用clang的 attribute 來單獨設置符號的可見性,如:

//符號可被外部連接
__attribute__(( visibility("default") )) void foo( void );
//符號不會被放到Dynamic Symbol Table裏,意味着不能夠再被其餘編譯單元連接
__attribute__(( visibility("hidden") )) void bar( int x );
複製代碼

符號的weak和strong

參考自 深刻理解Symbol

版權聲明:本文爲CSDN博主「黃文臣」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連接及本聲明。 原文連接:blog.csdn.net/Hello_Hwc/a…

默認的符號是 strong symbol 的,且必須有對應實現,且符號不能重名。

weak symbol 是一種能夠不包含相應函數實現的符號,即容許符號在運行的時候不存在。strong的能夠覆蓋weak的符號。

使用場景:

  1. 依賴注入:用weak symbol提供默認實現,外部能夠提供strong symbol把實現注入進來,能夠用來作依賴注入。
  2. weak linking:用來實現版本兼容。好比一個動態庫的某些特性只有iOS 10以上支持,那麼這個符號在iOS 9上訪問的時候就是NULL的,這種狀況就能夠用就能夠用weak linking
extern void demo(void) __attribute__((weak_import));
if (demo) {
    printf("Demo is not implemented");
}else{
    printf("Demo is implemented");
}
複製代碼

Xcode中的符號斷點

符號斷點在有些調試場景下很是實用:

(lldb) breakpoint set -F "-[UIViewController viewDidAppear:]"
Breakpoint 2: where = UIKitCore`-[UIViewController viewDidAppear:], address = 0x00007fff46b03dab
複製代碼

LLDB查看符號

image lookup命令能夠在調試時查看符號相關信息:

# 查看符號的定義
image lookup -t symbol_name
# 查看符號的位置
image lookup -s symbol_name
複製代碼

符號綁定

符號綁定,就是將符號名與其實際地址綁定起來的操做,如將函數名與函數體的地址綁定起來。

看這段Swift代碼:

# swift-hello.swift

private let name = "Chris"

func sayHello() {
  print("Hello \(name)")
}

sayHello()
複製代碼

使用命令 swiftc swift-hello.swift -o swift-hello.out ,生成可執行文件爲 swift-hello.out ,查看其符號信息:

➜  swift-hello git:(master) ✗ xcrun dyldinfo -bind swift-hello.out 
bind information:
segment section          address        type    addend dylib            symbol
__DATA_CONST __got            0x100001020    pointer      0 libSystem        dyld_stub_binder
__DATA_CONST __got            0x100001000    pointer      0 libswiftCore     _$sSSN
__DATA_CONST __got            0x100001008    pointer      0 libswiftCore     _$sSSs20TextOutputStreamablesWP
__DATA_CONST __got            0x100001010    pointer      0 libswiftCore     _$sSSs23CustomStringConvertiblesWP
__DATA_CONST __got            0x100001018    pointer      0 libswiftCore     _$sypN
複製代碼

-bind參數輸出的符號都是已經bind好了的,即屬於 __DATA_CONST __got section的。裏邊的 dyld_stub_binder 就是執行bind操做的工具。

而實際上,大部分的外部符號,在第一次使用的時候纔會bind,這就是 __la_symbol_ptr 。使用參數 -lazy_bind 能夠查看。

➜  swift-hello git:(master) ✗ xcrun dyldinfo -lazy_bind swift-hello.out
lazy binding information (from lazy_bind part of dyld info):
segment section          address    index  dylib            symbol
__DATA  __la_symbol_ptr  0x100002000 0x0000 libswiftCore     _$sSS19stringInterpolationSSs013DefaultStringB0V_tcfC
__DATA  __la_symbol_ptr  0x100002008 0x003C libswiftCore     _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
__DATA  __la_symbol_ptr  0x100002010 0x0089 libswiftCore     _$ss26DefaultStringInterpolationV06appendC0yyxs06CustomB11ConvertibleRzs20TextOutputStreamableRzlF
__DATA  __la_symbol_ptr  0x100002018 0x00F2 libswiftCore     _$ss26DefaultStringInterpolationV13appendLiteralyySSF
__DATA  __la_symbol_ptr  0x100002020 0x012E libswiftCore     _$ss26DefaultStringInterpolationV15literalCapacity18interpolationCountABSi_SitcfC
__DATA  __la_symbol_ptr  0x100002028 0x0186 libswiftCore     _$ss27_allocateUninitializedArrayySayxG_BptBwlF
__DATA  __la_symbol_ptr  0x100002030 0x01BC libswiftCore     _$ss5print_9separator10terminatoryypd_S2StF
__DATA  __la_symbol_ptr  0x100002038 0x01EE libswiftCore     _swift_bridgeObjectRelease
__DATA  __la_symbol_ptr  0x100002040 0x020F libswiftCore     _swift_bridgeObjectRetain
複製代碼

能夠看到,這裏的符號所有都屬於 __DATA __la_symbol_ptr 這個section,即 lazy bind 的。

  1. __la_symbol_ptr 中的指針,會指向 __stub_helper
  2. 第一次調用該函數的時候,使用 dyld_stub_binder 把指針綁定到函數的實現。
  3. 而彙編代碼調用函數的時候,直接調用 __DATA, __la_symbol_ptr 指針指向的地址。

fishhook其實就是利用了符號綁定的原理,使用符號重綁定(rebind),將指定函數符號的實現定位到本身定義的新的函數實現,以達到hook C語言函數的目的。

連接

靜態連接器ld

ld是靜態連接器,將不少源文件編譯生成的 .o 文件,進行連接而已。

動態加載器dyld

dylib這一類動態庫使用dyld進行連接。

dlopen和dlsym 是iOS系統提供的一組API,能夠在運行時加載動態庫和動態得獲取符號,不過線上App不容許使用。

extern NSString *myDyFunc(void);
void *handle = dlopen("my.dylib", RTLD_LAZY);
NSString *(*myFunc)(void) = dlsym(RTLD_DEFAULT,"myDyFunc");
NSString *result = myFunc();
複製代碼

使用dyld來進行hook

從博客 深刻理解Symbol 中看到dyld能夠用於hook。不過iOS禁用,只能用於MacOS和模擬器。

都知道C函數hook能夠用fishhook來實現,但其實dyld內置了符號hook,像malloc history等Xcode分析工具的實現,就是經過dyld hook和malloc/free等函數實現的。這裏經過dyld來hook NSClassFromString,注意dyld hook有個優勢是被hook的函數仍然指向原始的實現,因此能夠直接調用。

做者提供的示例代碼以下:

#define DYLD_INTERPOSE(_replacement,_replacee) \ __attribute__((used)) static struct{\ const void* replacement;\ const void* replacee;\ } _interpose_##_replacee \ __attribute__ ((section ("__DATA,__interpose"))) = {\ (const void*)(unsigned long)&_replacement,\ (const void*)(unsigned long)&_replacee\ };

Class _Nullable hooked_NSClassFromString(NSString *aClassName){
    NSLog(@"hello world");
    return NSClassFromString(aClassName);
}
DYLD_INTERPOSE(hooked_NSClassFromString, NSClassFromString);
複製代碼

靜態庫與動態庫

靜態庫 *.a 文件不會被連接,而是直接使用 ar 。相似於 tar 命令。

  1. ld連接靜態庫(.a文件)的時候,只有該文件中的符號被引用到的時候,該符號纔會寫入到最終的二進制文件中,不然會被丟棄。
  2. 使用靜態庫的時候,二進制直接將靜態庫中相應的符號相關的代碼數據拷貝到二進制中,這也使得二進制體積增大。且靜態庫更新時須要從新編譯二進制。而二進制是能夠單獨運行。
  3. 使用動態庫的時候,則二進制在編譯時僅肯定動態庫中有其使用到的符號實現便可,而不會拷貝任何動態庫中的符號相關代碼數據。二進制運行的時候,還須要動態庫,即運行時調用到某個函數時,還須要去動態庫中查找函數相應的實現。動態庫更新時,不須要從新編譯二進制。

假設有另一個可執行程序 F 和可執行程序 E 一樣須要引用 foo 函數:E 和 F 都引用靜態庫 S,那麼 E 和 F 編譯完成後都會有對應的 foo 函數代碼,foo 函數代碼有兩份。 E 和 F 都引用動態庫 D,那麼 E 和 F 編譯完成後,只須要在運行時引用動態庫 D 的 foo 函數代碼便可執行,foo 函數代碼只有動態庫 D 中的一份。

參考:About macOS & iOS symbol

符號化工具及命令

關於堆棧符號化,只要注意App、UUID、dSYM對應起來便可。

  1. uuid是二進制的惟一標識,經過它找到對應的dSYM和DWARF文件
  2. dSYM包含了符號信息,其中就有DWARF。
  3. crash記錄着原始的調用堆棧信息。

符號化的過程,即在指定的二進制對應的dSYM中,根據crash中堆棧的地址信息,查找出符號信息,即調用函數便可。

dwarfdump

dwarfdump命令能夠獲取dSYM文件的uuid,也能夠進行簡單的查詢。

dwarfdump --uuid dSYM文件
dwarfdump --lookup [address] -arch arm64 dSYM文件
複製代碼

mfind

使用mfind用於在Mac系統中定位dSYM文件,如:

mdfind "com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F"
複製代碼

symbolicatecrash

使用symbolicatecrash命令,能夠將crash文件進行符號化。

首先經過命令找到symbolicatecrash,以後把symbolicatecrash單獨拷貝出來便可使用(或者建立一個軟鏈接也能夠)。

find /Applications/Xcode.app -name symbolicatecrash -type f
複製代碼

使用方式以下:

./symbolicatecrash my.crash myDSYM > symbolized.crash
複製代碼

若出現下邊錯誤,則將 export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer 加到bashrc中便可。

Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
複製代碼

若是有出現 No symbolic information found,可能跟是否開啓Bitcode有關。開啓bitcode,則Xcode會生成多個dSYM文件;若關閉bitcode,則只會產生一個。具體內容能夠查看博客 ios bitcode 機制對 dsym 調試文件的影響

有些時候,某些個別符號的dSYM文件須要單獨從其餘地方拿到,如:

0x1001f263c _hidden#1_ + 26172 (__hidden#18_:33)
複製代碼

這時候可能須要用到atos命令了。

atos

使用atos命令,能夠對單個地址進行符號化。運行shell命令 xcrun atos -o [dwarf文件地址] -arch arm64 -l [loadAddress] [instructionAddress]

xcrun atos -o app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l -l 0x1006b4000 0x0000000100d382a8
複製代碼

實際上僅經過符號在對應mach-o中的 offset 便可符號化,可假設 loadAddress 爲1,計算 instructionAddress = offset + loadAddress 。atos命令不接受直接傳遞offset地址,很奇怪。且loadAddress不能爲0。

xcrun atos -o app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l 0x1 0xF781
複製代碼

其中,0xF781即爲loadAddress爲0x1的狀況下,經過offet計算獲得的instructionAddress。

參考資料

相關文章
相關標籤/搜索