深入理解Symbol
前言
符號(Symbol)是日常開發中經常接觸的一個概念,雖然日常開發中直接應用的場景比較少,但符號編譯期和運行時都扮演了重要的角色。
符號是什么
維基百科的定義
A symbol in computer programming is a primitive data type whose instances have a unique human-readable form.
直觀理解,符號是一個數據結構,包含了名稱(String)和類型等元數據,符號對應一個函數或者數據的地址。
Symbol Table
符號表存儲了當前文件的符號信息,靜態鏈接器(ld)和動態鏈接器(dyld)在鏈接的過程中都會讀取符號表,另外調試器也會用符號表來把符號映射到源文件。
如果把調試符號裁剪掉(Deployment Postprocessing選擇為YES),那么文件里的斷點會失效:
Release模式下是可以裁剪掉符號的,因為release模式下默認有dsym文件,調試器仍然可以從中獲取到信息正常工作。
符號表中存儲符號的數據結構如下:
struct nlist_64 {union {uint32_t n_strx; /* index into the 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) */ };字符串存儲在String Table里,String Table的格式很簡單,就是一個個字符串拼接而成。符號的n_strx字段存儲了符號的名字在String Table的下標。
Dynamic Symbol Table
Dynamic Symbol Table是動態鏈接器(dyld)需要的符號表,是符號表的子集,對應的數據結構很簡單,只存儲了符號位于Symbol Table的下標:
? otool -I main main: ... Indirect symbols for (__DATA,__la_symbol_ptr) 1 entries address index 0x000000010000c000 4 //對應符號表的idx為4的符號 ....感興趣的同學可能會問,既然Dynamic Symbol Table只存儲了下標,這里otool是如何知道這個Indirect symbol屬于__DATA,__la_symbol_ptr?
答案是用section_64的reserved字段:如果一個section是__DATA,__la_symbol_ptr,那么它的reserved1字段會存儲一個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的指針,我們可以通過如下的方式來獲取它的符號名:
一張圖回顧整個過程,可以看到MachO中各種下標的利用很巧妙:
fishhook就是利用類似的原理,遍歷__la_symbol_ptr,比較指針背后的函數符號名稱,如果只指定的字符串,就替換指針的指向。
DWARF vs DSYM
DWARF(debugging with attributed record formats)是一種調試信息的存儲格式,用在Object File里,用來支持源代碼級別的調試。
用Xcode編譯的中間產物ViewController.o,用MachOView打開后,可以看到很多DWARF的section:
打包上線的時候會把調試符號等裁剪掉,但是線上統計到的堆棧我們仍然要能夠知道對應的源代碼,這時候就需要把符號寫到另外一個單獨的文件里,這個文件就是DSYM。
可以通過命令dwarfdump來查詢dsym文件的內容,比如查找一個地址
dwarfdump --lookup 0x0007434d -arch arm64 DemoApp.app.dsymcrash堆棧還可以直接通過Xcode內置的命令來反符號化
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" alias symbolicatecrash='/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash' symbolicatecrash demo.crash DemoApp.app.dsym > result.crash裁剪
符號包含的信息太多,處于安全考慮,往往會進行最高級別的裁剪。對于.app,選擇裁掉All Symbol,而動態庫只能選擇Non-Global Symbol,因為動態庫需要把Global Symbol保留給外部鏈接用。
背后裁減的實際命令是strip,比如裁減local符號的指令是strip -x
符號生成規則
C的符號生成規則比較簡單,一般的符號都是在函數名上加上下劃線,比如main.c里包含mian和mylog兩個C函數,對應符號如下:
? nm main.o 0000000000000000 T _mainU _mylogC++因為支持命名空間,函數重載等高級特性,為了避免符號沖突,所以編譯器對C++符號做了Symbol Mangling(不同編譯器的規則不一樣)。
舉個例子:
namespace MyNameSpace {class MyClass{public:static int myFunc(int);static double myFunc(double);}; }編譯后,分別對應符號
? DemoApp nm DemoCpp.o 0000000000000008 T __ZN11MyNameSpace7MyClass6myFuncEd 0000000000000000 T __ZN11MyNameSpace7MyClass6myFuncEi其實,Symbol Mangling規則并不難,剛剛的兩個符號是按照如下規則生成的:
- 以_Z開頭
- 跟著C語言的保留字符串N
- 對于namespace等嵌套的名稱,接下依次拼接名稱長度,名稱
- 然后是結束字符E
- 最后是參數的類型,比如int是i,double是d
Objective C的符號更簡單一些,比如方法的符號是+-[Class_name(category_name) method:name:],除了這些,Objective C還會生成一些Runtime元數據的符號
? DemoApp nm ViewController-arm64.o U _OBJC_CLASS_$_BTDRouteBuilderU _OBJC_CLASS_$_BTDRouterU _OBJC_CLASS_$_UIViewController 0000000000000458 S _OBJC_CLASS_$_ViewControllerU _OBJC_METACLASS_$_NSObjectU _OBJC_METACLASS_$_UIViewController 0000000000000480 S _OBJC_METACLASS_$_ViewController所以當鏈接的時候類找不到了,會報錯符號_OBJC_CLASS_$_CLASSNAME找不到
當然,如果類的符號沒有被裁減掉,運行時就用_OBJC_CLASS_$_CLASSNAME作為參數,通過dlsym來獲取類指針。
符號的種類
按照不同的方式可以對符號進行不同的分類,比如按照可見性劃分
- 全局符號(Global Symbol) 對其他編譯單元可見
- 本地符號(Local Symbol) 只對當前編譯單元可見
按照位置劃分:
- 外部符號,符號不在當前文件,需要ld或者dyld在鏈接的時候解決
- 非外部符號,即當前文件內的符號
nm命令里的小寫字母對應著本地符號,大寫字母表示全局符號;U表示undefined,即未定義的外部符號
可見性
有個很常見的case,就是你有1000個函數,但只有10個函數是公開的,希望最后生成的動態庫里不包含其他990個函數的符號,這時候就可以用clang的attribute來實現:
//符號可被外部鏈接 __attribute__((visibility("default"))) //符號不會被放到Dynamic Symbol Table里,意味著不可以再被其他編譯單元鏈接 __attribute__((visibility("hidden")))clang來提供了一個全局的開關,用來設置符號的默認可見性:
如果動態庫的Target把這個開關打開,會發現動態庫仍然能編譯通過,但是App會報一堆鏈接錯誤,因為符號變成了hidden。
但這是一種常見的編譯方式:讓符號默認是Hidden的,即-fvisibility=hidden,然后手動為每個接口加上__attribute__((visibility("default")))。
//頭文件 #define AWE_EXPORT __attribute__((visibility("default"))) AWE_EXPORT void method_1(void);//實現文件 AWE_EXPORT void method_1(){NSLog(@"1"); }ld
剛剛提到了,鏈接的時候ld會解決重定位符號的問題,所以ld提供了很多與符號相關的選項。
-ObjC, -all_load, -force_load
ld鏈接靜態庫的時候,只有.a中的某個.o符號被引用的時候,這個.o才會被鏈接器寫到最后的二進制文件里,否則會被丟掉,這三個鏈接選項都是解決保留代碼的問題。
- -ObjC 保留所有Objective C的代碼
- -force_load 保留某一個靜態庫的全部代碼
- -all_load 保留參與鏈接的全部的靜態庫代碼
這就是為什么一些SDK在集成進來的時候,都要求在other link flags里添加-ObjC。
reexport
假設我有個動態庫A,A會鏈接B,我希望其他鏈接A動態庫也能直接訪問到B的符號,從而隱藏B的實現,應該怎么做呢?
答案就是:reexport。
這點在libSystem上體現的尤為明顯,libSystem.dylib reexport了像malloc,dyld,macho等更底層動態庫的符號。
exported_symbol
動態庫因為不知道外面是如何使用的,所以最好的方式是所有頭文件暴露出的符號全部導出來。從包大小的角度考慮,肯定是用到哪些符號,保留哪些符號對應的代碼,ld提供了這樣一個方案,通過exported_symbol來只保留特定的符號。
tbd
鏈接的過程中,只要知道哪個動態庫包括哪些符號即可,其實不需要一個完整的動態庫Mach-O。于是Xcode 7開始引入了tbd的概念,即Text Based Stub Library,里面包含了動態庫對外提供的符號,能大幅度減少Xcode的下載大小。
可以在以下目錄下找到tbd文件,文件格式就是普通的文本文件:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks以Account.framework為例,在內部可以找到Account.tbd:
除了包括一些基本信息,如架構,uuid,類,符號等,還有個信息是install-name,這個字段存儲了告訴鏈接器,動態庫在運行時位于系統的位置。
另外,Xcode里還提供了TBD相關的編譯選項:
flat_namespace
ld默認采用二級命名空間,也就是除了會記錄符號名稱,還會記錄符號屬于哪個動態庫的,比如會記錄下來printf來自libSystem
? xcrun dyldinfo -lazy_bind main segment section address index dylib symbol __DATA __la_symbol_ptr 0x10000C000 0x0000 libSystem _printf可以強制讓ld使用flat_namespace,使用一級命名空間,就是只記錄下來符號的名稱,運行時的時候dyld動態查找符號所處的位置。
flat_namespace容易發生符號沖突,比如運行時兩個動態庫有一樣的符號;另外效率也要比二級命名空間低一些。
但flat_namespace可以實現動態庫依賴主二進制這種野路子。
運行時
bind
應用會訪問很多外部的符號,編譯的時候是不知道這些符號的運行時地址的,所以需要在運行時綁定。
? xcrun dyldinfo -bind main bind information: segment section address type addend dylib symbol __DATA_CONST __got 0x100008000 pointer 0 libSystem dyld_stub_binder啟動的時候,dyld會讀取LINKEDIT中的opcode做綁定:
? xcrun dyldinfo -opcodes main binding opcodes: 0x0000 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM(1) 0x0001 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM(0x00, dyld_stub_binder) 0x0013 BIND_OPCODE_SET_TYPE_IMM(1) 0x0014 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB(0x02, 0x00000000) 0x0016 BIND_OPCODE_DO_BIND() 0x0017 BIND_OPCODE_DONELazy Symbol
多數符號在應用的生命周期內是用不到的,于是ld會盡可能的讓符號lazy_bind,即第一次訪問的時候才會綁定。比如log.c里面調用的printf就是lazy符號。
? xcrun dyldinfo -lazy_bind main lazy binding information (from lazy_bind part of dyld info): segment section address index dylib symbol __DATA __la_symbol_ptr 0x10000C000 0x0000 libSystem _printf為了支持lazy_bind,首先會在__DATA, __la_symbol_ptr創建一個指針,這個指針編譯期會指向__TEXT,__stub_helper,第一次調用的時候,會通過dyld_stub_binder把指針綁定到函數實現,下一次調用的時候就不需要再綁定了。
而匯編代碼調用printf的時候,直接是調用__DATA, __la_symbol_ptr指針指向的地址。
Weak Symbol
默認情況下Symbol是strong的,weak symbol在鏈接的時候行為比較特殊:
- strong symbol必須有實現,否則會報錯
- 不可以存在兩個名稱一樣的strong symbol
- strong symbol可以覆蓋weak symbol的實現
應用場景:用weak symbol提供默認實現,外部可以提供strong symbol把實現注入進來,可以用來做依賴注入。
此外還有個概念叫weak linking,這個在做版本兼容的時候很有用:比如一個動態庫的某些特性只有iOS 10以上支持,那么這個符號在iOS 9上訪問的時候就是NULL的,這種情況就可以用就可以用weak linking。
可以針對單個符號,符號引用加上weak_import即可
extern void demo(void) __attribute__((weak_import)); if (demo) {printf("Demo is not implemented"); }else{printf("Demo is implemented"); }實際開發中,更多的場景是整個動態庫都被弱鏈接,對應Xcode中的optional framework:
設置成optional后,鏈接的命令會變成-weak_framework Dynamic,對應在dyld bind的時候,符號也會標記為weak import,即允許符號運行時不存在
dlsym & dlopen
dlopen/dlsym是底層提供一組API,可以在運行時加載動態庫和動態的獲取符號:
extern NSString * effect_sdk_version(void);加載動態庫并調用C方法
void *handle = dlopen("path to framework", RTLD_LAZY); NSString *(*func)(void) = dlsym(RTLD_DEFAULT,"effect_sdk_version"); NSString * text = func();符號斷點
可以在指定的符號上打斷點
Xcode的GUI能設置的斷點,都可以用lldb的命令行設置
lldb
運行時,還可以用lldb去查詢符號相關的信息,常見的case有兩個
查看某個符號的定義
(lldb) image lookup -t ViewController 1 match found in /Users/huangwenchen/.../SymbolDemo.app/SymbolDemo: id = {0xffffffff00046811}, name = "ViewController", byte-size = 8, decl = ViewController.h:11, compiler_type = "@interface ViewController : UIViewController @end"查看符號的位置
(lldb) image lookup -s ViewController 2 symbols match 'ViewController' in /Users/huangwenchen/.../SymbolDemo.app/SymbolDemo:Address: SymbolDemo[0x0000000100005358] (SymbolDemo.__DATA.__objc_data + 0)Summary: (void *)0x000000010e74c380: ViewController Address: SymbolDemo[0x0000000100005380] (SymbolDemo.__DATA.__objc_data + 40)Summary: (void *)0x00007fff89aec158: NSObject基于dyld的hook
都知道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);但iOS上被禁用了,只能用于MacOS或者模擬器。
總結
平時寫代碼的時候符號應用的場景并不多,但了解符號、符號表等概念,有助于理解問題的本質,也能夠在做程序架構的時候多一些思路。
總結
以上是生活随笔為你收集整理的深入理解Symbol的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java正则表达式控制半角字符串输入
- 下一篇: Excel正确输入身份证号码