一.靜態(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).
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
2.dead code striping
靜態(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
查看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
都是全局符號
在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)到合并后的符號表,將地址等補充完整.
三.動態(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的位置
這個命令是這么定義的
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 */
};
和靜態(tài)鏈接類似,只不過這一步被推遲到程序加載的時候.編譯的時候,引用自動態(tài)鏈接庫的符號會被標記上dylib的名稱,并且只有占位地址.
制作一個dylib查看一下內(nèi)容結(jié)構(gòu)
xcrun clang -fPIC -shared B.c -o dyB.dylib
2.動態(tài)鏈接的符號重定向
每次啟動程序時,系統(tǒng)ASLR安全機制在都會分配一個隨機偏移值,符號在內(nèi)存的地址等于符號的偏移地址+隨機偏移值
舉個例子
新建一個iOS app,在viewDidLoad斷點
然后編譯,成功后在DerivedData里找到可執(zhí)行文件,用MachOView打開,查看符號表
然后看到一個偏移值,1f80
接下來運行,在斷點時,選擇xcode->Debug-> Debug WorkFlow -> Always show disassembly查看匯編
然后我們看到viewDidLoad的地址
接下來使用lldb命令 image list,找到最上面程序起始地址
首地址本應(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.
然后運行,
可以看到Symbol Stub
和__TEXT__text的匯編對比