ios编译与链接之符号

2,721 阅读6分钟

Mach-O

前序文章

  1. 多环境配置
  2. 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} //查看重定位符号表

image.png 为了进一步理解Mach-o文件的结构,

编译的过程

编译器将源代码.cpp .mm .m编译成.o文件(目标文件),连接器再链接所有.o文件生成.out文件(可执行文件). 接下来就感受下整个编译的过程. 有如下源文件main.m image.png 使用如下命令生成.o文件

clang -c main.m

执行上述命令之后,同级目录下得到一个main.o文件. 可以通过命令file来看下这个文件的性质.

image.png 可以看到,这个main.o文件是基于x86_64指令集下的Mach-O文件,当然也可以通过objdump --macho来查看各个段的内容. 生成的目标文件.o的过程中,连接器并没有被执行,并且目标文件不会包含UNix程序在被执行和装载时所必须的包含信息。怎么理解上面这段话呢?

  • 编译成.o文件的过程,把能够变成机器码的变成机器码
  • 对所有符号进行归类,把外部符号表放到重定位符号表

生成.o文件之后,执行objdump --macho --reloc main.o image.png 会看到,有很多重定位符号. 下图给出了每个字段对应含义 image.png 给了我们一个启发,我们可以通过检测.o文件的重定位符号表,来知道某个API的使用情况.

符号

符号表:

  • Symbol table: 保存符号的一张表, 保存了符号的名称以及符号的地址
  • String table: 保存符号名称
  • Indirect Symbol table: 间接符号表,保存使用的外部符号

全局符号和本地符号

在main.m中定义如下变量 image.png build 之后执行 objdump --macho --syms ${MACH_PATH} image.png 我们可以看到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 中输入如下代码 image.png 执行objdump --macho --exports-trie ${MACH_PATH}查看导出符号表 image.png 全局导出符号就一定是全局符号吗?我们可以通过链接器来控制的. 对于一个动态库来说,我们知道是在运行的时候进行加在,那么对于外部使用这个动态库的来说,动态库的作用就是提供符号。之前提到过间接符号表,是保存着当前mach-o文件使用的外部符号表。同样的代码运行objdump --macho --indirect-symbols ${MACH_PATH},查看间接符号表 image.png 可以看到NSLog是一个间接符号,是由Fundation这个动态库提供的.

通过上述分析,可以得到以下结论:

  • 全局符号可以变成导出符号,可以被外界使用
  • strip 符号的时候,是不能删除间接符号表的,且动态库的导出符号也是不能被strip的.

那么对于oc的代码(类...), 符号可见性是什么样的呢?编译添加LGOneObject这个类 image.png 执行objdump --macho --exports-trie ${MACH_PATH}查看导出符号表,如下 image.png 我们发现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}查看导出符号表,如下 image.png 可以发现,导出符号里面已经没有_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文件如下图所示: image.png 这个文件中会列出生成了几个目标文件,以及使用了什么库

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中实现 image.png 然后在main中import WeakSumbol.h这个头文件,并且在main.m 中重定义weak_function()这个函数 image.png 如果在WeakSymbol.h中的weak_function()不是弱定义的,编译就会报错,但是如果是弱定义的,就不会报错.

对于弱应用的符号,在WeakImportSymbol.h中定义wark_import_function(),然后在main.m中使用这个符号(注意此时不要编译WeakImportSymbol.m这个文件),代码如下图 image.png image.png 编译的时候出现编译错误,那么我们可以通过一个-U(undefined)的option告诉连接器,这个符号是动态查找的,不需要找这个符号.

OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function

image.png 此时再编译就不会再报错了。这个的作用是什么呢?在使用动态库的时候,我们可以将整个动态库变成弱引用的,那么我们在使用这个动态库的时候,就不会报找不到库的错误了.

common 符号

  • Common symbol: 在定义时,未初始化的全局符号
  • 连接器设置:
    • -d: 强制定义common symbol
    • -commons: 指定对待common symbol 如何响应 在main.m中输入如下代码,

image.png

此时编译的时候不会报错的,第二个是未定义的的全局符号,是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

注意,只能给外部符号起别名。编译之后,查看导出符号表 image.png 可以看到ML_NSLog作为一个重新导出的符号,放在了导出符号表中。这样做的好处是什么呢?当一个动态库A链接另一个动态库B的时候,如果我们将动态库B重新导出到动态库A,那么使用动态库A的程序,也可以看到动态库B的符号.

swift 符号

当在代码中添加一个swift文件,然后编译,查看导出符号表 image.png 可以看到一下多了好多符号,所以当了解了符号之后,可以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原理

image.png image.png image.png image.png

补充

当我们使用上述命令的时候,对一些命令会比较模糊, 可以通过man命令来查看这个命令的一些描述,以ld为例,在终端输入man ld image.png 发现ld有很多option,可以通过输入/{需要查找的命令}如下图所示 image.png 有可能匹配到很多个结果,那么按 n或者shift + n来向下或者向上查找。