iOS-开发进阶03:链接与Symbol(下)

iOS 开发进阶 文章汇总

目录


一、Mach-O文件格式

Mach-O中文件格式部分如下:

  • Mach Header的最开始是 Magic Number,表示这是一个 Mach-O 文件,除此之外还包含一些Flags,这些flags 会影响 Mach-O 的解析。
  • Mach-O中的Load Command __TEXT 中记录了代码的大小、第一行代码的起始位置,dyld根据这些信息就能读取到__TEXT代码段中的代码。由于Mach-O中都是二进制数据,因此dyld根据结构体内存对齐规则逐个读取到Load Command
  • Load Command LC_MAIN 中保存了入口函数,默认为main,也可以修改入口函数。
  • Load Command LC_LOAD_DYLIB中保存了加载的动态库
  • Load Command LC_SYMTAB中保存符号表的位置和信息
1、使用脚本命令查看Mach Header

参照上篇文章中在xcconfig文件中定义shell脚本的参数如下:

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脚本参数如下:

MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(FULL_PRODUCT_NAME)/$(PRODUCT_NAME)
CMD = objdump --macho -d ${MACHO_PATH}
TTY = /dev/ttys001

注释main函数中的代码并设置只编译main.m文件,编译后可以看到控制台的输出如下:

通过上面的操作我们可以发现Mach-O是可读的二进制数据,同时Mach-O也是可写的,签名之前之后都可以修改Mach-O,就像很多破解软件,修改签名后的Mach-O再次签名就可以了。

二、编译链接过程

编译器编译过程中主要做了一些工作:

  • 把能变成汇编的代码尽量变成汇编代码
  • 把各种符号进行归类,外部导入符号(NSLog...)放到重定位符号表
  • .o文件链接-->多个目标文件的合并、符号表合并成一张表-->生成可执行文件exec

因此链接的过程就是处理目标文件符号的过程

链接的本质就是把多个目标文件组合成一个文件

三、C语言符号

main.m文件中准备如下代码:

#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;// 静态变量
static int static_uninit_value;

int main(int argc, char * argv[]) {
    static_uninit_value = 10;
    NSLog(@"%d", static_init_value);
}

全局变量的静态变量的主要区别:全局性全局变量加上static后就变成了本地变量

查看当前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:全局符号
其中符号按照功能可做如下区分:

Type 说明
f File
F Function
O Data
d Debug
*ABS* Absolute
*COM* Common
*UND* 未定义

xcconfig文件中添加如下参数脱去调试符号

OTHER_LDFLAGS = $(inherited) -Xlinker -S


四、导入符号与导出符号

main.m函数中使用了NSLog函数,那么NSLog就是导入符号Foundation框架中导出了NSLog符号。

1、查看导出符号的命令如下:
objdump --macho -exports-trie ${MACHO_PATH}

可以看到导出符号就是上面对应的全局符号,但是导出符号不一定都是全局符号,可以通过链接器来控制。

由于NSLog函数是在动态库中,因此也存在于间接符号表中。

2、查看间接符号表的命令如下:
objdump --macho -indirect-symbols ${MACHO_PATH}


总结
  • 全局符号可以变成导出符号给外界使用
  • 由于动态库的符号存在间接符号表中,因此strip不能剥离全局符号
3、OC定的类不管有没有在头文件中暴露默认都是导出符号

Build Phases-->Compile Source中添加ViewController.m文件参与编译

因此如果是OC定义的动态库需要减少体积就需要把尽可能多的符号变成不导出符号

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: 表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。

  • Weak def int ion Symbol: 表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义符号,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。

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文件中添加如下参数就可以告诉编译器不检查弱引用符号,dyld运行起来的时候会自动寻找相应的符号:

OTHER_LDFLAGS = $(inherited) -Xlinker -U -Xlinker _weak_import_function
2、弱定义代码:
// 弱定义:(全局导出符号)同一作用域还可以申明同名的函数
void weak_function(void)  __attribute__((weak));
// 弱定义符号标记为隐藏:则变成本地符号
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详解

  • 对于动态库,我们可以剥离除间接符号表中符号之外的所有符号
  • 对于静态库,静态库是众多.o文件的合集,存在重定位符号表,只能剥离调试符号
strip剥离.o/静态库的调试符号(调试符号放到__DWARF段中)过程如下:
动态库/可执行文件剥离调试符号过程如下:
strip All Symbols过程如下:
strip Non-Global Symbols过程如下:

在LLVM项目中调试strip命令

参照上篇文章中在LLVM项目中调试nm命令的流程在在LLVM项目中调试strip命令。

LLVM项目中可以看到llvm-stripTarget没有源文件,只是执行了shell脚本。要想调试llvm-strip源码还需要做以下操作:

1、复制llvm-objcopy并重命名为strip
2、进入llvm-stripTarget执行的shell脚本中llvm-strip命令的链接地址
/Users/ztkj/Projects/LLVM_Projects/llvm-project/build_xcode/Debug/bin/llvm-strip

复制llvm-strip并重命名为strip

3、进入源码,并在main函数中打下断点
4、new Scheme...-->Target选择strip,并运行strip Scheme
5、添加参数调试strip命令(将可执行文件的路径添加到启动参数中)
6、运行项目后在控制台加载文件中的断点开始调试

控制台依次执行下面的命令:

// 从文件中读取断点
br read -f /Users/ztkj/Desktop/strip_lldb.m
// 将读取的断点加入到strip组中
br list strip
// 启用加载的断点
br enable strip

strip_lldb.m文件中的断点如下:

[
    {"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"}}}
]
控制台断点相关的命令

br read -f 断点文件路径 读取
br write -f 断点文件路径 写入
br list strip 断点加入到strip分组
br enable strip 开启strip分组的断点


APP使用动态库还是静态库体积更小?静态库,静态库中的符号会合并到APP的符号表中,在strip时会剥离静态库不放到间接符号表中的符号

参考

抖音品质建设 - iOS启动优化之原理篇
今日头条优化实践: iOS 包大小二进制优化,一行代码减少 60 MB 下载大小
抖音品质建设 - iOS 安装包大小优化实践篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容