C++库符号冲突杂谈

背景

最近在做toB业务,发现我们的SDK经常与客户之间符号冲突,要么编译链接不过,要么因为链接到错误的符号导致运行崩溃。

符号冲突

什么是符号冲突,就是库与库之间有相同的符号,使用者不知道用哪个;例如:A SDK有个符号a,B SDK也有个符号a,最终app调用a时,可能用的是A SDK的a,也可能是B SDK的a;这样的话,就会产生歧义,假如app想调用A SDK的a,但可能实际调用的却是B SDK的a,这样就会造成app行为异常,或是崩溃。

静态库之间符号冲突

静态库冲突经常会遇到下面几个问题:

  1. 为什么有些重复符号在链接时会报错,有些不会。
    首先静态库包含的是.o文件;.o文件就是对应的每个cpp/c文件编译后的产物。当链接时,链接器会按app使用到的函数逐个扫描静态库里的.o,如果发现要链接的.o里存在着已链接过的符号就会报错。不同编译器的链接算法不同,结果也不同。下面以vs 2015,xcode clang,ndk21来分析。
  • xcode clang: app使用到的函数是按字符串排列的,链接器会按这个顺序逐个扫描静态库,看下静态库里的.o是否存在在app使用到的函数,如果有就将.o所有符号放进全局符号表里,如果发现全局符号表里有相同的符号就报错
def symsGlobal
def symsAppCall
for (sym in symsAppCall) {
    if (symsGlobal dont found sym) {
        for (symsObj in symsObjs) {
            if(symsObj found sym) {
                if(symsGlobal dont contain symsObj) {
                    symsGlobal.addAll(symsObj)
                }else {
                    print sym conflict
                    abort
                }
            }
        }
    }
}
  • vs2015 vc,ndk21 clang:链接器会按静态库链接顺序扫描静态库,看.o是否存在着app使用的函数,如果有就将.o所有符号放进全局符号表里,如果发现全局符号表里有相同的符号就报错
def symsGlobal
def symsAppCall
for (symsObj in symsObjs) {
    for (sym in symsAppCall) {
        if (symsGlobal dont found sym) {
            if(symsObj found sym) {
                if(symsGlobal dont contain symsObj) {
                    symsGlobal.addAll(symsObj)
                }else {
                    print sym conflict
                    abort
                }
            }
        }
    }
}

备注:上面的算法并不一定完全准确,因为这些链接器的代码并不开源,只是通过例子推测出来,有问题欢迎指正

  1. 下面,我们结合例子分析下
    情形 1
    WX20200827-111004@2x.png

    上面情况,无论在xcode或是vs2015/ndk,app先链接谁就用谁的d函数,而且不会链接报错。
    情形 2
    WX20200827-112737@2x.png

    上面的情况:
  • 在xcode下,因为链接器会先链接a函数,他会遍历当前的静态库,发现在a.o里,然后将a.o里的所有符号都放进全局符号里;当链接d函数时,因为d已经在全局符号,因此不需要将b.o放进全局符号,所以无论链接顺序是怎样,app始终用的是liba.a的d;
  • 在vs2015/ndk下,当liba.a先链接时,链接器会发现a.o里存在着app需要的a,d函数,因此将a.o里的所有符号放进全局符号,因为app需要的函数都链接完了,所以不需要将b.o放进全局符号。当libb.a先链接时,链接器会发现b.o存在着app需要的d函数,因此将b.o所有符号放进全局符号。当链接到liba.a时,发现a.o里存在着app需要的a函数,当将a.o所有符号放进全局符号里时,发现已存在了d函数,因此就报符号冲突错误。
    情形 3
    WX20200827-180339@2x.png

    上面情况:无论在xcode或是vs2015/ndk都会报链接出错,因为无论怎么链接,都需要将a.o和b.o里的符号放进全局符号里。
  1. 链接顺序可以确保app使用的是哪个库的符号吗。
    不同编译器结果不同;对于xcode不能保证,对于vs,ndk,只要不报错,app会用先链接的库的符号。
  2. 怎样查找静态库中的重复符号。
    默认情况下,链接器是按需链接静态库,如果app没有用到.o里的函数,.o不会被链接到app,可以添加链接选项,让链接器将所有静态库的.o都链进app。这样重复的符号就会暴露出来,导致链接出错,以便我们分析,修改。
    • 对于vs2015,在链接选项里加上/WHOLEARCHIVE:a.lib,这样会强制将a里的.o链接到app
    • 对于xcode clang,在链接选项加上-all_load会强制链接所有静态库库到app,也可以用-force_load liba.a,只将a强制链接。
    • 对于ndk
      • Android.mk: LOCAL_WHOLE_STATIC_LIBRARIES += a;或者通过LOCAL_LDFLAGS += -Wl,--whole-archive /path/liba.a -Wl,--no-whole-archive /path/libb.a
      • CMakeLists.txt : target_link_libraries(myapp -Wl,--whole-archive a -Wl,--no-whole-archive b)
  3. 如何解决静态库之间的符号冲突
    • 更改名字:最原始有效的方法。
    • 声明强弱符号:这种方法比较少用,也不太实际,有兴趣的自行查找使用方法

