目录
-
一、Mach-O文件格式
1、使用脚本命令查看Mach Header、2、使用脚本命令查看__TEXT代码段 - 二、编译链接过程
- 三、C语言符号
- 四、导入符号与导出符号
- 五、弱引用和弱定义符号
- 六、llvm-strip详解
- 在LLVM项目中调试strip命令
一、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-strip
Target没有源文件,只是执行了shell
脚本。要想调试llvm-strip
源码还需要做以下操作:
1、复制llvm-objcopy并重命名为strip
2、进入llvm-strip
Target执行的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 安装包大小优化实践篇