iOS 編譯與鏈接三:靜態(tài)鏈接和動態(tài)鏈接

編譯的過程
編譯的產(chǎn)物

一.靜態(tài)鏈接

隨著計算機的發(fā)展,代碼早就不會只寫在一個文件里了,不同的文件互相關(guān)聯(lián),但卻需要分開編譯,在編譯的時候,每個.m文件都會分別編譯并生成目標文件,也就是.o的文件,而.o就是mach-o類型文件.
每個mach-o都可能有導(dǎo)入符號,這些符號的地址在編譯的時候是不知道的.打個比方就是要解決A.o如何訪問B.o的函數(shù)或者變量的問題.
編譯出來的文件可能是.o,靜態(tài)庫(.o的集合)等.LLVM的連接器會對符號的地址引用進行修正,因為在編譯的時候,這些地址都是假的占位,在鏈接的時候才會替換成真實的,把各個模塊間相互的引用能夠正確的鏈接好,最終將這些mach-o合并成一個mach-o.

而這個過程叫做靜態(tài)鏈接.完成這項工作的是鏈接器,從編譯到靜態(tài)鏈接,叫做構(gòu)建(build).


靜態(tài)鏈接

1.鏈接器

code文件經(jīng)過編譯生成.o, 接下來.o和.a以及.dylib一起經(jīng)過鏈接器合并成可執(zhí)行文件.
生成的可執(zhí)行文件有兩種去處,一個是運行時被loader執(zhí)行,開啟進程,也就是主程序;另一個是服務(wù)于dynamic linker,也就是動態(tài)鏈接庫.

蘋果使用的ld叫做ld64,位置在Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld;
并且開放了源碼

可以在終端查看ld的信息

man ld
image.png

2.dead code striping

image.png

靜態(tài)庫里未被引用的符號會被剝離,而主target里的只要在compile source里添加的,就會被鏈接.

3.鏈接策略

這里主要講的是build setting -> linking -> other link flag的配置,主要有-l -framework -Objc -all_load -force_load等

-l指主動鏈接靜態(tài)庫 如 -l"sqlite3.0"
-framework指主動鏈接framework 如-framework"AVFoundation"
對于這兩個其實并不是必要的,ld64具有自動鏈接的特性,編譯.o時,解析import,把依賴的framework寫入最后 Mach-O 里的LC_LINKER_OPTION.

-ObjC 強制加載所有包含ObjC class和category的.o (symbol name 包含 OBJC_CLASS 或者.objc_c)
想知道它是如何工作的,需要先了解oc符號的生成邏輯.
前文說到mach-o的符號有三種可見度,本地符號,全局符號和未定義符號.

對于一個.o:
OC定義的類,是全局符號;
OC定義的方法,是本地符號;
OC引用的外部類,是未定義符號;
而OC引用方法,卻不會生成符號.

也就是說A.m引用了B.m一個方法,在A.o的符號表只有OBJC_CLASS$_ClassB(undefined),而沒有-[ClassB method].
如果現(xiàn)在有一個分類文件C.m,它是B的分類,編譯之后,C.o中確實有一個方法符號-[ClassB categoryMethod];
當要鏈接的時候,如果C在主Target中,lb64會直接解析它,維護一個objc-cat-list,會保存所有的分類.
如果C在一個靜態(tài)庫中,lb64就沒有理由去鏈接它,因為A.o并沒有一個-[ClassB categoryMethod]的未定義符號需要重定位.
而-ObjC就是為了解決這個問題,可以強制把靜態(tài)庫中的objc class和category都鏈接進來.

現(xiàn)在我們知道了:
1.在靜態(tài)庫單獨定義的category默認不會被鏈接;
2.為被引用的符號會被剝離.
因此我們也可以手動實現(xiàn)-ObjC的效果,那就是在分類的.m文件中實現(xiàn)一個別的東西,可以是c方法,可以是oc類等等,然后引用他們,這樣分類也可以被鏈接,不過這個操作意義不是很大, 總歸要使用第三方靜態(tài)庫的,別人不一定會這么做.

