本文基于大量猜测,如有错误,请指出 👻

0x01 起因

前一段时间给公司的项目升级 CocoaPods 1.5,打算把所有的 Pods 由 Dynamic 变为 Static 集成。在集成完毕调试的时候,到处乱点触发了一次 Crash,堆栈大概长这样,

大概的过程是,ZipArchive 在内部调用 fill_fopen64_filefunc 这个 C 方法的时候,跳到了 Instabug 的同名函数实现中,然后就发生了 crash

刚看到这个堆栈的时候,我也是一脸懵逼,在一个 Framework 内部调用方法,怎么就调到另一个 Framework 中的同名方法了呢?难不成是静态库导致的?

0x02 尝试

抱着试一试的心态,我创建了一个 Demo 工程,以 Development Pods 的形式依赖 A、B 两个 Pods(use_framework!),

调用以下两个方法,

1
2
testA();
testB();

在都以 Dynamic Library(默认情况下)集成 A,B 的情况下,上面调用输出的结果是,

1
2
AAAAAA
BBBBBBB

嗯,意料之中

使用 CocoaPods 的新 feature,以 Static Library 的形式依赖 A 与 B,再看看上述调用的结果,

1
2
BBBBBBB
BBBBBBB

因缺思厅,看来复现了我们一开始遇到的问题

0x03 原因

动态库和静态库的一个明显的区别是,会不会被集成到最后的可执行文件中去。

静态库

在 link 静态库时候,linker 会把它需要的东西复制到可执行文件中,用 nm 查看以依赖 Static Library 集成的 binary,

可以发现,在 Static Library 中的函数符号出现在了最后的 binary 中(T 代表全局代码段符号),但由于这两个函数的名字都是 test,最终只有一个函数实现被合并进去了。

这个 test 的实现为什么会指向 B 呢?通过修改 OTHER_LDFLAGS 多次实验后发现,这里的实现指向跟 link 的顺序有关,我猜测,如果是后 link B,那么 linker 在复制 B 中符号的时候会把已经复制过的 A 的符号覆盖,导致 A 的实现就不见了。如果后 link A,那同理,B 的实现会被覆盖。

动态库

在以动态库集成的情况下,我们用 nm 查看一下 xxx.app/xxxxxx.app/Frameworks/A.framework/A 以及 xxx.app/Frameworks/B.framework/B

可以发现,在最终 binary 中,并未包含 A 或 B 中的任何符号(U 代表未定义符号),真正的符号存在于它们各自的 framework 中,即使 C 函数名重复也不会影响真正的调用。

实际上公司的项目的场景比我上面介绍的还要复杂一些,Instabug 和 ZipArchive 都是用 Pods 集成的,还使用了 cocoapods-amimono 插件,在没有升级到 CocoaPods 1.5 的时候没有出现问题。

但当把 cocoapods-amimono 拿掉,使用 CocoaPods 1.5 的 Static Framework 功能,就会发现 fill_fopen64_filefunc 在 main binary 中成了未定义的符号,也就是 ZipArchive 中的函数并未全被拷贝进最终的二进制文件中,而是指向了 Instabug.framework 中。

Instabug 的代码是提前编译好的,linker 没有办法把这部分代码合入 binary。在 ZipArchive 是静态库的情况下,linker 发现了 Instabug 中存在了这个函数,就没有把 ZipArchive 的拷贝进去(都是猜的 =。=

0x04 结论

如果你的库中包含某些 C 的函数或者全局变量,即使没有暴露到外面,也要记得加前缀。对于没有 namespace 的语言,加前缀总是没错的,如果还是有错,那就多加几位 🤣