C++库符号冲突杂谈

2020-10-13  本文已影响0人  豪_77e3

背景

最近在做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来分析。
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
                }
            }
        }
    }
}
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
    上面的情况:
  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
  1. 动态库链接的一些问题

总结

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

查看动态库的导出符号

查看静态库的符号

去除不必要的符号导出

上一篇 下一篇

猜你喜欢

热点阅读