Mach-O
前序文章
- 多环境配置
- Mach-O与XCode编译环境配置
延续之前的文章,本文延续介绍一些
xconfig文件的写法
xconfig
经常在build setting里面见到$(inherited), 这个是继承的意思, 望文生义可以有下面的用法
CMD = objdump --macho --syms ${MACH_PATH}
CMD = $(inherited) | grep LC
最终这个CMD = objdump --macho --syms ${MACH_PATH} | grep LC, 当设置other link flag 的时候,可能会link多个库, 就可以通过这种方式来设置OTHER_LDFLAGS.
查看Mach-O 相关命令
objdump --macho -private-header ${MACH_PATH} // 查看mach-header
otool -l ${MACH_PATH} //查看mach-header
objdump --macho -d ${MACH_PATH} //查看text段
objdump --macho --syms ${MACH_PATH} //查看符号表
objdump --macho --exports-trie ${MACH_PATH} //查看导出符号表
objdump --macho --indirect-symbols ${MACH_PATH} //查看间接符号表
objdump --macho --reloc ${MACH_PATH} //查看重定位符号表
otool -r ${MACH_PATH} //查看重定位符号表
为了进一步理解Mach-o文件的结构,
编译的过程
编译器将源代码.cpp .mm .m编译成.o文件(目标文件),连接器再链接所有.o文件生成.out文件(可执行文件). 接下来就感受下整个编译的过程. 有如下源文件main.m
使用如下命令生成
.o文件
clang -c main.m
执行上述命令之后,同级目录下得到一个main.o文件. 可以通过命令file来看下这个文件的性质.
可以看到,这个
main.o文件是基于x86_64指令集下的Mach-O文件,当然也可以通过objdump --macho来查看各个段的内容.
生成的目标文件.o的过程中,连接器并没有被执行,并且目标文件不会包含UNix程序在被执行和装载时所必须的包含信息。怎么理解上面这段话呢?
- 编译成
.o文件的过程,把能够变成机器码的变成机器码 - 对所有符号进行归类,把外部符号表放到重定位符号表
生成.o文件之后,执行objdump --macho --reloc main.o
会看到,有很多重定位符号. 下图给出了每个字段对应含义
给了我们一个启发,我们可以通过检测
.o文件的重定位符号表,来知道某个API的使用情况.
符号
符号表:
- Symbol table: 保存符号的一张表, 保存了符号的名称以及符号的地址
- String table: 保存符号名称
- Indirect Symbol table: 间接符号表,保存使用的外部符号
全局符号和本地符号
在main.m中定义如下变量
build 之后执行
objdump --macho --syms ${MACH_PATH}
我们可以看到
static开头的变量都变成了本地符号,全局跟本地符号的本质区别就是可见性的区别。
默认一个符号的可见性,是定义成什么就是什么,比如一个全局变量,那就是一个全局符号, 但同时我们可以修改这个符号的可见性
//visibility属性,控制文件导出符号,限制符号可见性
-fvisibility:clang参数
default:用它定义的符号将被导出。
hidden:用它定义的符号将不被导出。
int hidden_y __attribute__ ((visibility("hidden"))) = 1; //隐藏,本地符号
double default_y __attribute__ ((visibility("default"))) = 2;
最好的隐藏全局符号的方式,是将符号变成static的. 当然也可以通过__attribute__ ((visibility("hidden"))) 这种方式隐藏.
导入导出符号
在main.m 中输入如下代码
执行
objdump --macho --exports-trie ${MACH_PATH}查看导出符号表
全局导出符号就一定是全局符号吗?我们可以通过链接器来控制的. 对于一个动态库来说,我们知道是在运行的时候进行加在,那么对于外部使用这个动态库的来说,动态库的作用就是提供符号。之前提到过间接符号表,是保存着当前mach-o文件使用的外部符号表。同样的代码运行
objdump --macho --indirect-symbols ${MACH_PATH},查看间接符号表
可以看到
NSLog是一个间接符号,是由Fundation这个动态库提供的.
通过上述分析,可以得到以下结论:
- 全局符号可以变成导出符号,可以被外界使用
- strip 符号的时候,是不能删除间接符号表的,且动态库的导出符号也是不能被strip的.
那么对于oc的代码(类...), 符号可见性是什么样的呢?编译添加LGOneObject这个类
执行
objdump --macho --exports-trie ${MACH_PATH}查看导出符号表,如下
我们发现oc的代码默认都是导出的,也就是说是一个全局的符号. 所以当我们定义很多类很多全局符号的时候,导出符号表会很大,就会占用很空间。
当定义一个oc的动态库的时候,当一些没必要暴露的符号,我们可以strip掉,可以通过连接器将没必要暴露的符号,变成不导出
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_LGOneObject
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_LGOneObject
在xconfig文件中添加不需要导出的符号, build之后,执行objdump --macho --exports-trie ${MACH_PATH}查看导出符号表,如下
可以发现,导出符号里面已经没有
_OBJC_CLASS_$_LGOneObject和 _OBJC_METACLASS_$_LGOneObject这两个符号了. 那么strip的时候就可以strip掉非全局符号,能够减小动态库的体积。
链接器也可以通过别的参数来查看使用了什么库,以及所有的符号是什么, 在xconfig 文件中添加如下代码
OTHER_LDFLAGS=$(inherited) -Xlinker -S -Xlinker -map -Xlinker /Users/meilinli/Downloads/Symbol.text
build之后,打开symbol.text文件如下图所示:
这个文件中会列出生成了几个目标文件,以及使用了什么库
weak symbol
- Weak reference symbol: 标识此未定义符号是弱引用,如果动态链接器找不到该符号的定义,则将其设置为0,连接器会将此符号设置若链接标志
- Weak definition symbol: 表示此符号为若定义符号,如果静态连接器或者动态连接器为此符号找到另一个(非弱)定义,则弱定义将被忽略,只能将合并部分中的符号标记为弱定义。声明弱定义符号以及弱引用符号的方式如下
void weak_import_function(void) __atrribute__((weak_import))//弱应用
void weak_function(void) __attribute__((weak))弱定义
声明为弱定义符号,并不影响是否导出。
在WeakSymbol.h中声明一个弱定义符号,在WeakSymbol.m中实现
然后在main中import
WeakSumbol.h这个头文件,并且在main.m 中重定义weak_function()这个函数
如果在
WeakSymbol.h中的weak_function()不是弱定义的,编译就会报错,但是如果是弱定义的,就不会报错.
对于弱应用的符号,在WeakImportSymbol.h中定义wark_import_function(),然后在main.m中使用这个符号(注意此时不要编译WeakImportSymbol.m这个文件),代码如下图
编译的时候出现编译错误,那么我们可以通过一个
-U(undefined)的option告诉连接器,这个符号是动态查找的,不需要找这个符号.
OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function
此时再编译就不会再报错了。这个的作用是什么呢?在使用动态库的时候,我们可以将整个动态库变成弱引用的,那么我们在使用这个动态库的时候,就不会报找不到库的错误了.
common 符号
- Common symbol: 在定义时,未初始化的全局符号
- 连接器设置:
- -d: 强制定义common symbol
- -commons: 指定对待common symbol 如何响应
在
main.m中输入如下代码,
此时编译的时候不会报错的,第二个是未定义的的全局符号,是common 符号,common符号的作用是,当找到定义的时候,会将原来未定义的删掉.
re-export(重新导出)
在上面的main.m文件中,使用了Fundation库中的NSLog这个符号,那么我们是否可以让使用当前这个Mach-O文件也使用NSLog这个符号呢?引出一个概念重新导出. 当这个符号被重新导出之后,这个符号就会放到main.m 生成的Mach-O的导出符号表中。可以通过给一个符号起一个别名,来作为导出符号.
OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker ML_NSLog
注意,只能给外部符号起别名。编译之后,查看导出符号表
可以看到
ML_NSLog作为一个重新导出的符号,放在了导出符号表中。这样做的好处是什么呢?当一个动态库A链接另一个动态库B的时候,如果我们将动态库B重新导出到动态库A,那么使用动态库A的程序,也可以看到动态库B的符号.
swift 符号
当在代码中添加一个swift文件,然后编译,查看导出符号表
可以看到一下多了好多符号,所以当了解了符号之后,可以strip符号,达到减小库的大小
strip
- 全局符号-->导出符号, strip 动态库的时候,就可以strip 所有不是全局符号的符号
- APP一般不希望别人用到APP的符号,所以APP可以strip所有本地和全局的符号,只保留间接符号表
- 静态库:静态库 = 目标文件的合集 + 重定位符号表,所以静态库就只能stripe debugging symbols 所以就符号来说,当APP使用静态库的时候,体积会更小,因为当app使用静态库的时候,静态库的符号表都放在app的符号表中,我们在strip的时候,可以strip所有的symbol.
dead code strip
-dead_strip: remove functions and data that are unreachable by the entry point or exported symbols
dead_strip: 剥离没有使用到的非导出的符号.
strip原理
补充
当我们使用上述命令的时候,对一些命令会比较模糊, 可以通过man命令来查看这个命令的一些描述,以ld为例,在终端输入man ld
发现
ld有很多option,可以通过输入/{需要查找的命令}如下图所示
有可能匹配到很多个结果,那么按
n或者shift + n来向下或者向上查找。