-all_load會鏈接所有的.o,代價很大不建議使用
-force_load $(SRCROOT)/... 需要跟上路徑,指定鏈接某個靜態(tài)庫的全部.o

*4.靜態(tài)鏈接

分別創(chuàng)建A.c和B.c

//A.c
extern int global_var;
void func(int a);
int main() {
    int a = 100;
    func(a+global_var);
    return 0;
}

//B.c
int global_var = 1;
void func(int a) {
    global_var = a;
}

分別編譯出A.o和B.o

xcrun clang -c A.c
xcrun clang -c B.c

然后連接A.o和B.o生成可執(zhí)行文件AB

xcrun clang A.o B.o -o AB
MachOView打開A.o,B.o,AB

查看A.o的符號

objdump --macho --syms A.o
objdump --macho --syms B.o

輸出

A.o:

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000048 l     O __LD,__compact_unwind ltmp1
0000000000000000 g     F __TEXT,__text _main
0000000000000000         *UND* _func
0000000000000000         *UND* _global_var

B.o:

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
000000000000001c l     O __DATA,__data ltmp1
0000000000000020 l     O __LD,__compact_unwind ltmp2
0000000000000000 g     F __TEXT,__text _func
000000000000001c g     O __DATA,__data _global_var

A中未初始化的fun和global_var是未定義符號
B中實現(xiàn)了func和global_var,是全局符號

再看看AB

AB:

SYMBOL TABLE:
0000000100000000 g     F __TEXT,__text __mh_execute_header
0000000100003f94 g     F __TEXT,__text _func
0000000100004000 g     O __DATA,__data _global_var
0000000100003f4c g     F __TEXT,__text _main

都是全局符號

image.png

在MachOView中也能區(qū)分,白色是本地符號,土黃色是全局符號,綠色是未定義符號.

2.符號解析

也叫做符號決議.
1.根據(jù)預(yù)定規(guī)則來檢查符號,比如不允許存在相同的強符號,如果存在報錯dumplicate symbols,相同的符號有強有弱則保留強符號,多個相同的弱符號只保留一個.
2.處理未定義的符號,所有的已定義符號和未定義符號分別存在兩個集合中,然后遍歷未定義集合,去已定義集合中找,匹配成功就移除,如果最后未定義符號集合有沒能成功匹配的,也就是非空,則會報錯Undefined symbols.
3.如果鏈接了一個靜態(tài)庫,那么鏈接器會放棄靜態(tài)庫中沒有被引用的符號.比如引入了一個A.a,但是沒有一個目標文件(或者說項目)引用這個A.a里的符號(類,方法,變量),最終可執(zhí)行文件里就不會包含A.a里的符號.此時可執(zhí)行文件的大小和沒引入A.a編譯的可執(zhí)行文件大小相同.

這個過程是做一個檢查,放到代碼上說,就相當于檢查引用的類,變量,方法等是否真的定義了.如果這一步成功了,基本上就build succeeded了.

3.符號重定位

經(jīng)過檢查之后,知道了未定義的符號其實都在別的目標文件中定義了,那么下面要做的就是確定這些未定義符號的地址.
在上一篇提到過符號的地址,到目前為止程序還沒有運行起來,自然和內(nèi)存沒關(guān)系,這個指的是虛擬地址.
這個地址是從0x0開始的,當程序運行的時候,分配一個偏移量,這偏移就是程序在內(nèi)存的物理地址的開始,在這時偏移加上符號的地址就是物理地址了.

鏈接器在合并A和B的時候,首先兩個mach-o的段會進行合并,代碼段和數(shù)據(jù)段.
然后處理段的信息,合并mach header和load command.
最后重定位符號,要把那些未定義的符號都解決掉.

符號表中描述符號結(jié)構(gòu)體nlist定義如下,下載源碼
位置在EXTERNAL_HEADERS/mach-o/nlist.h

