Mach-O是macOS、iOS舱污、iPadOS存儲(chǔ)程序和庫(kù)的文件格式什往,對(duì)應(yīng)的系統(tǒng)通過(guò)應(yīng)用二進(jìn)制接口(ABI--MachO內(nèi)容的格式)來(lái)運(yùn)行該格式的文件。保存了在編譯過(guò)程和鏈接過(guò)程中產(chǎn)生的機(jī)器代碼和數(shù)據(jù)慌闭。為靜態(tài)鏈接和動(dòng)態(tài)鏈接提供了單一的文件格式别威。
當(dāng)我們點(diǎn)擊Xcode Run的時(shí)候,系統(tǒng)會(huì)加載IPA包內(nèi)的可執(zhí)行文件驴剔,調(diào)用fork函數(shù)省古,創(chuàng)建一個(gè)進(jìn)程,然后調(diào)用execve程序加載器丧失,將文件加載到內(nèi)存豺妓,分析MachO中的mach_header,以確認(rèn)它是有效格式的MachO文件布讹。
那么MachO到底是什么格式呢琳拭?如何查看MachO文件里面的內(nèi)容,我們可以通過(guò)一個(gè)可視化的工具M(jìn)achOView來(lái)查看描验,也可以通過(guò)objdump命令來(lái)查看objdump --macho --private-headers MachO文件路徑白嘁,通過(guò)控制臺(tái)輸出或MachOView可以看到,它里面有Header膘流、Load command還有一些Sections等絮缅,可以看出它其實(shí)就是有很多的配置項(xiàng)的二進(jìn)制文件。
編譯與鏈接:當(dāng)我們寫(xiě)的代碼進(jìn)行編譯的時(shí)候會(huì)生成一個(gè)個(gè).o文件呼股,也就是MachO文件耕魄,這個(gè)過(guò)程就是將代碼放到對(duì)應(yīng)的配置中,將各種類(lèi)型的符號(hào)進(jìn)行歸類(lèi)存放彭谁。而鏈接的本質(zhì)就是把多個(gè)目標(biāo)(.o)文件組合成一個(gè)可執(zhí)行文件吸奴。把多個(gè)目標(biāo)文件合并到一起,在合并的時(shí)候可以對(duì)其內(nèi)部符號(hào)對(duì)外暴露的屬性進(jìn)行修改缠局。
什么是符號(hào)则奥?符號(hào)存放在哪里?符號(hào)的概念比較廣甩鳄,可以說(shuō)我們編寫(xiě)的代碼中的函數(shù)逞度,變量等一切皆為符號(hào),他們?cè)贛achO中保存在一個(gè)叫Symbol Table的東西內(nèi)妙啃,除了Symbol Table(符號(hào)表)還有一個(gè)Indirect Symbol Table被稱(chēng)為間接符號(hào)表档泽,它是符號(hào)表的一個(gè)子集俊戳,里面保存了引用的其他庫(kù)的符號(hào),如我們使用使用NSLog()函數(shù)馆匿,但是NSLog()的實(shí)現(xiàn)是在Foundation中抑胎,它就是一個(gè)外部符號(hào),存放在間接符號(hào)表渐北。
符號(hào)有哪些種類(lèi)阿逃?我們可以親自實(shí)操一下,生成一個(gè)MachO文件然后通過(guò)終端去查看或MachOView查看赃蛛,假如我們使用MachOView會(huì)比較麻煩恃锉,所以我們先來(lái)了解一下如何實(shí)現(xiàn)xcode編譯后自動(dòng)實(shí)現(xiàn)在終端輸出我們想要查看的MachO信息,首先了解一個(gè)命令tty呕臂,通過(guò)man tty命令查看return user's terminal name可以知道這個(gè)命令輸出的是當(dāng)前終端的名稱(chēng)破托。既然知道了終端的名稱(chēng),那也就意味著我們可以通過(guò)重定向?qū)⒔Y(jié)束輸出到終端歧蒋。
同樣在xcode run script中我們也可以這么玩土砂,我們寫(xiě)一個(gè)xcode_tty.sh腳本放到工作根目錄,來(lái)讓這個(gè)過(guò)程自動(dòng)化谜洽,Run Script中執(zhí)行
sh $SRCROOT/xcode_tty.sh "${BUILT_PRODUCTS_DIR}/*" "/dev/ttys000"
#xcode_tty.sh腳本內(nèi)容
#!/bin/sh
EchoError() {
if [[ -n "$2" ]]; then
echo "$@" 1>&2>$2
else
echo "$@" 1>&2
fi
}
if [[ ! -e "$2" ]]; then
EchoError "===ERROR: Not Config tty to output."
exit -1
fi
#查看__TEXT
objdump --macho -d $1 1>$2
#查看mach-header
#objdump --macho --private-headers $1 1>$2
#查看符號(hào)類(lèi)型
#`objdump --macho -syms $1 1>$2`
#查看導(dǎo)出符號(hào)表
#objdump --macho --exports-trie $1 1>$2
#查看間接符號(hào)表
#objdump --macho --indirect-symbols $1 1>$2
使用查看符號(hào)類(lèi)型的命令輸出結(jié)果中小寫(xiě)l代表本地符號(hào)萝映,小寫(xiě)的g代表是全局符號(hào),在main.m文件中定義幾個(gè)變量運(yùn)行 -syms命令阐虚,可以看出我們定義的變量是全局變量序臂,而加了static之后就變成了本地變量,并且可以看出全局符號(hào)中未初始化的符號(hào)存放在__common段敌呈,已初始化的存放在__data段:
int cpy_inta = 10;
int cpy_intaa;
static int cpy_intb = 10;
static int cpy_inbb;
d *UND* _cpy_inta
0000000000000000 l d *UND* _cpy_inta
0000000000000000 l d *UND* _cpy_intaa
0000000100008018 g O __DATA,__data _cpy_inta
0000000100008040 g O __DATA,__common _cpy_intaa
使用objdump --macho --exports-trie $1 1>$2
命令可以查看所有的導(dǎo)出符號(hào)贸宏,也就說(shuō),符號(hào)還也可以分導(dǎo)入符號(hào)和導(dǎo)出符號(hào)磕洪,其實(shí)導(dǎo)出符號(hào)也就是對(duì)應(yīng)著全局符號(hào),也就是說(shuō)我們的全局符號(hào)是可以被別人使用的诫龙,但是全局符號(hào)并不一定全是導(dǎo)出符號(hào)析显,因?yàn)槲覀兊逆溄悠骺梢钥刂扑欠窨梢詫?dǎo)出。間接符號(hào)表保存著當(dāng)前可執(zhí)行文件使用的其他的庫(kù)的符號(hào)签赃,全局符號(hào)可以變成導(dǎo)出符號(hào)給別人使用谷异,因此這個(gè)間接符號(hào)表是不能被刪除,也就意味著所有的全局符號(hào)不能被刪除锦聊,我們的OC方法默認(rèn)都是全局符號(hào)歹嘹,因此我們沒(méi)有使用的OC類(lèi)要進(jìn)行刪除,不然還會(huì)占用空間孔庭。其實(shí)鏈接器也提供了一個(gè)參數(shù)可以控制符號(hào)不被導(dǎo)出OTHER_LGFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker ${符號(hào)}
尺上。追求極致的人可以通過(guò)鏈接器的此特性將使用的全局符號(hào)不需要被外部使用的全部設(shè)置為不導(dǎo)出材蛛,從而減小應(yīng)用包的體積。-unexported_symbol_list
可以指定一個(gè)文件進(jìn)行批量處理怎抛。OTHER_LGFLAGS=$(inherited) -Xlinker -map -Xlinker 文件路徑
可以輸出當(dāng)前可執(zhí)行文件使用的符號(hào)情況卑吭。
還有一個(gè)符號(hào)類(lèi)型叫Weak Symbol,Weak defintion Symbol表示弱定義符號(hào)马绝,如果鏈接器找到了另外一個(gè)非弱定義的符號(hào)豆赏,那么此弱定義符號(hào)將會(huì)被忽略,也就是說(shuō)它就是個(gè)備胎富稻。Weak Reference Symbol表示弱引用掷邦,如果鏈接器找不到該符號(hào)的定義,那么會(huì)將它置為0椭赋,使用if(弱引用) 相當(dāng)于if(0)抚岗。
//弱定義示例
void weakDefineFunction(void) __attribute__((weak));
//弱引用示例
void weakImportFunction(void) __attribute__((weak_import));
- 弱定義并不會(huì)影響它的全局屬性和導(dǎo)出(如果將弱定義隱藏,它將變成一個(gè)本地符號(hào))纹份,它的好處就是在別人導(dǎo)入它之后可以進(jìn)行重寫(xiě)實(shí)現(xiàn)苟跪。如果多個(gè)文件中都寫(xiě)了弱定義的實(shí)現(xiàn)方法,那么將會(huì)按照順序查找到第一個(gè)非弱定義的即停止蔓涧。
- 弱引用可以不實(shí)現(xiàn)件已,那么它在編譯時(shí)并不會(huì)報(bào)錯(cuò)。但是在鏈接時(shí)會(huì)產(chǎn)生報(bào)錯(cuò)undefined symbol元暴,可以通過(guò)鏈接器參數(shù)-U來(lái)告訴鏈接器它沒(méi)有定義篷扩,需要運(yùn)行時(shí)動(dòng)態(tài)查找,示例
OTHER_LGFLAGS=$(inherited) -Xlinker -U -Xlinker _weakImportFunction
茉盏。另外鏈接器還有一個(gè)-undefined
的參數(shù)鉴未,它默認(rèn)指定未定義時(shí)error,還有warning鸠姨,dynamic_lookup铜秆,假如我們將找不到的符號(hào)全部聲明成dynamic_lookup
那么程序?qū)⒃僖膊粫?huì)報(bào)錯(cuò)了,全部變成了動(dòng)態(tài)查找讶迁,當(dāng)然在正常開(kāi)發(fā)中千萬(wàn)不要這么亂搞连茧。在運(yùn)行時(shí)如果找不到定義它就是0。
Swift符號(hào)與OC不太一樣巍糯,swift是一種靜態(tài)語(yǔ)言啸驯,它的符號(hào)類(lèi)型根據(jù)關(guān)鍵字public/private來(lái)區(qū)分,使用public則是全局符號(hào)祟峦,使用private可以變成本地符號(hào)罚斗,因此swift中的關(guān)鍵字一定要合理的使用。
另外還有一種re-export符號(hào)宅楞,它是將引用的外部符號(hào)重新導(dǎo)出针姿。
了解了符號(hào)之后袱吆,我們才能更好的去理解Strip剝離符號(hào)。xcode提供的Strip Style有三種分別是All Symbol搓幌、Non-Global Symbol杆故、Debugging Symbol,究竟該如何使用溉愁?
- 對(duì)于動(dòng)態(tài)庫(kù)处铛,因?yàn)槭且獙?dǎo)出給別人使用,所以它的所有的全局符號(hào)都不能被脫掉拐揭,使用
Non-Global Symbol
撤蟆。 - 對(duì)于靜態(tài)庫(kù),它是.o文件的合集堂污,它的符號(hào)全部都放在重定位符號(hào)表中家肯,在鏈接時(shí)需要使用,因此它不能被脫掉盟猖,只有調(diào)試符號(hào)才能被脫掉讨衣,使用
Debugging Symbol
。 - 對(duì)于App式镐,它根本不需要導(dǎo)出給別人使用反镇,因此不管是本地符號(hào)、全局符號(hào)還有弱定義符號(hào)都可以干掉娘汞,所以在app上架是通常使用
All Symbol
歹茶,另外即使是All Symbol也不會(huì)脫掉間接符號(hào)表中的符號(hào)。
app使用靜態(tài)庫(kù)的符號(hào)最終會(huì)合并放到app內(nèi)你弦,也就意味著靜態(tài)庫(kù)的符號(hào)會(huì)變成了app內(nèi)的本地惊豺、全局、導(dǎo)出等符號(hào)禽作,因此最終也會(huì)被干掉尸昧,總的符號(hào)會(huì)變少。而動(dòng)態(tài)庫(kù)則只能增加符號(hào)旷偿,總的符號(hào)會(huì)變多彻磁。符號(hào)多寡也就影響包的體積。
Debugging Symbol:靜態(tài)庫(kù)的調(diào)試符號(hào)是存放在MachO文件的一個(gè)__DWARF的Segment中狸捅,DWARF全稱(chēng)Debugging With Attributed RecordFormats。符號(hào)剝離的過(guò)程為:MachO文件解析成模型Object累提,然后通過(guò)遍歷LoadCommands尘喝,找到Segname==__DWARF的LoadCommand,移除里面的section斋陪,再?gòu)姆?hào)表中移除Symbol朽褪,最后將修改后的模型Object重新寫(xiě)入MachO置吓,因此說(shuō)我們的MachO文件是一個(gè)可讀寫(xiě)的文件。Strip就是修改MachO文件缔赠。而動(dòng)態(tài)庫(kù)它是沒(méi)有__DWARF的段衍锚,它是遍歷符號(hào)表,將n_type包含N_STAB(0xe0)的全部刪除嗤堰,N_STAB(0xe0)代表調(diào)試符號(hào)戴质。
All Symbol:遍歷符號(hào),只要不是間接符號(hào)表中的符號(hào)踢匣,全部刪除
Non-Global Symbols:遍歷符號(hào)表告匠,判斷n_type!= N_EXT都可以刪除,N_EXT代表外部符號(hào)
Dead code Strip是鏈接器的一個(gè)參數(shù)离唬,它是用來(lái)剝離死代碼后专,將沒(méi)有用到的函數(shù)和數(shù)據(jù)干掉,Xcode中默認(rèn)是YES输莺。