iOS中的符号冲突(一)- 基础原理

  • 作为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 结果更便于阅读

image.png

image.png

image.png

  • g: 全局
  • l: 静态本地文件

这样执行命令似乎有点繁琐

  • 需要进入终端输入命令
  • 参数:可执行二进制文件的路径

简化一点

image.png

build 就会执行添加的脚本了, 输出信息就会出现在编译信息里了

image.png

还有个问题,每次查看的时候需要时不时该脚本,改完之后还需要 在编译信息列表里选中当前编译的时间版本,

这还是有点繁琐

更好的办法,把命令写入配置文件,在文件里可以随时修改命令,并且把命令执行的结果 直接输出到终端

也就是利用xcconfig配置文件配置相关变量,结合 script

image.png

image.png

编译,直接输出到终端

image.png

SO OSO 属于调试符号,但这种符号是我们不需要的,更好的方式当然是终端输出信息不包含这些了

脱去(strip)不需要的符号

看下配置

image.png

终端输出的符号信息并没有脱去符号,是因为还没配置

xcconfig 文件中配置 OTHER_LDFLAGS = -Xlinker -S 可以脱去干扰的调试符号

image.png

  • 补充知识

    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

    image.png

    发现 DEPLOYMENT_POSTPROCESSING = NO

    STRIP_STYLE = all

    image.png

    然后在配置文件里添加 DEPLOYMENT_POSTPROCESSING = YES

    我们再看下配置

    image.png

    配置已经根据 配置文件里的设置 变更了过来

    通过xcode配置文件修改setting 比起 我们直接手动修改灵活了很多,而且具体做了哪些配置 也比较明确

有了这些铺垫,接下来我们就可以分析符号本质及冲突问题了

理解strip命令的实际作用

一个全局函数

在底层被解析出来,也是一个全局符号

那么全局符号的作用域有多大 当前文件?当前app?当前进程?答案是当前进程

这里可以验证一下

  • 静态库里只声明一个全局函数,没有实现

image.png

  • app里声明全局函数

image.png

image.png

你会发现,静态库里能访问到 app里的全局符号,验证了全局符号的作用域应该是当前进程

static 函数 定义在什么地方,它的作用域就作用在什么范围 也就是文件范围

image.png

脱去干扰符号,你会发现

  • 定义的 global_func 属于全局符号
  • static_func 属于本地符号
  • 定义的OC符号,既有全局的,又有本地的

app当前进程里,除了app之外,还有没有其他的Mach-O格式的东西

还有动态库

那么当前app 当前进程 就不太好区分了

此时我们把 STRIP_STYLE 设置为all,脱去所有符号,但是 结果依然存在像 _NSLog 这样的符号

_NSLog 属于 Foundation 动态库

app运行时候,才会链接动态库,

所以在编译阶段,把整个的使用动态库的符号 保存下来

那么为什么编译阶段,使用的其他的动态库的符号,没有被脱掉?

  • 当我们在函数里调用另外一个函数时

    实际上执行的是汇编, 通过汇编直接找到调用函数的入口地址就可以了

    这个过程中不需要用到符号,没有符号也可以调用

    符号最大的作用 提供 可视化 使程序猿知道调用的地址 是什么名称

    但是 我们使用的动态库的符号 只有在运行时 才去找到动态库符号的真正地址,编译阶段不关注具体地址 所以编译时需要先保存下来 不能脱掉

    像这种使用了动态库的符号,我们放在另一个符号表里存储,这个符号表 叫 间接符号表, 这里的符号是我们不能脱掉的 运行的时候还要指望 间接符号去动态库里找到符号的地址

  • 根据以上分析

    • Non-Global Symbols non-global 脱去非全局符号, 应该是提供给哪种类型的mach-o使用

      当然是提供给动态库来使用,动态库里提供给外界的所有符号都是 全局符号,动态库本地符号脱掉是为了减小动态库的大小

    • Debugging Symbols - debugging 是给静态库来使用

      静态库是.o的合集,.o经过下一步 链接器ld 的操作,才会变成可执行文件(动态库)

      编译器 经过 汇编器 生成.o时,汇编器会把符号都保存下来,但是没有给符号分配虚拟内存地址,如果脱掉了,以后经过链接器链接动态库的时候,就没办法根据符号找虚拟内存地址了

      所以静态库只能脱 调试符号

    • All Symbols - all 给 app使用

      all 并不是所有符号都脱掉

      而是除了动态库符号(也就是动态库暴露出来的全局符号),其他的符号都可以脱掉

      其他的符号在app运行期过程中 只是地址

  • 动态库也能访问 app的全局符号

    全局符号作用域是 整个进程

    动态库能访问到 app全局符号,因为动态库也在当前app进程里

    • dyld把app 动态库 加载到进程之后,当发现未定义的符号之后,会在当前进程里挨个扫描app 和 动态库,查找有没有app或动态库拥有这个符号,直到找到为止

解决符号冲突

把之前 静态库里的global_func实现注释打开

编译app报错,提示 duplicate symbol 符号冲突了

如果 把静态库 改为 动态库 编译正常

image.png

动态库跟 app包含相同的全局符号,但是没有出现冲突

可能困惑了,为什么动态库反而没有冲突呢,如何理解?

既然全局符号的作用域为当前进程,为什么不报错呢

因为dyld在查找符号的时候,链接器ld引入了一个规则,ld在把.o文件链接生成可执行文件(或动态库)时,根据两级命名空间来查找符号

  • dyld访问符号,先访问符号所在的Mach-o文件,再去访问mach-o里的符号

    也就是 app.global_func 与 dylib.global_func,不会冲突

    而静态库 .o文件的合集,只经历了汇编器,跟app的.o合并在一起,最后生成app可执行文件, 最后都放到了app里,可以理解为前缀都是app,所以当然会冲突了, 重复定义的symbol

  • 切回静态库,我们把app中使用静态库的代码注释

image.png

静态库与app都定义了相同的全局函数,但是没有冲突

为什么不使用静态库代码的时候,不会冲突呢?

连接器ld 在链接静态库的时候,专门针对静态库提出一个规则

  • ld会去判断app使用的静态库的代码,发现如果没有用到静态库代码的时候,就不会把静态库代码加载进app

  • 编译时,并没有把静态库代码链接进去,同名的函数当然不会冲突

  • 好处就是可以减小app的大小

  • 坏处:对于开发中的分类,分类是运行时才动态创建加载的

    如果判断到app没有使用静态库里的代码,分类的代码就会被优化掉了,就会产生问题

    针对这个问题,ld提供了一个规则,通过参数控制 -ObjC

    • OTHER_LDFLAGS = $(inherited) -ObjC

回归到链接器ld本身

man ld

image.png

根据链接器手册 看下 静态库 动态库的描述

  • 静态库 .o文件的集合,.o文件里包含全局符号
  • 动态库 最终链接的镜像

继续在ld手册里搜索 ObjC

4种load形式

image.png

  • 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工程

image.png

找到llvm源码 llvm-objcopy 路径

image.png

在相同目录下 打开 CMakeLists.txt

image.png

添加 add_llvm_tool_subdirectory(llvm-objcopy)

image.png

在编译的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

image.png

使用 llvm-objcopy 之后,全局符号 _global_func 被修改 为 _libIFLTestStaticLib_global_func

image.png

工程集成调整过的静态库编译通过,并没有发生冲突,静态库中的同名全局函数正常执行

image.png

直接通过符号调用

image.png

猜你喜欢

转载自juejin.im/post/7123244073565028359