目錄
-
一、Mach-O文件格式
1、使用腳本命令查看Mach Header、2、使用腳本命令查看__TEXT代碼段 - 二、編譯鏈接過程
- 三、C語言符號
- 四、導(dǎo)入符號與導(dǎo)出符號
- 五、弱引用和弱定義符號
- 六、llvm-strip詳解
- 在LLVM項目中調(diào)試strip命令
一、Mach-O文件格式
Mach-O
中文件格式部分如下:
-
Mach Header
的最開始是Magic Number
,表示這是一個Mach-O
文件,除此之外還包含一些Flags
,這些flags
會影響Mach-O
的解析。 -
Mach-O
中的Load Command __TEXT
中記錄了代碼的大小、第一行代碼的起始位置,dyld
根據(jù)這些信息就能讀取到__TEXT代碼
段中的代碼。由于Mach-O
中都是二進(jìn)制數(shù)據(jù),因此dyld
根據(jù)結(jié)構(gòu)體內(nèi)存對齊規(guī)則逐個讀取到Load Command
。 -
Load Command LC_MAIN
中保存了入口函數(shù),默認(rèn)為main
,也可以修改入口函數(shù)。 -
Load Command LC_LOAD_DYLIB
中保存了加載的動態(tài)庫 -
Load Command LC_SYMTAB
中保存符號表的位置和信息
1、使用腳本命令查看Mach Header
參照上篇文章中在xcconfig
文件中定義shell
腳本的參數(shù)如下:
MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(FULL_PRODUCT_NAME)/$(PRODUCT_NAME)
// otool -h ${MACHO_PATH} 也能查看Mach Header
CMD = objdump --macho -private-header ${MACHO_PATH}
TTY = /dev/ttys001
編譯后可以看到控制臺的輸出如下:
2、使用腳本命令查看__TEXT代碼段
xcconfig
文件中定義的shell
腳本參數(shù)如下:
MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(FULL_PRODUCT_NAME)/$(PRODUCT_NAME)
CMD = objdump --macho -d ${MACHO_PATH}
TTY = /dev/ttys001
注釋main
函數(shù)中的代碼并設(shè)置只編譯main.m
文件,編譯后可以看到控制臺的輸出如下:
通過上面的操作我們可以發(fā)現(xiàn)
Mach-O
是可讀的二進(jìn)制數(shù)據(jù),同時Mach-O
也是可寫的,簽名之前之后都可以修改Mach-O
,就像很多破解軟件,修改簽名后的Mach-O
再次簽名就可以了。
二、編譯鏈接過程
編譯器編譯過程中主要做了一些工作:
- 把能變成匯編的代碼盡量變成匯編代碼
- 把各種符號進(jìn)行歸類,外部導(dǎo)入符號(
NSLog...
)放到重定位符號表 -
.o
文件鏈接-->多個目標(biāo)文件的合并、符號表合并成一張表-->生成可執(zhí)行文件exec
因此鏈接的過程就是處理目標(biāo)文件符號的過程
鏈接的本質(zhì)就是把多個目標(biāo)文件組合成一個文件
三、C語言符號
main.m
文件中準(zhǔn)備如下代碼:
#import <UIKit/UIKit.h>
int global_uninit_value;//全局變量
int global_init_value = 10;
double default_x __attribute__((visibility("hidden"))) ;
static int static_init_value = 9;// 靜態(tài)變量
static int static_uninit_value;
int main(int argc, char * argv[]) {
static_uninit_value = 10;
NSLog(@"%d", static_init_value);
}
全局變量的靜態(tài)變量的主要區(qū)別:全局性,全局變量加上
static
后就變成了本地變量
查看當(dāng)前main.m
文件中的符號情況:
MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(FULL_PRODUCT_NAME)/$(PRODUCT_NAME)
CMD = objdump --macho -syms ${MACHO_PATH}
TTY = /dev/ttys001
l
:本地符號,g
:全局符號
其中符號按照功能可做如下區(qū)分:
Type | 說明 |
---|---|
f | File |
F | Function |
O | Data |
d | Debug |
*ABS* | Absolute |
*COM* | Common |
*UND* | 未定義 |
xcconfig
文件中添加如下參數(shù)脫去調(diào)試符號:
OTHER_LDFLAGS = $(inherited) -Xlinker -S
四、導(dǎo)入符號與導(dǎo)出符號
在main.m
函數(shù)中使用了NSLog
函數(shù),那么NSLog
就是導(dǎo)入符號,Foundation
框架中導(dǎo)出了NSLog
符號。
1、查看導(dǎo)出符號的命令如下:
objdump --macho -exports-trie ${MACHO_PATH}
可以看到導(dǎo)出符號就是上面對應(yīng)的全局符號,但是導(dǎo)出符號不一定都是全局符號,可以通過鏈接器來控制。
由于NSLog
函數(shù)是在動態(tài)庫中,因此也存在于間接符號表中。
2、查看間接符號表的命令如下:
objdump --macho -indirect-symbols ${MACHO_PATH}
總結(jié)
- 全局符號可以變成導(dǎo)出符號給外界使用
- 由于動態(tài)庫的符號存在間接符號表中,因此strip不能剝離全局符號
3、OC定的類不管有沒有在頭文件中暴露默認(rèn)都是導(dǎo)出符號
Build Phases-->Compile Source
中添加ViewController.m
文件參與編譯
因此如果是OC定義的動態(tài)庫需要減少體積就需要把盡可能多的符號變成不導(dǎo)出符號
OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_ViewController
OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_ViewController
五、弱引用和弱定義符號
Weak Symbol:
Weak Reference Symbol
: 表示此未定義符號是弱引用
。如果動態(tài)鏈接器找不到該符號的定義,則將其設(shè)置為0。鏈接器會將此符號設(shè)置弱鏈接標(biāo)志。Weak def int ion Symbol
: 表示此符號為弱定義符號
。如果靜態(tài)鏈接器或動態(tài)鏈接器為此符號找到另一個(非弱)定義符號,則弱定義將被忽略。只能將合并部分中的符號標(biāo)記為弱定義。
1、弱引用代碼:
// 弱引用
void weak_import_function(void) __attribute__((weak_import));
//void weak_import_function(void) {
// NSLog(@"weak_import_function");
//}
int main(int argc, char * argv[]) {
if (weak_import_function) {
weak_import_function();
}
}
編譯會報如下錯誤:
Undefined symbols for architecture arm64:
"_weak_import_function", referenced from:
_main in main.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
xcconfig
文件中添加如下參數(shù)就可以告訴編譯器不檢查弱引用符號,dyld
運(yùn)行起來的時候會自動尋找相應(yīng)的符號:
OTHER_LDFLAGS = $(inherited) -Xlinker -U -Xlinker _weak_import_function
2、弱定義代碼:
// 弱定義:(全局導(dǎo)出符號)同一作用域還可以申明同名的函數(shù)
void weak_function(void) __attribute__((weak));
// 弱定義符號標(biāo)記為隱藏:則變成本地符號
void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));
void weak_function(void) {
NSLog(@"weak_function");
}
void weak_hidden_function(void) {
NSLog(@"weak_hidden_function");
}
六、llvm-strip詳解
- 對于動態(tài)庫,我們可以剝離除間接符號表中符號之外的所有符號
- 對于靜態(tài)庫,靜態(tài)庫是眾多
.o
文件的合集,存在重定位符號表,只能剝離調(diào)試符號
strip剝離.o/靜態(tài)庫的調(diào)試符號(調(diào)試符號放到__DWARF段中)過程如下:
動態(tài)庫/可執(zhí)行文件剝離調(diào)試符號過程如下:
strip All Symbols過程如下:
strip Non-Global Symbols過程如下:
在LLVM項目中調(diào)試strip命令
參照上篇文章中在LLVM
項目中調(diào)試nm
命令的流程在在LLVM
項目中調(diào)試strip
命令。
從LLVM
項目中可以看到llvm-strip
Target沒有源文件,只是執(zhí)行了shell
腳本。要想調(diào)試llvm-strip
源碼還需要做以下操作:
1、復(fù)制llvm-objcopy并重命名為strip
2、進(jìn)入llvm-strip
Target執(zhí)行的shell
腳本中llvm-strip
命令的鏈接地址
/Users/ztkj/Projects/LLVM_Projects/llvm-project/build_xcode/Debug/bin/llvm-strip
復(fù)制llvm-strip
并重命名為strip
3、進(jìn)入源碼,并在main
函數(shù)中打下斷點(diǎn)
4、new Scheme...-->Target選擇strip,并運(yùn)行strip Scheme
5、添加參數(shù)調(diào)試strip命令(將可執(zhí)行文件的路徑添加到啟動參數(shù)中)
6、運(yùn)行項目后在控制臺加載文件中的斷點(diǎn)開始調(diào)試
控制臺依次執(zhí)行下面的命令:
// 從文件中讀取斷點(diǎn)
br read -f /Users/ztkj/Desktop/strip_lldb.m
// 將讀取的斷點(diǎn)加入到strip組中
br list strip
// 啟用加載的斷點(diǎn)
br enable strip
strip_lldb.m
文件中的斷點(diǎn)如下:
[
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["removeSections"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["MachOObjcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["handleArgs"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["MachOObjcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["executeObjcopyOnBinary"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["MachOObjcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["markSymbols"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["MachOObjcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["main"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["llvm-objcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["getDriverConfig"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["llvm-objcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["executeObjcopy"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["llvm-objcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["parseStripOptions"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["llvm-objcopy.cpp"]},"Type":"ModulesAndCU"}}},
{"Breakpoint":{"BKPTOptions":{"AutoContinue":false,"ConditionText":"","EnabledState":false,"IgnoreCount":0,"OneShotState":false},"BKPTResolver":{"Options":{"NameMask":[56],"Offset":0,"SkipPrologue":true,"SymbolNames":["MachOWriter::write"]},"Type":"SymbolName"},"Hardware":false,"Names":["strip"],"SearchFilter":{"Options":{"CUList":["MachOWriter.cpp"]},"Type":"ModulesAndCU"}}}
]
控制臺斷點(diǎn)相關(guān)的命令
br read -f 斷點(diǎn)文件路徑 讀取
br write -f 斷點(diǎn)文件路徑 寫入
br list strip 斷點(diǎn)加入到strip分組
br enable strip 開啟strip分組的斷點(diǎn)
APP
使用動態(tài)庫還是靜態(tài)庫體積更小?靜態(tài)庫,靜態(tài)庫中的符號會合并到APP
的符號表中,在strip
時會剝離靜態(tài)庫不放到間接符號表中的符號
參考
抖音品質(zhì)建設(shè) - iOS啟動優(yōu)化之原理篇
今日頭條優(yōu)化實(shí)踐: iOS 包大小二進(jìn)制優(yōu)化,一行代碼減少 60 MB 下載大小
抖音品質(zhì)建設(shè) - iOS 安裝包大小優(yōu)化實(shí)踐篇