静态库中全局c方法被覆盖导致bug的原因和分析
背景
给客户提供了一个sdk,客户那边安装了sdk后,发现另一个sdk的就会异常.....最后分析来分析去,发现只要添加我们的sdk就会有这样的问题,于是,问题就到了我这了.
结论
先说结论吧,结论就是,我们的sdk中定义的一个全局c方法,覆盖了另一个sdk中的全局c方法,然后,我们两个c方法的作用又不一样,从而导致出了不同的结果,导致问题.
分析
通常这种诡异的问题,调试分析是最头痛的,因为完全不知道问题在哪,直接把对方的sdk和我们sdk的源码加到一个测试工程里来,调试,发现,确实是这样,由于我们这个sdk中的文件数量不多,于是,一个个的删除,定位到是由于一个包含全局c方法的文件中的c方法被对方sdk调用了...
也就是说,我们的c方法覆盖了对方sdk中的同名方法了.
对于同样都是静态库来说(静态framework,.a文件),在编译的时候,是会融合到宿主app的二进制中去的,那么按理说,编译阶段,同名c方法,会被xcode提示出来啊,为什么这里会没有提示呢?
我们随便新建一个sdk,再建立一个宿主app,使用它.
image.png
在demo app中使用这个sdk
image.png
有几个比较有意思的发现:
宿主工程不加-ObjC的时候
主工程和库工程中有重名的c方法的时候,xcode不会提示重复符号,且能够编译运行,至于哪个c方法会被调用,有如下的情况:
如果使用了c方法所在的文件中的类,那么调用的就是静态库中定义的c方法
- (void)viewDidLoad {
[super viewDidLoad];
convertSomething(@"");
CustomAction *act = [CustomAction new];
[act test1];
// Do any additional setup after loading the view.
}
image.png
如果没有使用,那么调用的就是宿主中定义的c方法.
image.png
宿主添加-ObjC的时候,xcode就会报错了,提示符号重复
image.png
这个也是我们通常认为的结果!!
可是,使用方明明是加了 -ObjC的,按照我们的理解,有两个重名的c方法,不是应该报错,提示符号重复么,为什么没有?
然后,让对方自己新建一个demo,添加-ObjC,看看,是不是会报,结果对方告诉我,不会....
心里立马万马奔腾啊,在群里和小伙伴们讨论,群里有人提示可能是弱符号.于是试了下弱符号 attribute((weak))
再编译一下,不报错了
这里插播一下弱符号是啥,有什么作用
弱符号
我们经常在编程中碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。比如我们在目标文件A和目标文件B都定义了一个全局函数/变量,并将它们都初始化,那么链接器将A和B进行链接时会报错.这种在全局中不能有重名的符号,可以称之为 strong symbol(强符号).
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
就如同我们上面的情况
为了解决这样的问题,就引入了弱符号(weak symbol)的概念:用attribute((weak))修饰的全局变量/函数就是 弱符号
针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:
规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件A定义全局变量global为int型,占4个字节;目标文件B定义global为double型,占8个字节,那么目标文件A和B链接后,符号global占8个字节(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。
更详细的介绍可以看GCC的强符号和弱符号
继续分析
以为发现了问题所在,兴冲冲的让对方去查下,是不是那个c方法用attribute((weak))修饰了,并且从内心已经认定是这样的了,准备收工,可现实往往那么的出其不意..对方告诉我,全局搜索了下,没有使用到 attribute((weak))....
我去,这,...还会有什么问题导致呢?
好吧,只能继续分析了
对方也发来了他们的测试demo,说确实会出现可以在宿主中定义同他们的静态库中的c方法名字一样的方法.
真的很奇怪,为啥我建立的demo,加了-ObjC,没用attribute((weak))会报错,他们建立的demo就不会呢?
一度怀疑,难道是我用的xcode和对方的有什么地方默认的不一致?
再查看对方的demo
.....
对方的domo中,c方法所在的文件,只有c方法,没有任何oc的类的定义.难道是这个导致的?
于是乎,试了下:
在demo中的sdk1中添加个只包含c方法的文件CustomeAction2
内容是
// CustomeAction2.h
#import <Foundation/Foundation.h>
NSString *convertSomething2(NSString * oriStr);
// CustomeAction2.m
#import "CustomeAction2.h"
NSString *convertSomething2(NSString * oriStr){
NSLog(@"convertSomething2 SDK1");
return @"convertSomething2 SDK1 ";
}
然后在宿主中定义一个同名的
NSString *convertSomething2(NSString * oriStr){
NSLog(@"convertSomething2 app");
return @"convertSomething2 app ";
}
编译,运行,果然不报错,
看来就是这个原因导致的了
再添加 -all_load,不出意外,报错了
image.png
问题原因解读
再度回忆 -ObjC的作用
-all_load Loads all members of static archive libraries.
-ObjC Loads all members of static archive libraries that implement an Objective-C class or category.
因为定义的c方法所在的文件并没有定义Objective-C的class或者category,所以 -ObjC并不会在符号表中导入他们,也即是这个-ObjC失效了,所以在宿主app中,就可以定义同名的方法了
当然了解决方式很简单
1 要求所有的宿主app,也就是接入方,在other linker flags中添加-all_load,这个来加载静态库中所有的方法,当然了,这个解决方式不太好.
更好的解决方式是
2 在仅仅包含c方法的文件中添加一个类的定义
后记
最后,我还对比了下,在一个方法中添加不添加attribute((weak))最后的可执行文件有什么不同
当把一个方法定义为弱符号后
attribute((weak)) NSData * pasa_cipherOperation(NSData *contentData, NSData *keyData, CCOperation operation)
在最后的 静态库中出现的不同是:
在符号表 Symbol Table中对应的方法
111.png
拿一个app的可执行文件来试试
在other linker flags中 添加 -ObjC 和不添加,对于最后的二进制的区别
当然,由于xcode编译两次后,codesign部分会不一致,可以用
codesign --remove-signature 可执行文件名 来移除签名
然后用 beyond compare来进行对比二进制
E3C9173D-5F09-4D3E-BC0A-69048627BDBC.png
再在machoview中查看对应的地址
DC24243A-D1CE-47D2-8A52-B03556765A8A.png看来,这两个,改变的都是最后的mach-o文件中符号表Symbol Table所在的内容.