iOS-開發(fā)進(jìn)階03:鏈接與Symbol(下)

iOS 開發(fā)進(jìn)階 文章匯總

目錄


一、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-stripTarget沒有源文件,只是執(zhí)行了shell腳本。要想調(diào)試llvm-strip源碼還需要做以下操作:

1、復(fù)制llvm-objcopy并重命名為strip
2、進(jìn)入llvm-stripTarget執(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í)踐篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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