一因痛、背景
首先要知道鏈接是干什么的饮醇。我們可以通過IDE寫一部分代碼毕源,也需要從其他的Libray或者FrameWork使用第三方提供的代碼浪漠。為了能夠用到這些三方代碼,我們需要一個鏈接器將代碼結(jié)合起來霎褐。關(guān)于鏈接大致上可以分為兩種類型:
- 靜態(tài)鏈接:發(fā)生在我們構(gòu)建我們應(yīng)用程序的時候址愿,也就是說這種鏈接方式會影響到我們構(gòu)建程序的時長、以及構(gòu)建出來的應(yīng)用程序的大卸沉А响谓;
- 動態(tài)鏈接:發(fā)生在我們應(yīng)用程序啟動/運行的時候,它會影響我們應(yīng)用程序的啟動時長省艳;
這篇Session里面會同時涉及到它們兩種娘纷,包含的議題如下:
- 什么靜態(tài)鏈接;
- ld64最近改進的地方跋炕;
- 靜態(tài)鏈接最佳實踐失驶;
- 什么是動態(tài)鏈接;
- 最近關(guān)于dyld的改進枣购;
- 動態(tài)鏈接最佳實踐;
- 最后會介紹一些新的工具擦耀;
在這篇文章中除了會介紹該Session的內(nèi)容棉圈,還會穿插一些我個人對相應(yīng)知識的理解。
二眷蜓、靜態(tài)鏈接
對于只有一個源文件的程序分瘾,構(gòu)建他對于我們來說是很簡單的。比如我們有個簡單的程序:
#include <unistd.h>
int main(int argc, const char *argv[]) {
char buf[] = "Link fast: Improve build and launch times\n";
write(STDOUT_FILENO, buf, sizeof(buf));
return 0;
}
然后通過clang進行編譯構(gòu)建即可吁系。
但是如果有多個源文件需要構(gòu)建呢德召?當(dāng)然我們也使用clang去編譯這些源文件白魂。但是問題來了,難道我們每次都要全量重新構(gòu)建所有的源文件嗎上岗?為了避免這個問題福荸,我們可以將這些源文件拆分成多個不同部分。這樣他們之間相互影響的可能性就降低了:
/// main.m
#include "pfb_out.h"
int main(int argc, const char *argv[]) {
char buf[] = "Link fast: Improve build and launch times\n";
pfb_std_out(buf, sizeof(buf));
return 0;
}
/// pfb_out.c
#include <unistd.h>
void pfb_std_out(const void * __buf, int len) {
write(STDOUT_FILENO, __buf, len);
}
針對多個源文件的情況下肴掷,我們就不是一次性將源文件構(gòu)建為一個可執(zhí)行的應(yīng)用程序敬锐,而是先將其編譯成“可重定位”的目標(biāo)文件。我們用如下指令來演示上面的過程(如果不清楚ld到底需要哪些參數(shù)呆瞻,我們可以直接clang編譯一個完成的程序并加上-v參數(shù)台夺,它會詳細地展示中途發(fā)生了任何事件):
$ cc -c prog.c -o prog.o
$ cc -c pfb_out.c -o pfb_out.o
// syslibroot:指定搜索的library和framework,因為這里我使用了write函數(shù)痴脾。它需要由系統(tǒng)庫提供支持
// lSystem:尋找libSystem
$ ld prog.o pfb_out.o -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -o prog -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin/libclang_rt.osx.a
既然都分了模塊颤介,那我們就可以使用“ar”指令來構(gòu)造一個靜態(tài)庫:
$ ar -rc libPfbout.a pfb_out.o /// 目標(biāo)文件打包為library
//$ ar -x libPfbout.a /// library拆分為目標(biāo)文件
// -Ldir 指定要查找library的路徑
// -lPfbout 要鏈接libPfbout.a
$ ld prog.o -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -o prog -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin/libclang_rt.osx.a -L/path/to/libarary/dir -lPfbout
使用這種靜態(tài)庫的方式極大地改善了代碼地共享。
重定位
用上面這種方式生成的最終產(chǎn)物有可能是十分龐大的赞赖,這是因為它可能會包含從Library中拷貝出來成千上萬的函數(shù)(即便是說我們明確引用并沒有多少函數(shù))滚朵。所以針對這個問題Apple提供了一個比較巧妙的優(yōu)化方式,相較于使用靜態(tài)庫中所有的.o(object 目標(biāo))文件薯定,我們在構(gòu)建的時候可以只獲取其中的一部分始绍。
還是以剛剛的例子我們改動一下,他們之間的依賴關(guān)系如下:prog.c依賴pfb_string.c(調(diào)用了pfb_string里面的newString、str_description话侄、str_free函數(shù))亏推,pfb_string依賴libpfbio.a(調(diào)用了pfb_std_out)。其中l(wèi)ibpfbio.a的pfb_std_in函數(shù)并未調(diào)用年堆,pfb_string.c中的pfb_str_len也未被調(diào)用吞杭。
最終我們使用ld指令生成最后的產(chǎn)物:
$ ld prog.o pfb_string.o -lpfbio -L/Users/wangwang/WorkStation/iOS/exec/Clang_basic/Clang_basic -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin/libclang_rt.osx.a -o prog
通過對比之后發(fā)現(xiàn)原來prog.o中callq指令后面的數(shù)字被替換為__text端里面真正的地址了,并且libpfbio.a里面包含的pfb_in.o相關(guān)的內(nèi)容并未在可執(zhí)行文件的__text中看見其蹤影变丧,但是我們在pfb_string.o里面未使用的函數(shù)pfb_str_len卻出現(xiàn)在了最終產(chǎn)物的__text里面芽狗。
那這個過程是如何執(zhí)行的呢?這是因為在每個目標(biāo)文件中存在一個Relocations來保留其所需要重定位的信息痒蓬,它描述了如何修改相應(yīng)段里面的內(nèi)容:
序號 | address | pcrel | length | extern | type | scattered | symbolnum/value |
---|---|---|---|---|---|---|---|
1 | 0x00000034 | 1 | 2 | 1 | 2 | 0 | 3 |
2 | 0x0000002b | 1 | 2 | 1 | 2 | 0 | 2 |
3 | 0x0000001e | 1 | 2 | 1 | 2 | 0 | 1 |
4 | 0x00000019 | 1 | 2 | 0 | 1 | 0 | 2 |
這里需要對重定位表的內(nèi)容解析一下童擎,重定位的結(jié)構(gòu)定義在relo.h 中。其中extern字段為1則表明symbolnum的值為符號表序號攻晒,比如第一個symbolmum為3找到符號表(下表)顾复,表示該符號名稱為_str_descirption;
如果extern字段為0則表示section的序號鲁捏,比如第4項它的symbolnum值表示的是目標(biāo)文件中的Section64(__TEXT, __cstring)芯砸。
序號 | Type | SEC_INDEX | StringTable | Symbol Name |
---|---|---|---|---|
1 | 0x0f | 01 | 0x00000012 | _main |
2 | 0x01 | 00 | 0x00000018 | _newString |
3 | 0x01 | 00 | 0x00000001 | _str_description |
4 | 0x01 | 00 | 0x00000023 | _str_free |
而重定位表中的address字段表示的是當(dāng)前要被重定位Section的偏移量。比如prog.o的第一個address值為0x34,通過otool查看prog.o的(__TEXT, __text)原始值:
? ? otool -t prog.o
prog.o:
(__TEXT,__text) section
0000000000000000 55 48 89 e5 48 83 ec 20 c7 45 fc 00 00 00 00 89
0000000000000010 7d f8 48 89 75 f0 48 8d 3d 23 00 00 00 e8 00 00
0000000000000020 00 00 48 89 45 e8 48 8b 7d e8 e8 00 00 00 00 48
0000000000000030 8b 7d e8 e8 00 00 00 00 31 c0 48 83 c4 20 5d c3
可以看到其記錄的值均為e8后面的00(這里e8是操作碼假丧,對應(yīng)call近地址相對位移指令:該指令后面所跟的值是指的與其下一個指令的偏移地址)双揪,由于在當(dāng)前目標(biāo)文件中,并不能知道真實的函數(shù)地址包帚。因此這里是用00來進行代替渔期。針對這個現(xiàn)象簡單描述一下最基礎(chǔ)的鏈接過程:
ld首先加載prog.o查看其符號表:
? ? nm -ax prog.o
//
Type SEC_INDEX StringTable Offset Symbol Name
0f 01 00000012 _main
01 00 00000018 _newString
01 00 00000001 _str_description
01 00 00000023 _str_free
/*
前面兩個用于高4位
#define N_STAB 0xe0
#define N_PEXT 0x10
后面兩個用于低4位
#define N_TYPE 0x0e
#define N_UNDF 0x0
========================================================================================================
子Type
#define N_ABS 0x2
#define N_SECT 0xe
#define N_PBUD 0xc
#define N_INDR 0xa
#define N_EXT 0x01
*/
首先Type上面4個位進行操作。如果其是和N_TYPE進行按位與操作婴噩,那將得到的結(jié)果還需要和其5個子type進行對比擎场;
而另外三個只需要和自身相比是否為0即可。比如:
0f & 0e(N_TYPE) = 0e ==> N_SECT
0f & 01(N_EXT) = 01 ==> N_EXT
01 & 0e(N_TYPE) = 00 ==> N_UNDF
01 & 01(N_EXT) = 01 ==> N_EXT
通過分析Type為01的_newString几莽、_str_description迅办、_str_free為未定義的符號。因此需要加載pfb_string.o來處理上一步中出現(xiàn)的三個未定義的符號章蚣。同樣也引入了新的未定義的符號:
? ? nm -ax pfb_string.o
0000000000000000 01 00 0000 00000001 _pfb_std_out
0000000000000000 01 00 0000 00000043 _free
0000000000000000 01 00 0000 00000049 _malloc
0000000000000000 0f 01 0000 0000002c _growth
00000000000000e0 0f 01 0000 00000034 _newString
00000000000001f0 0f 01 0000 0000001f _pfb_str_len
0000000000000190 0f 01 0000 0000000e _str_description
00000000000001c0 0f 01 0000 0000003f _str_free
在libpfbio.a中發(fā)現(xiàn)了一個我們需要的的未定義的符號:
? ? nm -ax libpfbio.a
libpfbio.a(pfb_in.o):
0000000000000000 0f 01 0000 00000001 _pfb_std_in
0000000000000000 01 00 0000 0000000d _read
libpfbio.a(pfb_out.o):
0000000000000000 0f 01 0000 00000001 _pfb_std_out
0000000000000000 01 00 0000 0000000e _write
最后我們來看一下最終的可執(zhí)行程序中符號表的情況(只剩下未定義的符號)上述各個目標(biāo)文件中出現(xiàn)的未定義的符號站欺,大部分的值都被重新進行了設(shè)置,除了幾個在下文將會提及的動態(tài)鏈接的符號纤垂。
我們再來對看看鏈接后的變化:
靜態(tài)庫中我們不需要的符號(比如這里的pfb_std_in)并未拷貝到最終的產(chǎn)物中矾策,源文件中的未使用的函數(shù)pfb_str_len依然被拷貝到了最終的產(chǎn)物里面。
ld64改進的地方
我們現(xiàn)在所用的ld實際上都是ld64峭沦,Apple說他們在這一年針對ld64進行了改進贾虽。按照Apple的說法,對于大部分的工程來說鏈接速度快了2倍吼鱼。其能夠更好利用我們機器的核心蓬豁,也就是說大部分的時候可以用多核并行執(zhí)行鏈接操作,其中包括有:
- 從輸入文件中拷貝相關(guān)內(nèi)容到輸出文件菇肃;
- 并行構(gòu)建LINKEDIT地粪;
- 并行hash計算;
- 優(yōu)化export-trie構(gòu)建算法(對于使用C++ string_view來表示每個符號的字符串效果十分明顯)琐谤;
- 針對二進制文件uuid計算蟆技,使用基于硬件加速的加密庫(SHA256);
靜態(tài)鏈接最佳實踐
在提升鏈接器性能時斗忌,有些應(yīng)用程序中的配置問題會影響鏈接時間质礼。下面我們就來看看哪些事情是我們能進行優(yōu)化的。
比如我們將源文件構(gòu)建到已有靜態(tài)庫的時候织阳,不僅僅是源文件需要編譯几苍,而且該靜態(tài)庫也需要重新編譯。這是由于文件重新編譯后陈哑,整個靜態(tài)庫包括相關(guān)的table都需要重新構(gòu)建, 這就引入了很多額外的 I/O。因此靜態(tài)庫只對于相對穩(wěn)定的代碼有意義:
如果是需要頻繁變更的代碼惊窖,最好是將其從靜態(tài)庫從移除刽宪!
而當(dāng)我們在構(gòu)建靜態(tài)庫的時候,有三個不為人熟知選項可以優(yōu)化鏈接耗時:
- 1界酒、-all_load和-force_load圣拄;
- 2、-no_export_symbols毁欣;
- 3庇谆、-no_deduplicate;
這幾個選項分別都有什么用處呢?在前面我們提到了可以從靜態(tài)庫中選擇部分內(nèi)容來進行加載凭疮,這樣做的一個壞處就是會降低鏈接速度。這是因為要滿足這個特性的話,鏈接器就必須要遵守傳統(tǒng)的鏈接規(guī)則以串行的方式處理各個靜態(tài)庫叫惊,這意味著說我們不能使用基于ld64的并行化能力哲身。反之我們就可以鏈接器的相關(guān)選項來加速我們的構(gòu)建工作。
這時候我們就可以使用all_load選項衰腌,它告訴鏈接器從所有的靜態(tài)庫中加載所有的目標(biāo)文件新蟆。如果我們最后的程序是需要這里面大部分的內(nèi)容的話,這個選項會有很大的收益右蕊,這是因為all_load是讓鏈接器并行地去解析相關(guān)內(nèi)容琼稻。
不過使用這個選項也有壞處:
- 當(dāng)我們的應(yīng)用程序使用某種手段讓多個靜態(tài)庫使用了相同的符號,并且是強依賴ld指令中輸入的靜態(tài)庫順序的時候饶囚。那我們就沒有辦法使用all_load選項了帕翻;
- 使用了all_load之后會使得我們的應(yīng)用程序包體積變大,這是因為很多無用的代碼也會被添加到程序中坯约;
為了彌補這個缺點熊咽,我們可以使用 -dead_strip(死代碼消除) 選項,該選項會讓鏈接器去移除那些無用的代碼和數(shù)據(jù)闹丐。Apple說現(xiàn)在dead strip的算法很很快横殴,足以彌補我們?yōu)榱藘?yōu)化鏈接耗時使用all_load而帶來包體積的劣化。不過Apple還是建議我們可以用-all_load/-dead_strip和不使用all_load做一下收益對比(不同應(yīng)用程序有不同的表現(xiàn))卿拴。
這第二個選項就是 no_export_symbols衫仑。這里先插入一點背景知識,由鏈接器生成的LINKEDIT段包含了export-trie(它是一個前綴樹堕花,對所有生成的符號名稱文狱、地址和標(biāo)志進行編碼):
雖然說所有dylib都需要導(dǎo)出符號(export Symbols),但程序二進制文件通常不需要任何導(dǎo)出符號缘挽。通常是不需要在程序的非主二進制文件(這里“主”的意思是相對其他靜態(tài)庫而言的瞄崇,比如包含了main函數(shù)的二進制文件)進行符號查找的呻粹。這樣的話,我們可以對相應(yīng)的程序使用 -no_exported_symbols 以節(jié)省在 LINKEDIT 中創(chuàng)建 trie 數(shù)據(jù)結(jié)構(gòu)的耗時苏研。
當(dāng)然這也是有壞處的:
- 如果我們主程序要加載可以鏈接到主線程的插件時等浊,不導(dǎo)出相關(guān)符號是沒有辦法;
- 如果我們將xctest做為程序的主環(huán)境(host environment)來加載xctest bundles時摹蘑,不導(dǎo)出相關(guān)符號也是無法實現(xiàn)的筹燕;
對于這個選項,Apple的建議是只有當(dāng)export-trie很大時才有必要使用這個選項衅鹿。我們可以使用dyld_info指令來檢查我們程序?qū)С龇柕臄?shù)量:
? ? dyld_info -exports prog
prog [x86_64]:
-exports:
offset symbol
0x00000000 __mh_execute_header
0x00003CC0 _growth
0x00003DA0 _newString
0x00003E50 _str_description
0x00003E80 _str_free
0x00003EB0 _pfb_str_len
0x00003ED0 _pfb_std_out
0x00003F00 _main
? ? dyld_info -exports prog | wc -l
11
顯然我們這個demo的導(dǎo)出符號的數(shù)量太少撒踪,使用這個選項并沒有多大的收益,但對于存在很大導(dǎo)出符號的程序而言大渤,Apple給出的數(shù)據(jù)是鏈接器需要花費2到3秒的時間來構(gòu)建export-trie制妄。
第三個是 no_deduplicate 選項。Apple介紹在幾年前他們?yōu)殒溄悠餍绿砑恿艘粋€用于合并具有相同指令但是名稱不一樣函數(shù)的pass兼犯,這么做代價是比較大的忍捡,因為鏈接器需要對每個函數(shù)的指令遞歸地進行hash操作,以便于能夠找到重復(fù)的內(nèi)容切黔。因此Apple限制其只在弱定義的(weak-def)符號上使用該算法砸脊。
這里的弱符號簡單延伸一下,強弱符號的定義和引用都是針對于鏈接器而言的纬霞。默認我們在定義一個符號時給了其一個初值凌埂,它默認是強符號;反之如果只是簡單定義了符號由編譯器給一個缺省值诗芜,這種就默認是弱符號(發(fā)生符號沖突之后瞳抓,弱符號會被強符號覆蓋)。當(dāng)然我們也可以明確指定弱符號伏恐,使用attribute((weak)):
int pfb_strong_symbol = 1; /// 強符號
int pfb_weak_symbol; /// 弱符號
__attribute__((weak)) int pfb_symbol_value = 10;
void pfb_symbol_function(int a) __attribute((weak));
相對于定義而言孩哑,引用也有強弱之分。我們可以使用attribute((weakref))來定義:
__attribute__ ((weakref)) void foo();
deduplicate主要是用于體積優(yōu)化翠桦。對于debug是為了快速構(gòu)建而不是包體積横蜒,所以在默認情況下Xcode是通過傳遞no_deduplicate來取消去重能力,如果設(shè)置-O0的也是關(guān)閉去重能力的销凑。
這些選項我們在Xcode里面都可以進行設(shè)置:
當(dāng)我們在使用靜態(tài)庫的時候會出現(xiàn)一些意想不到的事情丛晌。比如當(dāng)我們使用靜態(tài)庫鏈接到我們程序中時某些代碼可能并不會出現(xiàn)在最終的產(chǎn)物里面。比如我們對某些函數(shù)添加了attribute((unused))斗幼,或者是使用了Objective-C的category澎蛛,由于鏈接器會選擇性地加載了靜態(tài)庫中用到的符號,而沒有用到的將不會出現(xiàn)在產(chǎn)物里面蜕窿;
另一個比較神奇的現(xiàn)象是靜態(tài)庫和dead_strip的搭配谋逻,dead_strip可以隱藏很多靜態(tài)庫的問題呆馁。正常來說缺少符號和重復(fù)符號都會使得鏈接器報錯,而當(dāng)配置了dead_strip之后鏈接器會從main函數(shù)開始對所有的指令和數(shù)據(jù)進行可達性分析毁兆,如果發(fā)現(xiàn)丟失/重復(fù)的符號來自無法訪問的代碼智哀,鏈接器將不會拋出符號缺失/重復(fù)的錯誤信息。
最神奇的現(xiàn)象是當(dāng)一個靜態(tài)庫被合并到多個framework中的時候荧恍,他們單獨運行沒有問題。但當(dāng)他們被同時放到同一個程序中時屯吊,由于多個定義而遇到奇怪的運行時問題送巡。
總的來說靜態(tài)庫很強大,但這取決于我們對其有充分認識以避免會出現(xiàn)各種各樣奇怪問題盒卸。
三骗爆、動態(tài)鏈接
由于應(yīng)用程序可能會引入越來越多的代碼或者靜態(tài)庫,這就導(dǎo)致我們的程序包體積會越來越大蔽介,這就是為什么我們需要動態(tài)鏈接的原因摘投!
在90年代的時候,是通過將ar(我們前面講的將目標(biāo)文件打包成靜態(tài)庫的工具)改成ld輸出一個可執(zhí)行的二進制文件虹蓄。
在Mac相關(guān)的生態(tài)里面動態(tài)庫叫做dylib犀呼,而在其他平臺則是叫做DSOs(Linux一般都叫做dso ,dynamic shared Object)或者DDLs(Windows平臺薇组,Dynamic-Link Library)外臂。
相較于靜態(tài)鏈接是將代碼拷貝到最終的產(chǎn)物里面,動態(tài)鏈接只是記錄了從動態(tài)庫中使用到的符號律胀、以及運行時的路徑宋光。這樣做的好處就是我們可以自己控制程序的包大小,最終的產(chǎn)物只是包含相關(guān)源代碼炭菌,動態(tài)庫相關(guān)的內(nèi)容只有在運行時才需要罪佳。并且靜態(tài)鏈接的時間只是和我們的代碼多少相關(guān),和需要動態(tài)庫的多少無關(guān)黑低;另外一個好處針對內(nèi)存這塊赘艳,當(dāng)同一個動態(tài)庫被多個進程使用的時候,虛擬內(nèi)存系統(tǒng)會針對dylib重用相同的物理頁投储。
為了能夠更好地理解這一點第练,我這邊構(gòu)建了一個簡單的動態(tài)庫。其源代碼如下:
#include <stdio.h>
void pfb_foo(int i) {
printf("\nLink Fast: %d\n", i);
}
使用 "clang -shared -fPIC -o libpfbdyc.dylib pfb_dylib.c" 進行構(gòu)建玛荞。這里PIC指的是地址無關(guān)代碼娇掏,這樣共享的指令在裝載時不會因為裝載地址的改變而發(fā)生改變,也就是我們常說的la_symbol_ptr勋眯。然后我們再修改一下前面用到prog.c:
#include "pfb_string.h"
#include <unistd.h>
extern void pfb_foo(int i);
int main(int argc, const char *argv[]) {
pfb_string *str_ptr = newString("Link fast: Improve build and launch times");
str_description(str_ptr);
int len = pfb_str_len(str_ptr);
str_free(str_ptr);
pfb_foo(len);
int i = 0;
while (i < 60)
{
sleep(1);
i++;
}
return 0;
}
然后我們使用" clang prog.c -lpfbio -lpfbdyc -L . pfb_string.o -o prog_dylib "構(gòu)建出一個可執(zhí)行文件婴梧。為了驗證第一點下梢,即動態(tài)鏈接并不會將代碼拷貝到最終的產(chǎn)物里面。我們可以通過“otool -tv prog_dylib”來查看其內(nèi)容:
prog_dylib:
(__TEXT,__text) section
_main:
0000000100003c70 pushq %rbp
0000000100003c71 movq %rsp, %rbp
...
_pfb_std_out:
0000000100003cf0 pushq %rbp
...
_growth:
0000000100003d20 pushq %rbp
...
_newString:
0000000100003e00 pushq %rbp
...
_str_description:
0000000100003eb0 pushq %rbp
...
_str_free:
0000000100003ee0 pushq %rbp
...
_pfb_str_len:
0000000100003f10 pushq %rbp
...
在上面的(__TEXT,__text)里面并未看到pfb_foo相關(guān)的代碼塞蹭。
而對于內(nèi)存使用這塊兒孽江,是虛擬內(nèi)存系統(tǒng)分配這塊兒做針對物理內(nèi)存進行了復(fù)用。不過這塊兒目前我沒有找到可以驗證的方法番电,但是可以通過 vmmap 來查看每個進程的虛擬內(nèi)存使用情況岗屏。
動態(tài)綁定過程
當(dāng)我們在進行程序構(gòu)建的時候,我們會通過ld鏈接器對所有的符號進行重定位(大致是靜態(tài)鏈接)漱办。但是動態(tài)鏈接的操作只是在最終的產(chǎn)物里面添加了stub(也就是通常說的樁)这刷,并不是真正意義上的函數(shù)或者數(shù)據(jù)的地址:
地址0x100003f2e的內(nèi)容如下:
針對每一個stub在集合中的下標(biāo),根據(jù)相同的下標(biāo)在在__la_symbol_ptr里面找到對應(yīng)的symbol的指針娩井。這個__la_symbol_ptr是存在于(__DATA, __la_symbol_ptr)暇屋。這個數(shù)據(jù)段是可寫的,我們通過lldb的“image dump section”來進行驗證:
查看其內(nèi)存值:
(lldb) x 0x0000000100008000
0x100008000: 8c 3f 00 00 01 00 00 00
0x100008008: 96 3f 00 00 01 00 00 00
0x100008010: a0 3f 00 00 01 00 00 00
地址0x3F8C對應(yīng)的指令如下洞辣,接著會執(zhí)行push和jmp指令咐刨。這個jmp指令就到了__stub_helper的起始地址,真正執(zhí)行相關(guān)的跳轉(zhuǎn)操作扬霜。
當(dāng)我們動態(tài)鏈接之后再來看該地址所對應(yīng)的值:
(lldb) x 0x0000000100008000
0x100008000: 80 4f 1d 00 01 00 00 00
0x100008008: ab ff 1c 12 f8 7f 00 00
0x100008010: af 76 1b 12 f8 7f 00 00
而對應(yīng)執(zhí)行的指令已經(jīng)被正確地更正了:
-> 0x100003f6e <+0>: jmpq *0x4094(%rip) ; (void *)0x00007ff8121cffab: printf
這個過程相對來說就是一個比較宏觀的動態(tài)鏈接過程定鸟,這個過程Apple稱之為“Fix up”。
Fishhook實際上就是利用了這個原理畜挥,通過下面這一系列的操作找到我們需要替換的符號:
這樣就可以定位我們需要修改的符號仔粥,最后修改對應(yīng)__lazy_symbol_ptr的值(函數(shù)指針)從而避免了走stub那一套:
struct dysymtab_command* dysymtab_cmd = NULL;
...
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
...
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
...
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
...
/// 更新函數(shù)指針
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
針對動態(tài)鏈接的優(yōu)化
Chain Fixup
今年Apple提供了一個新的fix up——“Chained Fixups”。它的第一個優(yōu)勢是使得LinkEdit變得更小了蟹但,LinkEdit里面包含了很多和鏈接相關(guān)的信息躯泰,我們在靜態(tài)鏈接部分提到過。由于不是存儲所有需要fix的位置华糖,新格式只存儲每個 DATA 頁面中第一個修正位置的位置以及導(dǎo)入符號的列表麦向,其余的信息都是被編碼在DATA段的內(nèi)部。
該結(jié)果在iOS13.4以后就已經(jīng)支持客叉。
加載流程緩存
第二個優(yōu)化就是針對和dyld相關(guān)的執(zhí)行流程優(yōu)化诵竭,傳統(tǒng)的程序運行過程是先解析Mach-O文件,接著是找到所有依賴的動態(tài)庫兼搏,找到之后通過mmap映射到內(nèi)存中卵慰。接著尋找所需的符號并進行修正。最后執(zhí)行相關(guān)的初始化器佛呻。
在2017年以后裳朋,Apple上面的三個綠色的步驟做了緩存。也就是說如果沒有修改程序吓著、而且動態(tài)庫沒有發(fā)生修改的情況下鲤嫡,綠色的步驟是被緩存起來的送挑。
Page In Linking
相對于每次啟動的時候都去做相關(guān)的修正操作,內(nèi)核現(xiàn)在則是在DATA Page In的時候進行修正(頁裝載到內(nèi)存中的時候)暖眼。當(dāng)我們通過mmap首次使用某些地址的時候會觸發(fā)內(nèi)核去讀取某些頁惕耕,現(xiàn)在如果是DATA頁的話,內(nèi)核還會去做修正工作诫肠。這么做的話會使得在啟動階段司澎,減少部分Dirty Memory。
這個feature之前只在MacOS上生效栋豫,iOS16也會引入該能力惭缰。需要注意的是 dyld 僅在啟動期間使用此機制,在此之后的任何時間調(diào)用 dlopen 的 dylib 都不支持page-in linking笼才。
動態(tài)鏈接最佳實踐
上面Apple也做不少的優(yōu)化工作,我們唯一能做的就是控制動態(tài)庫的數(shù)量了络凿。如果我們的代碼每次都會執(zhí)行的話骡送,可以考慮將動態(tài)庫遷移到靜態(tài)庫;
所有在初始化階段需要耗時超過幾毫秒的任務(wù)都不要放在初始化階段絮记,比如IO和網(wǎng)絡(luò)相關(guān)的事情摔踱;
動態(tài)鏈接的兩面性
那這些收益的代價是什么呢?可以肯定的是使用動態(tài)編譯可以優(yōu)化我們的構(gòu)建時間怨愤,代價卻是在啟動程序的時長變得更長了派敷。這是因為加載不僅僅只是裝載一個程序,而是需要將各個dylib加載和連接起來;
其次是使用了動態(tài)庫的程序會存在更多的臟頁撰洗,iOS的虛擬內(nèi)存分為clean memory(不可更改篮愉,或者未寫的內(nèi)存)、dirty memory(可更改差导、或者被寫入數(shù)據(jù)的內(nèi)存)试躏。這里為什么說dirty memory變多了呢?是因為一些lazy bind操作所需的符號信息设褐,是放在__DATA里面的颠蕴。而每個dylib都有自己的__DATA;
最后,由于動態(tài)鏈接機制的原因需要做運行時的鏈接操作助析,這會使得運行期間的執(zhí)行效率將會有所下降犀被。
五、工具介紹
第一個工具是dyld_usage外冀,它只在macOS下面生效寡键,不過我們可以在模擬器啟動階段使用:
sudo dyld_usage imeituan
由于我沒有升級最新的系統(tǒng),所以我就針對WWDC視頻截圖:
第二個工具是dyld_info锥惋,可以使用它來檢查磁盤上和當(dāng)前 dyld 緩存中的二進制文件昌腰。比如我們可以用fixup選項开伏,來看我們在動態(tài)鏈接的時候需要修正的符號:
? ? dyld_info -fixups prog_dylib2
prog_dylib2 [x86_64]:
-fixups:
segment section address type target
__DATA_CONST __got 0x100004000 bind libSystem.B.dylib/dyld_stub_binder
__DATA __la_symbol_ptr 0x100008000 rebase 0x100003F8C
__DATA __la_symbol_ptr 0x100008000 bind libpfbdyc.dylib/_pfb_foo
__DATA __la_symbol_ptr 0x100008008 rebase 0x100003F96
__DATA __la_symbol_ptr 0x100008008 bind libSystem.B.dylib/_printf
__DATA __la_symbol_ptr 0x100008010 rebase 0x100003FA0
__DATA __la_symbol_ptr 0x100008010 bind libSystem.B.dylib/_sleep
這基本上和我們前面看到的lazy_symbol_ptr一致。
使用export選項可以查看當(dāng)前動態(tài)庫遭商,對外導(dǎo)出了哪些符號:
? ? dyld_info -exports libpfbdyc.dylib
libpfbdyc.dylib [x86_64]:
-exports:
offset symbol
0x00003F80 _pfb_foo