【问题标题】:C++ two libraries depend on same lib but different versions?C++ 两个库依赖于相同的库但不同的版本?
【发布时间】:2012-05-08 05:40:04
【问题描述】:

如果我使用 GCC 编译器在 C++ 中有库 A、B 和 C。 Lib A 和 B 都依赖于 C,但依赖于它的不同版本。然后我可以在另一个程序中同时使用 A 和 B 吗?或者 A 和 B 要求的 C 的不同版本会冲突吗?我该如何解决这个问题,我可以吗?

【问题讨论】:

    标签: c++ gcc dependencies


    【解决方案1】:

    动态库不做强大的版本检查,这意味着如果 A 在 C 中使用的入口点没有改变,那么它仍然可以使用更高版本的 C。话虽如此,Linux 发行版通常使用提供版本支持的符号链接文件系统方法。这意味着如果一个可执行文件被设计为仅与 1.2.2 一起使用,那么它可以专门链接到查找/usr/lib/mylib-1.2.2

    大多数程序都链接到查找一般情况,例如。 /usr/lib/mylib 这将象征性地链接到机器上的版本。例如。 /usr/lib/mylib -> /usr/lib/mylib-1.2.2。如果您不链接到特定版本并且实际接口不更改,那么向前兼容性应该不是问题。

    如果要检查库 A 和 B 是否绑定到特定命名的 C 版本,可以在它们上使用 ldd 命令检查 dll 搜索路径。

    【讨论】:

      【解决方案2】:

      我假设您正在动态链接。如果 A 和 B 都完全封装了它们各自版本的 C,那么就有可能做到这一点。您可能必须确保不同版本的 C 命名不同(即 libMyC.1.so 和 libMyC.2.so)以避免在运行时加载它们时造成混淆。

      您还可以调查静态构建 A 和 B 以避免运行时负载混淆的可能性。

      找出答案的最简单方法就是尝试一下。应该不会花很长时间来确定它是否会起作用。

      最后,当然,到目前为止,最简单的解决方案,从维护的角度来看也是最好的,是将 A 或 B 提升到另一个级别,以便它们都使用相同版本的 C。这在有这么多方法,我强烈建议你这样做,而不是尝试解决一个真正的问题。

      【讨论】:

        【解决方案3】:

        我在寻找答案时发现了这个问题,正如@Component-10 所建议的,我创建了一组最小的文件来调查这种行为并使用 MacOS + CLANG 进行了测试。

        • 如果将 A 和 B 构建为共享库,您可以获得对依赖库 C 的正确解析,该依赖库 C 是 A 和 B 的依赖项,但版本不同。
        • 如果将 A 和 B 构建为静态,则会失败。

        编辑

        正如 cmets 中所指出的,共享库方法不是跨平台的,在 Linux 中不起作用。

        @SergA 使用动态加载库 (dl) API (https://www.dwheeler.com/program-library/Program-Library-HOWTO/x172.html) 创建了一个解决方案。

        @SergA 使用 dlopen 的解决方案

        #include <stdio.h>
        #include <stdlib.h>
        #include <dlfcn.h>
        
        // #define DLOPEN_FLAGS RTLD_LAZY | RTLD_LOCAL
        #define DLOPEN_FLAGS RTLD_LAZY
        
        #if defined(_WIN32) || defined(__CYGWIN__)
            // Windows (x86 or x64)
            const char* libA = "libA.shared.dll";
            const char* libB = "libB.shared.dll";
        #elif defined(__linux__)
            // Linux
            const char* libA = "libA.shared.so";
            const char* libB = "libB.shared.so";
        #elif defined(__APPLE__) && defined(__MACH__)
            // Mac OS
            const char* libA = "libA.shared.dylib";
            const char* libB = "libB.shared.dylib";
        #elif defined(unix) || defined(__unix__) || defined(__unix)
            // Unix like OS
            const char* libA = "libA.shared.so";
            const char* libB = "libB.shared.so";
        #else
            #error Unknown environment!
        #endif
        
        int main(int argc, char **argv)
        {
          (void)argc;
          (void)argv;
        
          void *handle_A;
          void *handle_B;
          int (*call_A)(void);
          int (*call_B)(void);
          char *error;
        
          handle_B = dlopen(libB, DLOPEN_FLAGS);
          if(handle_B == NULL) {
            fprintf(stderr, "%s\n", dlerror());
            exit(EXIT_FAILURE);
          }
        
          handle_A = dlopen(libA, DLOPEN_FLAGS);
          if(handle_A == NULL) {
            fprintf(stderr, "%s\n", dlerror());
            exit(EXIT_FAILURE);
          }
        
        
          call_A = dlsym(handle_A, "call_A");
          error = dlerror();
          if(error != NULL) {
            fprintf(stderr, "%s\n", error);
            exit(EXIT_FAILURE);
          }
          call_B = dlsym(handle_B, "call_B");
          error = dlerror();
          if(error != NULL) {
            fprintf(stderr, "%s\n", error);
            exit(EXIT_FAILURE);
          }
        
          printf(" main_AB->");
          call_A();
          printf(" main_AB->");
          call_B();
        
          dlclose(handle_B);
          dlclose(handle_A);
        
          return 0;
        }
        

        以前的解决方案显示静态与共享

        这是我的文件集。为简洁起见,我不会在这里全部展示。

        $ tree .
        .
        ├── A
        │   ├── A.cc
        │   └── A.hh
        ├── B
        │   ├── B.cc
        │   └── B.hh
        ├── C
        │   ├── v1
        │   │   ├── C.cc
        │   │   └── C.hh
        │   └── v2
        │       ├── C.cc
        │       └── C.hh
        ├── compile_shared_works.sh
        ├── compile_static_fails.sh
        ├── main_A.cc
        ├── main_AB.cc
        └── main_B.cc
        

        A 依赖于 C 版本 1,B 依赖于 C 版本 2。每个库都包含一个函数,例如libA 包含调用libC v1 的call_Ccall_A,而libB 包含调用libC v1 的call_Ccall_B

        然后main_A 只链接到libAmain_B 只链接到lib_Bmain_AB 链接到两者。

        compile_static_fails.sh

        以下命令集静态构建libAlibB

        #clean slate
        rm -f *.o *.so *.a *.exe
        
        #generate static libA
        g++ -I . -c C/v1/C.cc A/A.cc
        ar rvs libA.a *.o
        rm -f *.o
        
        #generate static libB
        g++ -I . -c C/v2/C.cc B/B.cc
        ar rvs libB.a *.o
        rm -f *.o
        
        #generate 3 versions of exe
        g++ -L . -lA main_A.cc -o main_A.exe
        g++ -L . -lB main_B.cc -o main_B.exe
        g++ -L . -lA -lB main_AB.cc -o main_AB.exe
        ./main_A.exe
        ./main_B.exe
        ./main_AB.exe
        

        输出是

        main_A->call_A->call_C [v1]
        main_B->call_B->call_C [v2]
        main_AB->call_A->call_C [v1]
        main_AB->call_B->call_C [v1]
        

        main_AB 执行call_B 时,它会走错地方!

        compile_shared_works.sh

        #clean slate
        rm -f *.o *.so *.a *.exe
        
        #generate shared libA
        g++ -I . -c -fPIC C/v1/C.cc A/A.cc
        g++ -shared *.o -o libA.so
        rm *.o
        
        #generate shared libB
        g++ -I . -c -fPIC C/v2/C.cc B/B.cc
        g++ -shared *.o -o libB.so
        rm *.o
        
        #generate 3 versions of exe
        g++ -L . -lA main_A.cc -o main_A.exe
        g++ -L . -lB main_B.cc -o main_B.exe
        g++ -L . -lA -lB main_AB.cc -o main_AB.exe
        ./main_A.exe
        ./main_B.exe
        ./main_AB.exe
        

        输出是

        main_A->call_A->call_C [v1]
        main_B->call_B->call_C [v2]
        main_AB->call_A->call_C [v1]
        main_AB->call_B->call_C [v2]
        

        它可以工作(在 MacOS 上)!

        【讨论】:

        • 您好,无法重现您的结果。我尝试使用 GCC 和 Clang 构建测试样本。在所有变体中,我得到错误的结果。我认为这与我们系统上不同的 ld.so 行为有关。
        • 我在这里根据您的答案创建测试:github.com/anton-sergeev/multiversion_library_test。我添加了 dlopen-variant 这对我有用。
        • 当。我想这仅适用于 MacOS/X + CLANG。我会相应地更新答案。
        • 如果您避免在 libA 和 libB 中重新导出 libC 的符号(=定义 ABI),您的共享库案例也可以在 Linux 中工作。您可以为 libA 和 libB 使用 version script(如果您无法控制 libC 的构建脚本/源代码),或者使用 -fvisibility=hidden 或通过代码注释 __attribute__((visibility("hidden"))) 直接在 libC 中指定可见性。
        【解决方案4】:

        @SergA 的解决方案也适用于 Linux,如果我们使用标志打开共享库 RTLD_LAZY | RTLD_LOCAL
        输出是:
        1. Main_AB_dlopen -> CallA -> callC(v1)
        2. Main_AB_dlopen -> callB -> callC(v2)

        【讨论】:

        • RTLD_DEEPBIND 也是必需的
        【解决方案5】:

        dlopen(RTLD_LOCAL | RTLD_DEEPBIND)我有一个解决方案 或dlmopen(LM_ID_NEWLM, "filename.so", ...) (和上面其他答案一样熟悉)

        首先我们应该阅读这篇文章来了解 .dynsym 是 linux ELF 中的“导出符号表”:https://blogs.oracle.com/solaris/post/inside-elf-symbol-tables

        每个elf文件都包含可执行文件和动态库,头文件中有.dynsym。以__attribute__((visibility("default"))) 声明的函数将记录在此标头区域中。
        但是如果-fvisibility=hidden没有分配给编译器(gcc),所有的函数都会被编译器自动声明为__attribute__((visibility("default")))。 (所有功能都标记为导出)

        .so 加载情况如何?

        所有的.so会在程序启动之前被加载到内存中int main(),所有文件在ELF头中被一一标记为DT_NEEDED。
        当前程序在系统中有一个全局符号表,所有的.so文件都加载并填入该表中所持有的函数名的地址,如果两个.so文件有相同的函数名,只有第一个会接受的。

        dlopen() dlopen()中加载的程序与ELF头加载没有区别,全局符号表也由dlopen()填充。

        例如

        // main.cpp
        __attribute__((visibility("default"))) int fn() { return 10; }
        void print();
        int main() { print(); return 0; }
        
        // libfn.cpp => libfn.so
        __attribute__((visibility("default"))) int fn() { return 999; }
        __attribute__((visibility("default"))) void print() { cout<<fn(); }
        

        10 会打印,因为 main.cpp 中的 fn() 会在 libfn.so 之前加载。

        如果使用dlopen(libfn.so, RTLD_LOCAL)而不使用RTLD_DEEPBIND,数字10仍然会被打印出来。

        如果在编译阶段使用普通链接,即使使用lib_try_in_middle.so之类的东西来分隔ELF-header中的.dynsymany文件中的函数标记为__attribute__((visibility("default"))) 总是出现在全局符号表中。

        已知问题

        address sanitizer 无法与 RTLD_DEEPBIND 一起使用,我还没有找到任何解决方案让 asan 在 dlmopen() 的新命名空间中运行。

        参考:https://man7.org/linux/man-pages/man3/dlopen.3.html

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-02-25
          • 1970-01-01
          • 1970-01-01
          • 2014-08-18
          • 2016-04-03
          • 2018-12-06
          • 2018-04-24
          相关资源
          最近更新 更多