struct nlist {
    union {
#ifndef __LP64__
        char *n_name;   /* for use when in-core */
#endif
        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 */
    int16_t n_desc;     /* see <mach-o/stab.h> */
    uint32_t n_value;   /* value of this symbol (or stab offset) */
};

#define N_STAB  0xe0  /* if any of these bits set, a symbolic debugging entry */
#define N_PEXT  0x10  /* private external symbol bit */
#define N_TYPE  0x0e  /* mask for the type bits */
#define N_EXT   0x01  /* external symbol bit, set for external symbols */

#define N_UNDF  0x0     /* undefined, n_sect == NO_SECT */
#define N_ABS   0x2     /* absolute, n_sect == NO_SECT */
#define N_SECT  0xe     /* defined in section number n_sect */
#define N_PBUD  0xc     /* prebound undefined (defined in a dylib) */
#define    N_INDR   0xa     /* indirect */

#define NO_SECT     0   /* symbol is not in any section */
#define MAX_SECT    255 /* 1 thru 255 inclusive */
符號描述

N_SECT表示明確位置,N_EXT表示外部符號,N_UNDF表示位置不明確.
所以只要N_SECT表示本地,N_SECT+N_EXT表示全局,有N_UNDF表示未定義.

編譯器無法在編譯期確定所有符號的地址,會在mach-o中生成一條對應(yīng)的Relocation信息,這樣連接器就知道section中哪些位置需要被重定位,如何重定位.
在進行重定位的時候,首先會檢查重定位表Relocations


重定位表

重定位表中元素的結(jié)構(gòu)體定義如下
位置在EXTERNAL_HEADERS/mach-o/reloc.h

struct relocation_info {
   int32_t  r_address;  /* offset in the section to what is being
                   relocated */
   uint32_t     r_symbolnum:24, /* symbol index if r_extern == 1 or section
                   ordinal if r_extern == 0 */
        r_pcrel:1,  /* was relocated pc relative already */
        r_length:2, /* 0=byte, 1=word, 2=long, 3=quad */
        r_extern:1, /* does not include value of sym referenced */
        r_type:4;   /* if not 0, machine specific relocation type */
};

鏈接時,首先段進行合并,符號表也會合并,然后在重定位表取一個符號,對應(yīng)到合并后的符號表,將地址等補充完整.


可執(zhí)行文件AB的符號表

三.動態(tài)鏈接

上面說到,為了不把代碼都寫在同一個文件,產(chǎn)生了靜態(tài)鏈接.
而動態(tài)鏈接:
cocoa的各種庫,比如每個app都需要UIKit,每個app在編譯的時候都拷貝一份,這會占用很多硬盤空間,運行的時候,又會都加載到內(nèi)存中,增加內(nèi)存占用,當這些庫需要更新的時候,所有的app都需要更新一次.
因此為了解決這些問題,在硬盤和內(nèi)存中共用文件,所以產(chǎn)生了動態(tài)鏈接.
也因此,和靜態(tài)連接是在編譯的時候,目標文件和靜態(tài)庫會被鏈接打包成一個mach-o不同,動態(tài)鏈接是在運行時進行的.

1.dyld
需要加載動態(tài)鏈接庫的mach-o,其load command會有一個dyld加載命令.指定了dyld的位置

LC_LOAD_DYLINKER

這個命令是這么定義的

struct dylinker_command {
    uint32_t    cmd;        /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
                       LC_DYLD_ENVIRONMENT */
    uint32_t    cmdsize;    /* includes pathname string */
    union lc_str    name;       /* dynamic linker's path name */
};

這個命令還指定了dyld的路徑,如果有這個命令,dlyd就會開始工作.

dyld (the dynamic link editor),動態(tài)連接器,也是一個mach-o,loader中定義這個filetype

#define MH_DYLINKER 0x7     /* dynamic link editor */

啟動了dyld,之后就要加載具體的dylib(dynamically linked shared library)動態(tài)鏈接庫.
因此還有dylib加載命令,和dylib結(jié)構(gòu)體的定義

struct dylib {
    union lc_str  name;         /* library's path name */
    uint32_t timestamp;         /* library's build time stamp */
    uint32_t current_version;       /* library's current version number */
    uint32_t compatibility_version; /* library's compatibility vers number*/
};

/*
 * A dynamically linked shared library (filetype == MH_DYLIB in the mach header)
 * contains a dylib_command (cmd == LC_ID_DYLIB) to identify the library.
 * An object that uses a dynamically linked shared library also contains a
 * dylib_command (cmd == LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, or
 * LC_REEXPORT_DYLIB) for each library it uses.
 */
struct dylib_command {
    uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
                       LC_REEXPORT_DYLIB */
    uint32_t    cmdsize;    /* includes pathname string */
    struct dylib    dylib;      /* the library identification */
};
dylib

和靜態(tài)鏈接類似,只不過這一步被推遲到程序加載的時候.編譯的時候,引用自動態(tài)鏈接庫的符號會被標記上dylib的名稱,并且只有占位地址.


dylib

制作一個dylib查看一下內(nèi)容結(jié)構(gòu)

xcrun clang -fPIC -shared B.c -o dyB.dylib
dylib

2.動態(tài)鏈接的符號重定向

每次啟動程序時,系統(tǒng)ASLR安全機制在都會分配一個隨機偏移值,符號在內(nèi)存的地址等于符號的偏移地址+隨機偏移值
舉個例子
新建一個iOS app,在viewDidLoad斷點


image.png

然后編譯,成功后在DerivedData里找到可執(zhí)行文件,用MachOView打開,查看符號表


image.png

然后看到一個偏移值,1f80
接下來運行,在斷點時,選擇xcode->Debug-> Debug WorkFlow -> Always show disassembly查看匯編
image.png

然后我們看到viewDidLoad的地址

接下來使用lldb命令 image list,找到最上面程序起始地址


image.png

首地址本應(yīng)該是0x0,現(xiàn)在是0x000000010428b000,這個就是隨機偏移量
然后計算一下,正好是1f80.
(lldb) p/x 0x10428cf80-0x000000010428b000
(long) $0 = 0x0000000000001f80

一個動態(tài)鏈接庫比如libsystem.B.dylib,里面有巨量的符號,但是這個main只使用了一個NSLog,因此動態(tài)鏈接不會在程序一啟動的時候就去連接,而是在使用到某個符號的時候才會去做符號重定位,再之后使用就不需要重定位了.
當程序首次訪問外部符號時,先執(zhí)行Symbol Stubs樁代碼,然后跳轉(zhuǎn)到Lazy Symbol Pointers對應(yīng)符號的地址,首次訪問會根據(jù)這個地址在Assembly文件中找到相應(yīng)的代碼執(zhí)行,最后調(diào)用dyld_stub_binder函數(shù)進行符號綁定,綁定完成之后就會更新Lazy Symbol Pointers表中的值,將符號地址直接寫入到表中,再次訪問的時候就可以直接訪問這個地址而不需要在執(zhí)行Assembly中的代碼.

添加一句NSLog,這個函數(shù)來自Foundation.


image.png

然后運行,


image.png

可以看到Symbol Stub

和__TEXT__text的匯編對比


image.png

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驴娃,隨后出現(xiàn)的幾起案子抹锄,更是在濱河造成了極大的恐慌谭贪,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迈嘹,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機宴抚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甫煞,“玉大人菇曲,你說我怎么就攤上這事「Х停” “怎么了常潮?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長楷力。 經(jīng)常有香客問我喊式,道長,這世上最難降的妖魔是什么萧朝? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任岔留,我火速辦了婚禮,結(jié)果婚禮上检柬,老公的妹妹穿的比我還像新娘献联。我一直安慰自己,他們只是感情好何址,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布里逆。 她就那樣靜靜地躺著,像睡著了一般用爪。 火紅的嫁衣襯著肌膚如雪原押。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天项钮,我揣著相機與錄音班眯,去河邊找鬼。 笑死烁巫,一個胖子當著我的面吹牛署隘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亚隙,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼磁餐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诊霹,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤羞延,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脾还,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伴箩,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年鄙漏,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗤谚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡怔蚌,死狀恐怖巩步,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情桦踊,我是刑警寧澤椅野,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站籍胯,受9級特大地震影響竟闪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜芒炼,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一瘫怜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧本刽,春花似錦鲸湃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至斜友,卻和暖如春炸裆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鲜屏。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工烹看, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人洛史。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓惯殊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親也殖。 傳聞我的和親對象是個殘疾皇子土思,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容