本文介紹了iOS開發中常見的符號及堆棧符號化等內容。html
對於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
變量、函數都是符號。連接就是將各個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中的下標。
符號表存儲了符號信息。ld和dyld都會在link的時候讀取符號表,
二進制中的全部字符串都存儲在 String Table 中。
使用strings命令能夠查看二進制中的能夠打印出來的字符串,String Table裏邊的字符串固然也在其中了。
strings - find the printable strings in a object, or other binary, file
複製代碼
動態符號表,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 複製代碼
上邊的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 的符號流程以下:
__non_la_symbol_ptr 也是相似的原理,非懶加載。
二進制加載的時候,對於使用到的符號,先經過一系列的關係查找到 lazy symbol 和 non lazy symbol ,將函數符號定位到其函數實現,兩者綁定起來的過程就是符號綁定。
這裏主要參考nm的命令幫助,以及大神的博客 深刻理解Symbol。
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 );
複製代碼
參考自 深刻理解Symbol。
版權聲明:本文爲CSDN博主「黃文臣」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連接及本聲明。 原文連接:blog.csdn.net/Hello_Hwc/a…
默認的符號是 strong symbol 的,且必須有對應實現,且符號不能重名。
而 weak symbol 是一種能夠不包含相應函數實現的符號,即容許符號在運行的時候不存在。strong的能夠覆蓋weak的符號。
使用場景:
extern void demo(void) __attribute__((weak_import));
if (demo) {
printf("Demo is not implemented");
}else{
printf("Demo is implemented");
}
複製代碼
符號斷點在有些調試場景下很是實用:
(lldb) breakpoint set -F "-[UIViewController viewDidAppear:]"
Breakpoint 2: where = UIKitCore`-[UIViewController viewDidAppear:], address = 0x00007fff46b03dab
複製代碼
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 的。
fishhook其實就是利用了符號綁定的原理,使用符號重綁定(rebind),將指定函數符號的實現定位到本身定義的新的函數實現,以達到hook C語言函數的目的。
ld是靜態連接器,將不少源文件編譯生成的 .o 文件,進行連接而已。
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();
複製代碼
從博客 深刻理解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 命令。
假設有另一個可執行程序 F 和可執行程序 E 一樣須要引用 foo 函數:E 和 F 都引用靜態庫 S,那麼 E 和 F 編譯完成後都會有對應的 foo 函數代碼,foo 函數代碼有兩份。 E 和 F 都引用動態庫 D,那麼 E 和 F 編譯完成後,只須要在運行時引用動態庫 D 的 foo 函數代碼便可執行,foo 函數代碼只有動態庫 D 中的一份。
關於堆棧符號化,只要注意App、UUID、dSYM對應起來便可。
符號化的過程,即在指定的二進制對應的dSYM中,根據crash中堆棧的地址信息,查找出符號信息,即調用函數便可。
dwarfdump命令能夠獲取dSYM文件的uuid,也能夠進行簡單的查詢。
dwarfdump --uuid dSYM文件
dwarfdump --lookup [address] -arch arm64 dSYM文件
複製代碼
使用mfind用於在Mac系統中定位dSYM文件,如:
mdfind "com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F"
複製代碼
使用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命令,能夠對單個地址進行符號化。運行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。