作为iOS开发人员,经常会碰到符号的问题,确切的说在调试,收集崩溃时,避免不了
乍一看,是个抽象的东西,不像我们处理逻辑业务问题那样,直接依托于语言本身,逻辑漏洞分析
你面对的项目不可能是独立的很小的项目,往往依赖很多库,动态库等等,冲突无可避免,至于如何解决,那么前提你就需要多多少少理解符号的本质了
虽然研究符号主要为了解决符号冲突问题
但在分析符号问题阶段,你会涉及到编译,链接的基本知识,在此博客里,你会复习到这些知识,当然就不会很细节展开了,但是对于iOS开发人员分析来讲,是够用的,这些关联知识需要深入的话,就需要靠自己了
理解符号的种类与作用
按照功能划分
Type | 说明 |
---|---|
f | File |
F | Function |
O | Data |
d | Debug |
'ABS' | Absolute |
'COM' | Common |
'UND' | ? |
按照符号种类划分
Symbol Type | 说明 ①:小写 代表local symbol |
---|---|
U | undefined (未定义) |
A | absolute (绝对符号) |
T① | text section symbol(__TEXT.__text) |
D① | data section symbol(__DATA.__text) |
B① | bss section symbol(__DATA.__bss) |
C | common symbol(只能出现在 MH_OBJECT 类型的 Mach-O 文件中) |
- | debugger symbol table |
S① | 除了上面所述的,存放在其他 section 的内容,例如未初始化的全局变量存放在(__DATA.__common)中 |
I | indirect symbol(符号信息相同,代表同一符号) |
U | 动态共享库总的小写u表示一个未定义引用对同一库中另一个模块中私有外部符号 |
查看符号的两个命令 nm & objdump
objdump 结果更便于阅读
- g: 全局
- l: 静态本地文件
这样执行命令似乎有点繁琐
- 需要进入终端输入命令
- 参数:可执行二进制文件的路径
简化一点
build 就会执行添加的脚本了, 输出信息就会出现在编译信息里了
还有个问题,每次查看的时候需要时不时该脚本,改完之后还需要 在编译信息列表里选中当前编译的时间版本
这还是有点繁琐
更好的办法,把命令写入配置文件,在文件里可以随时修改命令,并且把命令执行的结果 直接输出到终端
也就是利用xcconfig配置文件配置相关变量,结合 script
编译,直接输出到终端
SO OSO 属于调试符号,但这种符号是我们不需要的,更好的方式当然是终端输出信息不包含这些了
脱去(strip)不需要的符号
看下配置
终端输出的符号信息并没有脱去符号,是因为还没配置
xcconfig 文件中配置 OTHER_LDFLAGS = -Xlinker -S 可以脱去干扰的调试符号
-
补充知识
strip命令 执行的时机
strip究竟是在编译.o 还是链接可执行文件 脱去符号呢?
都不是。
而是在
生成exe可执行文件之后,再去对可执行文件里面的符号表进行修改
Xcode中,strip配置 在我们打包ipa包的时候才起作用,默认debug下是不生效的
此时我们把上面 截图里 Strip Debug Symbols During Copy 改为 YES
但我期望的是有条件的strip符号,不然就得 来回切换选择Strip Style 3种方式
- All Symbols
- Non-Global Symbols
- Debugging Symbols
-
更好的strip配置方式
继续回到.xcconfig文件 (后缀全程 - xcode配置文件)
修改手动的繁琐的 build setting 手动设置
换成利用 xccondig 声明好各种条件的配置项 一劳永逸
现在测试下这种方式
终端 - 进入工程根目录 - 执行命令 xcodebuild -showBuildSettings | grep DEPLOY
发现 DEPLOYMENT_POSTPROCESSING = NO
STRIP_STYLE = all
然后在配置文件里添加 DEPLOYMENT_POSTPROCESSING = YES
我们再看下配置
配置已经根据 配置文件里的设置 变更了过来
通过xcode配置文件修改setting 比起 我们直接手动修改灵活了很多,而且具体做了哪些配置 也比较明确
有了这些铺垫,接下来我们就可以分析符号本质及冲突问题了
理解strip命令的实际作用
一个全局函数
在底层被解析出来,也是一个全局符号
那么全局符号的作用域有多大 当前文件?当前app?当前进程?答案是当前进程
这里可以验证一下
- 静态库里只声明一个全局函数,没有实现
- app里声明全局函数
你会发现,静态库里能访问到 app里的全局符号,验证了全局符号的作用域应该是当前进程
static 函数 定义在什么地方,它的作用域就作用在什么范围 也就是文件范围
解决符号冲突
把之前 静态库里的global_func实现注释打开
编译app报错,提示 duplicate symbol 符号冲突了
如果 把静态库 改为 动态库 编译正常
动态库跟 app包含相同的全局符号,但是没有出现冲突
可能困惑了,为什么动态库反而没有冲突呢,如何理解?
既然全局符号的作用域为当前进程,为什么不报错呢
因为dyld在查找符号的时候,链接器ld引入了一个规则,ld在把.o文件链接生成可执行文件(或动态库)时,根据两级命名空间来查找符号
-
dyld访问符号,先访问符号所在的Mach-o文件,再去访问mach-o里的符号
也就是 app.global_func 与 dylib.global_func,不会冲突
而静态库 .o文件的合集,只经历了汇编器,跟app的.o合并在一起,最后生成app可执行文件, 最后都放到了app里,可以理解为前缀都是app,所以当然会冲突了, 重复定义的symbol
- 切回静态库,我们把app中使用静态库的代码注释
静态库与app都定义了相同的全局函数,但是没有冲突
为什么不使用静态库代码的时候,不会冲突呢?
连接器ld 在链接静态库的时候,专门针对静态库提出一个规则
ld会去判断app使用的静态库的代码,发现如果没有用到静态库代码的时候,就不会把静态库代码加载进app
编译时,并没有把静态库代码链接进去,同名的函数当然不会冲突
好处就是可以减小app的大小
-
坏处:对于开发中的分类,分类是运行时才动态创建加载的
如果判断到app没有使用静态库里的代码,分类的代码就会被优化掉了,就会产生问题
针对这个问题,ld提供了一个规则,通过参数控制 -ObjC
- OTHER_LDFLAGS = $(inherited) -ObjC
回归到链接器ld本身
man ld
根据链接器手册 看下 静态库 动态库的描述
- 静态库 .o文件的集合,.o文件里包含全局符号
- 动态库 最终链接的镜像
继续在ld手册里搜索 ObjC
4种load形式
- all_load 所有静态库里的所有内容全部链接 只要能编译成macho,就全部链接
- ObjC 静态库里的OC class 或分类 链接
- force_load 多个静态库,只需要指定一个静态库里的所有内容链接到当前app
- load_hidden
- 当前app链接静态库,静态库里正好有一个全局符号,本来在静态库里是全局符号,链接到app后依然是全局符号
- 导致问题:本来不想暴露给外边使用,经过app一链接,直接暴露给所有外部使用了
- load_hidden 在链接到app之后,就可以把所有静态库里的全局符号变成本地符号,这样就可以隐藏符号,同时可以减小app体积
避免冲突分析
我们可以修改全局符号 增加前缀
但是如果我们拿到的是别人打包好的静态库,我们根本不可能修改源文件
很多情况下,静态库里包含分类,这个时候 参数 -ObjC 是一定要使用的
那么通过链接器参数控制的方式也不可取
- 只能考虑其他的方式 把静态库里产生冲突的符号给修改掉 llvm-objcopy
llvm源码编译工具 - llvm-objcopy
为了不至于显得突兀,稍微简单说下 工具的编译
因为之前利用llvm做过一些插件,所以我本地已经有编译过的llvm xcode工程
找到llvm源码 llvm-objcopy 路径
在相同目录下 打开 CMakeLists.txt
添加 add_llvm_tool_subdirectory(llvm-objcopy)
在编译的xcode工程目录下 执行命令 cmake -G Xcode ../llvm-project/llvm
编译 llvm-objcopy
源码很大,编译后20多个G,而且下载源码不是轻松的事情,以后有机会,关于llvm源码的编译单独更新一篇博客出来
然后执行编译后的工具命令
- llvm-objcopy --redefine-sym _global_func=_libIFLTestStaticLib_global_func libIFLTestStaticLib.a
结果出错 unsupported load command
源码做了些小调整,可直接下载使用 修改编译后的llvm-objcopy工具
使用 llvm-objcopy 之前,静态库 libIFLTestStaticLib.a 有个全局符号 _global_func
使用 llvm-objcopy 之后,全局符号 _global_func 被修改 为 _libIFLTestStaticLib_global_func
工程集成调整过的静态库编译通过,并没有发生冲突,静态库中的同名全局函数正常执行
直接通过符号调用