动态库与静态库之间符号冲突

  1. 可以将动态库视为只有一个.o的静态库,链接算法与静态库差不多,但有一点区别:
    • 对于xcode和ndk,当静态库遇到动态库符号时,动态符号会被覆盖掉,而不是报错
    • 对于vs,算法与静态库一样,发现有相同的符号时,一样会报错。
  2. 在编译链接不报错的情况下,静态库先链接,一定会优先用静态库的符号
  3. 如何解决动态库与静态库之间的符号冲突
  4. 同一个静态库里有相同的符号是非常坑的,当编译源文件顺序不同时,最终链接的结果也不同。

动态库与动态库之间符号冲突

  1. 动态库之间相同的符号在链接时不会报错,先链接谁就用谁的符号。所以要解决他们之间的冲突,只能查看动态库的导出符号,更改相同的名字;其次是去除不必要的符号导出,减少冲突的可能性。
  2. app本质上也是一个动态库。

动态库的符号查找问题。

  1. 动态库是如何查找他依赖的函数呢?
    • 对于win,动态库会有个导入表,里面存储着他链接时所依赖的库和对应依赖的符号;如下图;可以用 dumpbin a.dll /IMPORTS 来列出所依赖的导入信息。当动态库被加载时,加载器会读取这个表,依次加载所依赖的动态库,从依赖的库中拿到依赖函数的地址填入表中。

      wim.png

    • 对于android,动态库存在2个表来存储这些信息。

      • 一个是链接时所依赖的so的导入库表,这个表的顺序是:直接依赖的编译链接顺序,间接依赖用户库链接顺序,间接依赖系统库顺序,例如,如果a库直接依赖b,c,而b又依赖d和系统库e,那么a的导入库表将是b,c,d,e;
      • 一个是所依赖的导入符号表。
      • 当需要使用一个符号时,加载器会去导入库表查找so,会用先找到的so里的符号,找不到则报错
      • 我们可以用arm-linux-androideabi-readelf -d liba.so来列出所依赖的动态库。然后用arm-linux-androideabi-nm liba.so -D来列出所依赖的导入符号。
        aim.png
  • 对于ios/macos,动态库的导入信息与win类似;如下图;我们可以用/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dyldinfo -lazy_bind liba.dylib 来列出所依赖的导入符号信息。
    mim.png
  1. 动态库链接的一些问题
  • vs导入表里存的是他需要的信息,如果将一个无关的库b加入c库编译链接过程里,c库里不会储存b的信息,而且存的是他直接依赖的库信息;
  • ndk,xcode,只要参加了链接都会保存对其的依赖信息,而且间接依赖库也会保存其中。
  • ndk依赖的符号与库是没有明显的对应关系,这会存在一个问题,如果a.so,b.so同时存在a符号,而c同时依赖a和b库;在开始时c调用的a符号是属于a库的,但如果在某次升级中将a库中的a符号去掉,此时a的调用就会跑到b库去了。vs和xcode在这种情况下,程序直接会出错,反馈说在a库里找不到a符号。

总结

  1. 别以静态库形式提供给客户,静态库的符号冲突比较隐蔽,机率比较大,而且修改成本也大;优先用动态库。
  2. 通过去掉不必要的导出符号,能降低动态库符号冲突的机率,但是代价比较大,特别是多团队合作的时候。从上面看出在vs和xcode里,依赖符号和依赖库是有明确的对应关系,因此可以将接口和核心功能分成两个动态库,只让接口动态库参加到客户的编译链接。这样客户的代码就不会链接到我们的核心库,冲突的几率会降低很多,万一接口库与客户有相同的符号,要修改的范围也小很多。

查看动态库的导出符号

  • vs:在vc bin目录下的dumpbin可以查看;例如:dumpbin a.dll /EXPORTS
  • ndk:在ndk工具目录下的xxx-nm可以查看;例如:arm-linux-androideabi-nm liba.so -D,其中类型“D”表示的是导出变量,“T”表示的是导出函数,“U”表示依赖别的动态库符号,即导入符号
  • xcode:在xcode工具目录下也有个dyldinfo可以查看;例如:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dyldinfo -export liba.dylib

查看静态库的符号

  • vs:使用dumpbin工具;例如:dumpbin a.lib /SYMBOLS /ARCHIVEMEMBERS 会列出所有.o的符号
    类似02F 00000000 SECTA notype () External | _printf 这种,表示“printf”是.o的符号;
    类似029 00000000 UNDEF notype () External | _foo这种,表示“foo”是引用别的模块的符号
  • ndk : 使用ndk工具中的readelf;例如 arm-linux-androideabi-readelf liba.a -s
    类似 00000001 28 FUNC GLOBAL DEFAULT 11 test这种,表示“test”是.o的符号;
    类似 * 00000000 0 NOTYPE GLOBAL DEFAULT UND foo*这种,表示“foo”是引用别的模块的符号
  • xcode: 使用xcode工具中的objdump;例如 objdump liba.a --syms
    类似:0000000000000020 g F __TEXT,__text _test这种,表示“test”是.o的符号;
    类似: 0000000000000000 UND _foo这种,表示“foo”是引用别的模块的符号

去除不必要的符号导出

  • vs:vs默认是不会将符号导出的,所有要导出的符号都必须声明为__declspec(dllexport),所以只要将不必要导出的符号去掉这个声明就可以了
  • ndk: 可以增加编译选项-fvisibility=hidden,这样默认所有符号就不会导出,对于要导出的符号显示声明__attribute__ ((visibility ("default")))
  • xcode:可以通过ndk方式实现;也可以在xcode的设置中将Symbols Hidden by Default设置为Yes,但是这方式有个坑要注意,当Enable TestabilityYes时,前面的设置无效,所以最直接的方式是ndk那种。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335