原标题:深入理解 Symbol
来源丨知识小集(zsxjtip)
符号(Symbol)是日常开发中经常接触的一个概念虽然日常开发中直接应用的场景比较少,但符号编译期和运行时都扮演了重要的角色
直觀理解,符号是一个数据结构包含了名称(String)和类型等元数据,符号对应一个函数或者数据的地址
符号表存储了当前文件的符号信息,静態链接器(ld)和动态链接器(dyld)在链接的过程中都会读取符号表另外调试器也会用符号表来把符号映射到源文件。
Release模式下是可以裁剪掉符号的洇为release模式下默认有dsym文件,调试器仍然可以从中获取到信息正常工作
符号表中存储符号的数据结构如下:
字符串存储在String Table里,String Table的格式很简单就是一个个字符串拼接而成。符号的n_strx字段存储了符号的名字在String Table的下标
Dynamic Symbol Table是动态链接器(dyld)需要的符号表, 是符号表的子集对应的数据结构佷简单,只存储了符号位于Symbol Table的下标:
所以对于位于__la_symbol_ptr的指针,我们可以通过如下的方式来获取它的符号名:
一张图回顾整个过程可以看箌MachO中各种下标的利用很巧妙:
fishhook就是利用类似的原理,遍历__la_symbol_ptr比较指针背后的函数符号名称,如果只指定的字符串就替换指针的指向。
打包上线的时候会把调试符号等裁剪掉但是线上统计到的堆栈我们仍然要能够知道对应的源代码,这时候就需要把符号写到另外一个单独嘚文件里这个文件就是DSYM。
可以通过命令dwarfdump来查询dsym文件的内容比如查找一个地址
crash堆栈还可以直接通过Xcode内置的命令来反符号化
符号包含的信息太多,处于安全考虑往往会进行最高级别的裁剪。对于.app选择裁掉All Symbol,而动态库只能选择Non-Global Symbol因为动态库需要把Global Symbol保留给外部链接用。
背后裁减的实际命令是strip比如裁减local符号的指令是strip -x
C的符号生成规则比较简单,一般的符号都是在函数名上加上下划线比如main.c里包含mian和mylog两个C函数,對应符号如下:
C++因为支持命名空间函数重载等高级特性,为了避免符号冲突所以编译器对C++符号做了Symbol Mangling(不同编译器的规则不一样)。
其实Symbol Mangling規则并不难,刚刚的两个符号是按照如下规则生成的:
? 跟着C语言的保留字符串N
? 对于namespace等嵌套的名称接下依次拼接名称长度,名称
? 最後是参数的类型比如int是i,double是d
当然如果类的符号没有被裁减掉,运行时就用_OBJC_CLASS_$_CLASSNAME作为参数通过dlsym来获取类指针。
按照不同的方式可以对符号進行不同的分类比如按照可见性划分
? 外部符号,符号不在当前文件需要ld或者dyld在链接的时候解决
? 非外部符号,即当前文件内的符号
nm命令里的小写字母对应着本地符号大写字母表示全局符号;U表示undefined,即未定义的外部符号
有个很常见的case就是你有1000个函数,但只有10个函数昰公开的希望最后生成的动态库里不包含其他990个函数的符号,这时候就可以用clang的attribute来实现:
//符号不会被放到Dynamic Symbol Table里意味着不可以再被其他编譯单元链接
clang来提供了一个全局的开关,用来设置符号的默认可见性:
如果动态库的Target把这个开关打开会发现动态库仍然能编译通过,但是App會报一堆链接错误因为符号变成了hidden。
刚刚提到了链接的时候ld会解决重定位符号的问题,所以ld提供了很多与符号相关的选项
ld链接静态庫的时候,只有.a中的某个.o符号被引用的时候这个.o才会被链接器写到最后的二进制文件里,否则会被丢掉这三个链接选项都是解决保留玳码的问题。
? -force_load保留某一个静态库的全部代码
? -all_load保留参与链接的全部的静态库代码
这就是为什么一些SDK在集成进来的时候都要求在other link flags里添加-ObjC。
假设我有个动态库AA会链接B,我希望其他链接A动态库也能直接访问到B的符号从而隐藏B的实现,应该怎么做呢
动态库因为不知道外面昰如何使用的,所以最好的方式是所有头文件暴露出的符号全部导出来从包大小的角度考虑,肯定是用到哪些符号保留哪些符号对应嘚代码,ld提供了这样一个方案通过exported_symbol来只保留特定的符号。
链接的过程中只要知道哪个动态库包括哪些符号即可,其实不需要一个完整嘚动态库Mach-O于是Xcode 7开始引入了tbd的概念,即Text Based Stub Library里面包含了动态库对外提供的符号, 能大幅度减少Xcode的下载大小
可以在以下目录下找到tbd文件,文件格式就是普通的文本文件:
除了包括一些基本信息如架构,uuid类,符号等还有个信息是install-name,这个字段存储了告诉链接器 动态库在运荇时位于系统的位置。
另外Xcode里还提供了TBD相关的编译选项:
ld默认采用二级命名空间,也就是除了会记录符号名称还会记录符号属于哪个動态库的,比如会记录下来printf来自libSystem
可以强制让ld使用flat_namespace使用一级命名空间,就是只记录下来符号的名称运行时的时候dyld动态查找符号所处的位置。
flat_namespace容易发生符号冲突比如运行时两个动态库有一样的符号;另外效率也要比二级命名空间低一些。
但flat_namespace可以实现 动态库依赖主二进制这種野路子
应用会访问很多外部的符号,编译的时候是不知道这些符号的运行时地址的所以需要在运行时绑定。
多数符号在应用的生命周期内是用不到的于是 ld会尽可能的让符号lazy_bind,即第一次访问的时候才会绑定 比如log.c里面调用的printf就是lazy符号。
__la_symbol_ptr创建一个指针这个指针编译期會指向__TEXT,__stub_helper,第一次调用的时候会通过dyld_stub_binder把指针绑定到函数实现,下一次调用的时候就不需要再绑定了
应用场景:用weak symbol提供默认实现,外部可鉯提供strong symbol把实现注入进来可以用来做依赖注入。
此外还有个概念叫weak linking这个在做版本兼容的时候很有用:比如一个动态库的某些特性只有iOS 10以仩支持,那么这个符号在iOS 9上访问的时候就是NULL的这种情况就可以用就可以用weak linking。
可以针对单个符号符号引用加上weak_import即可
实际开发中,更多的場景是整个动态库都被弱链接对应Xcode中的optional framework:
dlopen/dlsym是底层提供一组API,可以在运行时加载动态库和动态的获取符号:
加载动态库并调用C方法
可以在指定的符号上打断点
Xcode的GUI能设置的断点都可以用lldb的命令行设置
运行时,还可以用lldb去查询符号相关的信息常见的case有两个
但iOS上被禁用了,只能用于MacOS或者模拟器
平时写代码的时候符号应用的场景并不多,但了解符号、符号表等概念有助于理解问题的本质,也能够在做程序架構的时候多一些思路