got 是什么
iOS 開發(fā)中垦藏,動態(tài)庫是個繞不開的話題,系統(tǒng)庫基本上是動態(tài)庫轰驳。它的一大優(yōu)勢是節(jié)約內(nèi)存弟灼,可讓多個程序映射同一份的動態(tài)庫,實現(xiàn)代碼共享田绑。動態(tài)庫本身也是一個 Mach-O
文件,也有數(shù)據(jù)段芒划、代碼段等欧穴。其中代碼段可讀可執(zhí)行,數(shù)據(jù)段可讀可寫涮帘。
動態(tài)庫共享的只是代碼段部分,為了達(dá)到代碼段共享的目的疮鲫,其符號地址在生成時就不能寫死,因為它映射到每個程序中虛擬內(nèi)存空間中的位置可能不一樣棚点。對于數(shù)據(jù)段部分,由于各個程序會對其進行修改瘫析,因此每個程序會單獨映射一份。
那么如何解決代碼段共享的問題呢咸包?聰明的人們杖虾,想出一種精妙的解決方式。通過添加一個中間層
奇适,到另一個表中去查找符號的地址。這個表就叫 got
嚷往,global offset table
,全局符號偏移表籍琳,然后在運行時綁定地址信息贷祈,將地址填入到 got
中。這樣代碼段中的符號就與具體地址無關(guān)势誊,只和 got
有關(guān)。這種方式就叫 PIC
闻丑,Program Independent Code
勋颖,程序地址無關(guān)代碼。
或許你可能會想到饭玲,got
中保存的是符號地址,而每個程序的地址是不一樣的矮冬,那 got
肯定是不能共享的。沒錯胎署,所以 got
會保存在數(shù)據(jù)段中,每個程序單獨一份恢筝。在進行符號綁定時巨坊,更新 got
中對應(yīng)符號的地址即可。
got 的位置
在了解 got
是什么之后侄柔,我們再來看看 Mach-O
中 got
到底放在了哪里。
通過下圖可以看出暂题,有個專門的 __got section
存放 got
數(shù)據(jù)妈候,而它是屬于 __DATA segment
。
對于 segment
和 section
,可能大家會有些困惑赶站。下面來簡單解釋一下。
section
section
稱為節(jié)贝椿,是編譯器對 .o
內(nèi)容的劃分,將同類資源在邏輯上劃分到一起瑟蜈。常見的 section
有:
存放代碼指令渣窜,
.text
存放已初始化全局變量,
.data
存放未初始化的全局變量和靜態(tài)局部變量乔宿,
.bss
符號表,
.symtab
字符串表掂林,
.strtab
segment
segment
稱為段,它是權(quán)限屬性相同 section
的集合精置。
在程序裝載時,操作系統(tǒng)并不關(guān)心 section
的數(shù)量和內(nèi)容脂倦,只對其權(quán)限敏感蹲堂,因此沒必要一個個加載 section
,只需將權(quán)限相同的 section
合到一起加載即可政供。
另外,這樣還可節(jié)省內(nèi)存布隔。由于內(nèi)存按頁分配稼虎,即使不滿一個頁也得分配一整頁。若單個 section
大小非系統(tǒng)頁長度的整數(shù)倍哀军,會造成內(nèi)存碎片。而將其合并后杉适,會有效緩解這種情況柳击。
舉個栗子, .text
和 .init
的權(quán)限都是只讀可執(zhí)行,.init
是程序初始化代碼蹬叭。
假設(shè)頁的大小是 4 KB
,.text
大小為 4098
字節(jié)秽五,.init
大小為 900
字節(jié)试幽。如下圖所示卦碾,若將它們單獨映射起宽,.text
會占用 2
個頁,.init
占用 1
個頁绿映,整體占用 3
個頁腐晾。
如果它們合并成代碼段,那么只需占用 2
個頁淹冰,減少內(nèi)存浪費。如下圖所示樱拴。
可執(zhí)行文件是由多個 .o
文件鏈接而成的洋满,每個 .o
文件有各自的 section
。因此鏈接器將所有 .o
文件中權(quán)限相同的 section
合并到一起正罢,形成 segment
驻民。操作系統(tǒng)只需將 segment
映射到虛擬內(nèi)存空間即可。
平常我們所說的代碼段回还、數(shù)據(jù)段,便是指鏈接后的 segment
鞭盟。
動態(tài)庫符號類型
動態(tài)庫中的符號分為 non-lazy symbol
和 lazy symbol
。
non-lazy symbol
,是指在啟動時就必須鏈接的符號本刽,確定好符號地址候味。lazy symbol
洪灯,顧名思義,延遲綁定符號,只在使用時才進行鏈接坏快。
為啥要分為兩種類型呢憎夷?我們試想一下,如果所有動態(tài)庫的符號都是啟動時鏈接拾给,一個程序隨隨便便依賴的系統(tǒng)動態(tài)庫就有大幾十個。每個動態(tài)庫中符號還不少级及,并且也不是所有符號都會用到额衙,這樣勢必會拖慢啟動速度。所以采用延遲綁定技術(shù)入偷,只需在第一次用到時進行綁定,可提高性能殿雪。而數(shù)據(jù)符號相對較少锋爪,則可以采用 non-lazy
的方式,放到啟動時就鏈接其骄。
因此,Mach-O
中劃分了兩個 section
來保存 non-lazy symbol
和 lazy symbol
索抓。其中 __got
中保存的是 non-lazy symbol
,__la_symbol_ptr
保存的是 lazy symbol
逼肯。
下面桃煎,我們來實踐一下,驗證上述說法的正確性三椿。請將以下文件放在同一個目錄下缺菌。
print.c:
#include <stdio.h>
char *global = "hello";
void print(char *str)
{
printf("%s\n", str);
}
main.c:
void print(char *str);
extern char *global;
int main()
{
print(global);
return 0;
}
run.sh:
// 生成 main.o伴郁,目標(biāo)版本 14.0
xcrun -sdk iphoneos clang -c main.c -o main.o -target arm64-apple-ios14.0
// 生成 libPrint.dylib 動態(tài)庫
xcrun -sdk iphoneos clang -fPIC -shared print.c -o libPrint.dylib -target arm64-apple-ios14.0
// 鏈接生成可執(zhí)行文件纽乱,"-L .", 表示在當(dāng)前目錄中查找。"-l Print"租冠,鏈接 libPrint.dylib 動態(tài)庫
xcrun -sdk iphoneos clang main.o -o main -L . -l Print -target arm64-apple-ios14.0
給 run.sh
添加可執(zhí)行權(quán)限后再運行,生成可執(zhí)行文件顽爹。
chmod +x run.sh
./run.sh
執(zhí)行完畢后骆姐,在目錄中會生成 libPrint.dylib
動態(tài)庫和 main
可執(zhí)行文件。
將 main
拖到 MachOView
中肉渴,如下圖所示:
右邊紅框中的 _global
就是動態(tài)庫 libPrint.dylib
中的符號带射。它被放到了 __got
中,并且其初始地址為 0窟社。它是表的第一項,表地址是 0x10008000
关炼,那么 0x10008000
中的值就是符號地址匣吊。
另外,我們還發(fā)現(xiàn)色鸳,在 __got
中還有一條記錄 dyld_stub_binder
,初始地址也是 0。它是表的第二項池户,也就是 0x10008008
地址中的值為符號地址凡怎。稍后會講它的作用赊抖。
_global
在啟動時會進行鏈接,那么如何知道需要鏈接哪個動態(tài)庫呢氛雪?我們點開 Symbol Table
,會看到如下信息:
可見浴鸿,符號表中已經(jīng)包含了 _global
所屬動態(tài)庫的信息弦追,libPrint.dylib
。同樣 dyld_stub_binder
劲件,它在 libSystem.B.dylib
中。
雖然動態(tài)庫中的符號苗分,在生成可執(zhí)行文件時牵辣,沒有進行鏈接,但是在符號表中記錄了它在哪個動態(tài)庫中服猪。這樣在運行時進行鏈接,才能到相應(yīng)動態(tài)庫中找到罢猪。
dyld_stub_binder
在上節(jié)中,我們遇到了 dyld_stub_binder
這個陌生人粘捎。從字面意思危彩,我們大致可以猜到,它是用來做符號綁定用的汤徽。前面提到過,函數(shù)符號都是在第一次使用時才進行綁定拼坎,其實是通過 dyld_stub_binder
來進行符號查找與地址重定位浮毯。鑒于它肩負(fù)重大使命债蓝,因此必須預(yù)先綁定好地址,所以會放到 __got
中饰迹。
dyld_stub_binder
是用匯編實現(xiàn)的余舶,在 dyld_stub_binder.s
中。它的調(diào)用鏈路如下:
// 匯編中調(diào)用 fastBindLazySymbol
1. dyld::fastBindLazySymbol
// 調(diào)用 ImageLoader 處理
2. ImageLoaderMachOCompressed::doBindFastLazySymbol
// 符號綁定
3. ImageLoaderMachOCompressed::bindAt
// 符號地址解析
4. ImageLoaderMachOCompressed::resolve
// 符號地址更新
5. ImageLoaderMachO::bindLocation
其中 resolve
是解析符號地址莉掂,bindLocation
進行符號地址更新。
lazy 符號重定位
上面我們說到憎妙,函數(shù)符號的重定位是通過 dyld_stub_binder
來做的曲楚,那么有沒有依據(jù)可尋呢?當(dāng)然有啦龙誊。
從下圖可以看出,_print
的地址是 0x100007FAC
鹤树,不是說在第一次調(diào)用時才綁定地址嗎?為什么該函數(shù)的地址會有值呢罕伯?沒錯叽讳,但它需要有人幫忙來進行地址重定位,這個幫手就是 0x100007FAC
處的神秘嘉賓岛蚤。
這個地址處在 __TEXT
段范圍,通過查看 __TEXT
段各個 section
的地址范圍单雾,我們很容易發(fā)現(xiàn)它處在 __stub_helper
中。如下圖所示:
請注意看圖上的 1蜂奸、2、3 標(biāo)號。地址 0x100007FAC
處于 1 號围详。它對應(yīng)的匯編代碼功能是:
取出
0x100007fb4
處的值放入 w16,也就是將 w16 清 0买羞。b 是無返回跳轉(zhuǎn)指令,跳轉(zhuǎn)到
0x100007f94
畜普,也就是開頭 2 號處群叶。
然后,從 2 號處開始執(zhí)行街立,一直到 3 號位置。3 號區(qū)域的功能是:
第一行是相對地址偏移取值指令赎离。在距離當(dāng)前行地址
0x10007FA4
偏移0x64
的地方取出值,放入 x16虽画。也就是取出0x10007FA4 + 0x64 = 0x10008008
處的內(nèi)容荣病。br x16
,進行函數(shù)調(diào)用众雷,跳轉(zhuǎn)到 x16 中的地址。
所以砾省,最主要是得弄清楚 0x10008008
地址里面的內(nèi)容是啥,根據(jù) br
指令推斷轩性,它肯定是個函數(shù)地址狠鸳。
有沒有覺得 0x10008008
有些熟悉呢悯嗓?再看看下面這張圖卸察,其實在第一節(jié)的圖中我們已經(jīng)看到過它。got
中第二項的地址就是 0x10008008
坑质,而它正好存儲的是 dyld_stub_binder
地址。
這樣稼跳,一切都清楚了吃沪。
函數(shù)符號的地址綁定會調(diào)用到
dyld_stub_binder
。通過它獲取到地址后票彪,再更新下圖中紅框處的值為函數(shù)的真正地址。
以后就不用走
dyld_stub_binder
地址綁定的流程了锉屈,直接跳轉(zhuǎn)到函數(shù)地址去執(zhí)行。
got 符號值查找
查找原理
變量和函數(shù)統(tǒng)稱為符號颈渊,所有符號信息都在符號表 Symbol Table
中终佛,符號值在字符串表 String Table
中。符號表只是記錄了它在字符串表中的下標(biāo)铃彰,因為這樣可以節(jié)省空間。
而我們上文中提到的 global
是個外部全局變量竹揍,那么它存在了符號表中的哪里?可以通過何種路徑找到它呢芬位?下面來探尋一下带到。
首先讓我們回到 Mach-O
的 Load Commands
中。它里面有一系列的加載命令,告訴系統(tǒng)如何加載不同的 segment
四康。加載命令中包含了 Section Header
的數(shù)組狭握,header
里面包含了每個 section
的基礎(chǔ)信息,比如節(jié)名稱论颅、所屬 segment
的名稱、地址嗅辣、大小澡谭、偏移、保留字段等等蛙奖。
既然 __got
是一個 section
杆兵,那么肯定也有對應(yīng)的頭信息。從下圖可以看到琐脏,在 LG_SEGMENT_64(__DATA_CONST)
中,包含了 __got
的 header
日裙。
注意右邊紅框中 Indirect Sym Indx
部分吹艇,它表示了 __got
中的第一個符號
在間接表中的下標(biāo)受神,間接表其實就是動態(tài)庫符號表格侯。如果 __got
中有多個符號,那么下標(biāo)依次 +1
即可联四。
舉個栗子,假設(shè) __got
第一個符號在間接表中的下標(biāo)是 x
碎连,那么第二個符號的下標(biāo)為 x+1
,第三個為 x+2
廉嚼,以此類推。如下圖所示:
而間接表中的內(nèi)容是該符號在符號表的下標(biāo)怠噪,取出內(nèi)容,然后到符號表中查找矫夷,便可找到符號信息。到這里還沒完双藕,由于符號值并不是直接存在符號表中阳仔,而是在字符串表。最后拿字符串下標(biāo)到字符串表中查找近范。
這里有點繞,流程如下:
1. 通過 __got section header评矩,拿到 indirectSymIndex。
2. 拿 indirectSymIndex 到間接表中(indirect symbol table)取到符號表中的下標(biāo) symIndex虱颗。
3. 拿 symIndex 到符號表中取到最終的符號信息,這里有它在字符串表中的下標(biāo) strIndex上枕。
4. 拿 strIndex 到字符串表中取到符號字符字符串弱恒。
整體圖示如下(注:符號表中僅畫出了下標(biāo),省略了其他信息):
實踐驗證
光說不練假把式锈玉,下面我們來驗證一下。
__got section header
中在間接符號表的下標(biāo)為 1拉背,也就是說第一個符號下標(biāo)為 1。從上文圖中可以看到椅棺,__got
中總共有 2
個符號,分別為 _global
和 dyld_stub_bind
两疚。如果找到的符號為 _global
,那么表示上述結(jié)論是正確的诱渤。
此時 __got section header
的數(shù)據(jù)如下圖所示,indirect sym index = 1
:
那我們到 dynamic symbol table
中去瞧一瞧递胧,找到下標(biāo)為 1 的數(shù)據(jù)信息,即第二個數(shù)據(jù)缎脾。如下所示:
從上圖可以看出占卧,在對應(yīng)的 Data
一列中,內(nèi)容為 3屉栓,表示它在符號表中的下標(biāo)為 3友多。
此時 indirect symbol table
中的數(shù)據(jù)如下所示:
然后繼續(xù)到符號表中看看下標(biāo)為 3 的數(shù)據(jù)是啥堤框。如下圖所示:
第四項數(shù)據(jù) String Table Index
,它的值是 0x1c
蜈抓,轉(zhuǎn)換為十進制為 28
,這就是字符串表中的下標(biāo)委可。
此時符號表中的數(shù)據(jù)如下所示:
最后一步,來到字符串表中着倾⊙嗌伲看看下標(biāo)為 28
的內(nèi)容是什么?一行是 16 字節(jié)客们,第二行倒數(shù)第四個數(shù)就是符號開始處(不放心的可以自己數(shù)一數(shù)??)材诽。
其中恒傻,5F
是 _
的 ascii
碼,67
是 g
的 ascii
碼碌冶,...,一直到 .
號為止譬重。正好對應(yīng)的是 _global
,也就證明了查找過程的正確性臀规。
此時字符串表數(shù)據(jù)如下:
那對于第二個符號 dyld_stub_binder
栅隐,你是否可以自行實踐出來呢?
其實租悄,以上查找不僅限于 __got
中的符號,對于延遲加載符號一樣適用泣棋。下圖中 __la_symbol
同樣也有 Indirect Sym Index
。動態(tài)庫中的符號都是這種查找方式潭辈。
總結(jié)
這篇文章中,我們介紹了什么是 got寄摆、got 在 mach-o 中的位置、函數(shù)符號如何與 dyld_stub_binder 進行關(guān)聯(lián)婶恼,以及如何一步步查找動態(tài)庫符號的值柏副。希望對你有用處~
參考資料:
- https://juejin.cn/post/6844903926051897358
- 《程序員的自我修養(yǎng)》