之前寫(xiě)過(guò) Xcode中和symbols有關(guān)的幾個(gè)設(shè)置之拨,天真地以為只要把和 STRIP_INSTALLED_PRODUCT
打開(kāi)量没,且選擇 STRIP_STYLE
為 All Symbols 就能讓包大小在“符號(hào)”層面達(dá)到最優(yōu)。畢竟剝離掉的是“All Symbols”啊冬耿。
直到經(jīng)人指點(diǎn)發(fā)現(xiàn)了 EXPORTED_SYMBOLS_FILE
這個(gè)配置闲擦,才發(fā)現(xiàn) Strip 能控制的并非全部符號(hào)。合理配置 Exported Symbols纷妆,可以進(jìn)一步在“符號(hào)”層面優(yōu)化二進(jìn)制文件的大小盔几。
為了便于解釋,我們首先來(lái)介紹下動(dòng)態(tài)庫(kù)的EXPORTED_SYMBOLS_FILE
配置掩幢,再發(fā)散到可執(zhí)行文件上逊拍。
EXPORTED_SYMBOLS_FILE 配置項(xiàng)是什么上鞠?
EXPORTED_SYMBOLS_FILE
是 Xcode Build Settings 中的一個(gè)配置項(xiàng)。默認(rèn)為空顺献。
Xcode 的文檔中旗国,我們可以讀到官方對(duì)這個(gè)配置項(xiàng)的解釋:
EXPORTED_SYMBOLS_FILE
This is a project-relative path to a file that lists the symbols to export. Seeld -exported_symbols_list
for details on exporting symbols.
需要在 EXPORTED_SYMBOLS_FILE
填入的是一個(gè)文件路徑,這個(gè)文件以白名單的形式列出了需要被 export 的所有符號(hào)注整。未被列入的符號(hào)將不被 export能曾。
接著去看 man ld 中對(duì) -exported_symbols_list
的解釋:
-exported_symbols_list filename
The specified filename contains a list of global symbol names that will remain as global symbols in the output file. All other global symbols will be treated as if they were marked as__private_extern__
(aka visibility=hidden) and will not be global in the output file. The symbol names listed in filename must be one per line. Leading and trailing white space are not part of the symbol name. Lines starting with # are ignored, as are lines with only white space. Some wildcards (similar to shell file matching) are supported. The * matches zero or more characters. The ? matches one character. [abc] matches one character which must be an 'a', 'b', or 'c'. [a-z] matches any single lower case letter from 'a' to 'z'.
也就是說(shuō),如果設(shè)置了 EXPORTED_SYMBOLS_FILE
配置項(xiàng)肿轨,那么不在 EXPORTED_SYMBOLS_FILE
中的符號(hào)寿冕,就會(huì)被認(rèn)為是 __private_extern__
的。
被導(dǎo)出的符號(hào)存在哪里椒袍?
通過(guò)對(duì)比可以發(fā)現(xiàn)驼唱,被導(dǎo)出的符號(hào)存在動(dòng)態(tài)庫(kù)的 Export Info 中。
這些符號(hào)是怎么被用到的驹暑?
動(dòng)態(tài)庫(kù)的“符號(hào)”是怎么被用到的玫恳?
可以想象,如果可執(zhí)行文件 A 想要調(diào)用動(dòng)態(tài)庫(kù)中的一個(gè)函數(shù) B优俘,A 在靜態(tài)的編譯鏈接完成后京办,并不知道 B 函數(shù)在內(nèi)存中的地址。B 函數(shù)的地址也是運(yùn)行時(shí)計(jì)算得到的帆焕。這個(gè)過(guò)程叫做“綁定”惭婿。
Export Info 中的信息就是必不可少的一環(huán)。
走一遍動(dòng)態(tài)庫(kù)函數(shù)調(diào)用流程
那么現(xiàn)在我們就來(lái)走一遍動(dòng)態(tài)庫(kù)中函數(shù)調(diào)用的流程叶雹。
可執(zhí)行文件 XSQExportIOSDemo 將調(diào)用動(dòng)態(tài)庫(kù) XSQFramework 中的方法 _helloFramework财饥。
1、__TEXT, __text ?? __TEXT, __stubs
可以在 XSQExportIOSDemo 形成可執(zhí)行文件后的 __TEXT, __text 段中折晦,找到 helloFramework() 這行調(diào)用對(duì)應(yīng)的匯編钥星。
bl 指令會(huì)使程序跳到 0x100006568 的地址中執(zhí)行,這個(gè)地址位于 __TEXT, __stubs 節(jié)满着。
2谦炒、__TEXT, __stubs ?? __DATA, __la_symbol_ptr
查看 0x100006568 地址中的內(nèi)容,
?nop? 為空命令
?ldr? 這一行的意思是漓滔,將當(dāng)前 pc 寄存器中的值编饺,加上 ?0x1aac,再存到 x16 寄存器中
?br? 這一行的意思是响驴,跳轉(zhuǎn)到 x16 寄存器的值指向的地址透且。
x16 中存儲(chǔ)的數(shù),將會(huì)是 0x10000656c + 0x1aac = 0x100008018。0x100008018 位于 __DATA, __la_symbol_ptr 節(jié)秽誊。
3鲸沮、__DATA, __la_symbol_ptr ?? __TEXT, __stub_helper
__la_symbol_ptr 節(jié)是一系列指針,這些指針指向的锅论,是某一個(gè)指令的地址讼溺。
0x100008018 對(duì)應(yīng)的 0x100006604,位于 __TEXT, __stub_helper 節(jié)最易。
4怒坯、__TEXT, __stub_helper ?? dyld_stub_binder
接下來(lái),指令會(huì)隨著 b 0x1000065ec 指令藻懒,跳轉(zhuǎn)到 0x1000065ec剔猿,這個(gè)地址是 __stub_helper 節(jié)的開(kāi)頭。
如果用 Hopper 查看嬉荆,會(huì)發(fā)現(xiàn)指令經(jīng)過(guò)一些準(zhǔn)備工作归敬,最終會(huì)跳轉(zhuǎn)到 dyld_stub_binder 函數(shù)。
dyld_stub_binder 就是用于動(dòng)態(tài)綁定的函數(shù)鄙早。剛才的路途中汪茧,我們看到了 __la_symbol_ptr 節(jié)。__la_symbol_ptr 映射到內(nèi)存后限番,是一個(gè)函數(shù)指針的數(shù)組舱污。符號(hào)綁定的最終目的,是將 helloFramework() 真正在內(nèi)存中的地址扳缕,寫(xiě)入到這個(gè)數(shù)組中慌闭。這樣下次 helloFramework() 函數(shù)被調(diào)用别威,就不再走綁定流程躯舔,指令將直接進(jìn)入到 helloFramework() 真正在內(nèi)存中的地址中。
那接下來(lái)有兩個(gè)關(guān)鍵點(diǎn):
- 需要找到被寫(xiě)入的地址省古,也就是 __la_symbol_ptr 節(jié)中哪個(gè)位置是需要被覆蓋的粥庄。
- 需要找到 helloFramework() 真正在內(nèi)存中的地址
接下來(lái)的步驟,我們將結(jié)合 dyld 源碼和符號(hào)斷點(diǎn)調(diào)試來(lái)推演豺妓。
dyld 是開(kāi)源的惜互,我們可以下載源碼了解其中的部分邏輯:源碼
如果我們想驗(yàn)證某個(gè)函數(shù)是否被執(zhí)行了,Xcode 的符號(hào)斷點(diǎn)可以利用起來(lái)琳拭。但這里有個(gè)小技巧:dyld 本身的函數(shù)由于執(zhí)行太過(guò)頻繁训堆,lldb 默認(rèn)不會(huì)將符號(hào)斷點(diǎn)斷在 dyld 的函數(shù)上。需要在 ~/.lldbinit 文件中加上
set set target.breakpoints-use-platform-avoid-list 0
這一行白嘁,才能用符號(hào)斷點(diǎn)來(lái)調(diào)試 dyld坑鱼。我們可以通過(guò)符號(hào)斷點(diǎn)的斷住時(shí)的調(diào)用棧信息,窺探符號(hào)綁定的過(guò)程中做了什么。
5鲁沥、進(jìn)入 dyld_stub_binder呼股,獲取參數(shù)
進(jìn)入 dyld_stub_binder 之前,有一些準(zhǔn)備工作可以分析一下画恰。
dyld_stub_binder 函數(shù)是用匯編寫(xiě)的彭谁。從注釋中可以推測(cè),它接收兩個(gè)參數(shù):
/*
* sp+0 lazy binding info offset
* sp+8 address of ImageLoader cache
*/
而在 __TEXT, __stub_helper 節(jié)中允扇,調(diào)用 dyld_stub_binder 之前缠局,有一個(gè)壓棧操作:
00000001000065f4 stp x16, x17, [sp, #-0x10]!
x16 就是 dyld_stub_binder 的參數(shù),代表 lazy binding info offset考润。
x16 的值是怎么來(lái)的呢甩鳄?我們?cè)俚雇艘徊剑瑒偛旁谔D(zhuǎn)到 __TEXT, __stub_helper 的第一行之前额划,有一個(gè)準(zhǔn)備工作:
0000000100006604 ldr w16, 0x10000660c
Ldr 這行指令的意思是妙啃,將 0x10000660c 這個(gè)地址里的值,加載到 w16 寄存器中俊戳。w16 寄存器和 x16 寄存器其實(shí)是同一個(gè)揖赴,只是使用 w16 訪問(wèn)時(shí),它是一個(gè) 32 位的數(shù)抑胎。
那么 0x10000660c 這個(gè)地址里的值是什么燥滑?它位于 __TEXT, __stub_helper 節(jié),值為 0x32阿逃。(其實(shí)就是下一行)
這個(gè) 0x32 是一個(gè)重要的參數(shù)铭拧,接下來(lái)和綁定有關(guān)的信息將從這個(gè) 0x32 中衍生出來(lái)。
6恃锉、讀取 lazy binding info
dyld_stub_binder 函數(shù)是匯編寫(xiě)的搀菩,它調(diào)用了 dyld::fastBindLazySymbol(ImageLoader** imageLoaderCache, uintptr_t lazyBindingInfoOffset) 函數(shù)。
然后按照這個(gè)調(diào)用棧破托,
調(diào)用到了
ImageLoaderMachOCompressed::doBindFastLazySymbol(uint32_t lazyBindingInfoOffset, const LinkContext& context,
void (*lock)(), void (*unlock)())
函數(shù)肪跋。
doBindFastLazySymbol 函數(shù)將讀取 MachO 文件的 lazy binding info 信息。
lazy binding info 信息在 MachO 靠近尾部的部分土砂。
這時(shí)州既,0x32 這個(gè) lazyBindingInfoOffset 這個(gè)數(shù)就要派上用場(chǎng)。
Lazy binding info 從 0x10000C2A0 開(kāi)始萝映。加上 0x32 這個(gè) offset 后吴叶,dyld 將會(huì)去讀取 0x10000C2D2 的內(nèi)容。
MachOView 已經(jīng)幫我們將這段數(shù)字翻譯了一下序臂。從中 dyld 可以讀取到的信息有:
segment(2)蚌卤、offset(24)、dylib(1)、flags(0)造寝、name(_helloFramework)
最后的 BIND_OPCODE_DONE 代表的意思是磕洪,讀到這一行后,dyld 將不再繼續(xù)往下讀诫龙。所以對(duì)于 helloFramework() 這次函數(shù)調(diào)用析显,這次綁定只會(huì)綁定 _helloFramework 一個(gè)符號(hào)。
segment(2)签赃、offset(24) 組合谷异,可以得到“需要被替換的地址”。
dylib(1)锦聊,可以得到歹嘹,dyld 應(yīng)該從哪個(gè)動(dòng)態(tài)庫(kù)里去查找 _helloFramework 這個(gè)符號(hào)。
name(_helloFramework)孔庭,可以得到符號(hào)的名字尺上。
7、找到 __DATA, __la_symbol_ptr 中圆到,“需要被替換的地址”
segment(2)怎抛、offset(24) 組合,可以得到“需要被替換的地址”芽淡。關(guān)鍵代碼為
ImageLoaderMachOCompressed::doBindFastLazySymbol
中的
uintptr_t address = segActualLoadAddress(segIndex) + segOffset;
這一行
從 segment(2) 這個(gè)信息中马绝,dyld 會(huì)去尋找第 index=2 個(gè) Load Command,也就是 LC_SEGMENT_64(__DATA),
通過(guò) VM Address 找到內(nèi)存中的 __DATA 段的起始地址(4295000064 轉(zhuǎn)換為 16 進(jìn)制為 0x100008000)挣菲。
0x100008000 再加上 offset=24富稻,就是“需要被替換的地址”。即 0x100008000 + 0x18 = 0x100008018白胀。這個(gè)地址位于 __la_symbol_ptr 節(jié)椭赋,正好回到了 helloFramework() 剛才走到過(guò)的 __la_symbol_ptr 節(jié)的位置。
8纹笼、確定去哪個(gè)動(dòng)態(tài)庫(kù)找 _helloFramework
從剛才從 lazy binding info 中讀取到的 dylib(1)纹份,可以得到苟跪,dyld 應(yīng)該從哪個(gè)動(dòng)態(tài)庫(kù)里去查找 _helloFramework 這個(gè)符號(hào)廷痘。
關(guān)鍵的代碼是
ImageLoaderMachOCompressed::resolve
中的
*targetImage = libImage((unsigned int)libraryOrdinal-1);
ImageLoader::recursiveLoadLibraries 的時(shí)候,會(huì)按照 LC_LOAD_DYLIB 的順序 setLibImage〖眩現(xiàn)在 getLibImage 能得知每個(gè)動(dòng)態(tài)庫(kù)的加載地址。
9、尋找 _helloFramework 真正對(duì)應(yīng)的地址
現(xiàn)在我們需要搬出 XSQIOSFrameworkDemo 的 MachO 文件苍鲜,這是一個(gè)動(dòng)態(tài)庫(kù)的 MachO 文件曹阔。
在最開(kāi)頭介紹 Exported Symbols 的時(shí)候,我們已經(jīng)知道,Exported Symbols 的信息枢冤,是記錄在動(dòng)態(tài)庫(kù)的 MachO 的 Export Info 中的鸠姨。
Export Info 的信息,使用了 trie 樹(shù)(前綴樹(shù))這種數(shù)據(jù)結(jié)構(gòu)做了編碼淹真。我們?cè)?dyld 的源碼中找到了和 trie 樹(shù)解析相關(guān)的函數(shù):trieWalk讶迁。給這個(gè)函數(shù)增加符號(hào)斷點(diǎn),我們得到了 dyld 通往 trie 樹(shù)解析的調(diào)用棧核蘸。
從 resolveTwolevel 這個(gè)函數(shù)開(kāi)始巍糯,這個(gè)過(guò)程最終的目的,就是從 XSQIOSFrameworkDemo 的 Image 中客扎,找到 _helloFramework 真正的地址祟峦。
其中 Export Info 中可以提供的信息是:_helloFramework 符號(hào)對(duì)應(yīng)的
Flags 00
Symbol Offset 0x7EB4
0x7EB4 這個(gè) Offset 從 MachO 角度,確實(shí)是 _helloFramework 符號(hào)對(duì)應(yīng)的指令的起始徙鱼。
10宅楞、將 _helloFramework 真正的地址寫(xiě)入 __la_symbol_ptr 中
ImageLoaderMachO::bindLocation 函數(shù)將會(huì)通過(guò) *locationToFix = newValue; 這一行,將 _helloFramework 的真正地址袱吆,寫(xiě)入 __la_symbol_ptr 對(duì)應(yīng)的函數(shù)指針數(shù)組里咱筛。
至此,綁定的過(guò)程完成杆故。如果下次繼續(xù)調(diào)用 _helloFramework迅箩,則走到 2 這一步就可以直接進(jìn)入 _helloFramework 的指令中。
不導(dǎo)出符號(hào)有什么風(fēng)險(xiǎn)处铛?
如果我們修改了 XSQIOSFrameworkDemo 的 EXPORTED_SYMBOLS_FILE饲趋,讓它不導(dǎo)出符號(hào),那么 Export Info 將空撤蟆。綁定時(shí)奕塑,dyld 會(huì)報(bào)錯(cuò),表現(xiàn)為 crash家肯,因?yàn)樗也坏?_helloFramework 的符號(hào)龄砰。
但是,一個(gè)動(dòng)態(tài)庫(kù)中讨衣,并不是所有符號(hào)都需要被其他動(dòng)態(tài)庫(kù)使用的换棚。私有的符號(hào)完全不需要在 Export Info 中體現(xiàn)。但大家寫(xiě)代碼時(shí)反镇,一般也考慮不到這些固蚤,并不會(huì)對(duì)函數(shù)、全局變量等加上 __private_extern__
歹茶。
EXPORTED_SYMBOLS_FILE
這個(gè)選項(xiàng)夕玩,可以在動(dòng)態(tài)庫(kù)打包你弦,以白名單的形式指定需要暴露的符號(hào)。起到優(yōu)化包大小燎孟、安全的作用禽作。
說(shuō)回可執(zhí)行文件
試想,如果動(dòng)態(tài)庫(kù)想要反過(guò)來(lái)調(diào)用可執(zhí)行文件的符號(hào)揩页,會(huì)怎么樣呢领迈?
可以想像,綁定的流程會(huì)和剛才類似碍沐,可執(zhí)行文件的 Export Info 也會(huì)被讀取狸捅。
之前我們給主工程使用 EXPORTED_SYMBOLS_FILE 優(yōu)化了 2MB 包大小,優(yōu)化前累提,主工程的 Export Info 包含了各種全局變量尘喝、OC 類的符號(hào)信息。在判斷這個(gè)操作是否有風(fēng)險(xiǎn)時(shí)斋陪,通過(guò)對(duì) Export Info 作用的分析朽褪,我們可以知道,這在我們的 app 中基本是沒(méi)有風(fēng)險(xiǎn)的无虚,除非有動(dòng)態(tài)庫(kù)想調(diào)用可執(zhí)行文件的符號(hào)缔赠。
“有動(dòng)態(tài)庫(kù)想調(diào)用可執(zhí)行文件的符號(hào)”,其實(shí)也有這個(gè)場(chǎng)景友题,就是自動(dòng)化測(cè)試嗤堰。所以現(xiàn)在,內(nèi)測(cè)版保留了 Export Info 中的信息度宦,正式版是不導(dǎo)出的踢匣。
參考資料
dyld 源碼
Setting breakpoint in dynamic loader on iOS simulator