26

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

4

5 回答 5

6

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

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

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

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

于 2012-04-27T13:26:34.603 回答
2

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

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

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

于 2012-04-27T13:26:07.487 回答
2

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

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

编辑

正如评论中所指出的,共享库方法不是跨平台的,在 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包含call_A哪些调用libCv1'scall_ClibB包含call_B哪些调用libCv1's call_C

然后main_A链接到 only libAmain_Bto onlylib_Bmain_ABboth 。

compile_static_fails.sh

以下命令集以libA静态libB方式构建。

#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 上)!

于 2017-06-21T21:45:13.347 回答
2

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

于 2020-06-15T06:56:59.997 回答
0

我有一个解决方案dlopen(RTLD_LOCAL | RTLD_DEEPBIND) or 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,因为fn()in main.cpp 将在 libfn.so 之前加载。

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

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

已知问题

address sanitizer 无法RTLD_DEEPBINDdlmopen().

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

于 2021-11-06T10:50:05.827 回答