前言
符號(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
的指針,我們可以通過如下的方式來獲取它的符號名:
- 遍歷load command,如果發現是
__DATA,__la_symbol_ptr
,那麼讀取reserved1,即__la_symbol_ptr
的符號位於Dynamic Symbol Table的起始地址。 - 遍歷
__DATA,__la_symbol_ptr
處的指針,當前遍歷的下標爲idx,加上reserved1就是該指針對應的Dynamic Symbol Table下標 - 通過Dynamic Symbol Table,讀取Symbol Table的下標
- 讀取Symbol Table,找到String Table的Index
- 找到符號名稱
一張圖回顧整個過程,可以看到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.dsym
crash堆棧還可以直接通過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 _main
U _mylog
C++因爲支持命名空間,函數重載等高級特性,爲了避免符號衝突,所以編譯器對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_$_BTDRouteBuilder
U _OBJC_CLASS_$_BTDRouter
U _OBJC_CLASS_$_UIViewController
0000000000000458 S _OBJC_CLASS_$_ViewController
U _OBJC_METACLASS_$_NSObject
U _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_DONE
Lazy 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,即允許符號運行時不存在
➜ Desktop xcrun dyldinfo -bind /Demo.app/Demo
bind information:
segment section address type addend dylib symbol
__DATA __got 0x100003010 pointer 0 Dynamic _demo (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) breakpoint set -F "-[UIViewController viewDidAppear:]"
Breakpoint 2: where = UIKitCore`-[UIViewController viewDidAppear:], address = 0x00007fff46b03dab
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或者模擬器。
總結
平時寫代碼的時候符號應用的場景並不多,但瞭解符號、符號表等概念,有助於理解問題的本質,也能夠在做程序架構的時候多一些思路。