從 Exported Symbols 應(yīng)用于包大小優(yōu)化說(shuō)到符號(hào)綁定

之前寫(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. See ld -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):

  1. 需要找到被寫(xiě)入的地址省古,也就是 __la_symbol_ptr 節(jié)中哪個(gè)位置是需要被覆蓋的粥庄。
  2. 需要找到 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市戈抄,隨后出現(xiàn)的幾起案子离唬,更是在濱河造成了極大的恐慌,老刑警劉巖划鸽,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件输莺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡裸诽,警方通過(guò)查閱死者的電腦和手機(jī)嫂用,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)崭捍,“玉大人尸折,你說(shuō)我怎么就攤上這事∫笊撸” “怎么了实夹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)粒梦。 經(jīng)常有香客問(wèn)我亮航,道長(zhǎng),這世上最難降的妖魔是什么匀们? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任缴淋,我火速辦了婚禮,結(jié)果婚禮上泄朴,老公的妹妹穿的比我還像新娘重抖。我一直安慰自己,他們只是感情好祖灰,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布钟沛。 她就那樣靜靜地躺著,像睡著了一般局扶。 火紅的嫁衣襯著肌膚如雪恨统。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天三妈,我揣著相機(jī)與錄音畜埋,去河邊找鬼。 笑死畴蒲,一個(gè)胖子當(dāng)著我的面吹牛悠鞍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播模燥,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼狞玛,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了涧窒?” 一聲冷哼從身側(cè)響起心肪,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纠吴,沒(méi)想到半個(gè)月后硬鞍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡戴已,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年固该,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糖儡。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伐坏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出握联,到底是詐尸還是另有隱情桦沉,我是刑警寧澤每瞒,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站纯露,受9級(jí)特大地震影響剿骨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜埠褪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一浓利、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钞速,春花似錦贷掖、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至遵班,卻和暖如春屠升,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狭郑。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工腹暖, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人翰萨。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓脏答,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親亩鬼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子殖告,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359