c++ - 当两个共享库定义相同的符号时实际发生了什么?

标签 c++ shared-libraries

我最近在将两个共享库(都是我自己制作的)链接在一起时遇到了崩溃问题。我最终发现这是因为一个源文件在两个文件之间重复。在该源文件中定义了一个全局 std::vector(实际上是一个类的静态成员),它最终被释放了两次——每个库释放一次。

然后我写了一些测试代码来验证我的想法。 在 header 中,我声明了一个类和该类的一个全局对象:

#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};

extern const Data data;

#endif

FuncDefinedByLib函数未定义。 然后我创建了两个库,libAlibB ,两者都包含此 header 。 libA看起来像这样

const Data data;

int Data::FuncDefinedByLib(void) const {return 1;}

void PrintA(void) {
  std::cout << "LibB:" << &data << " "
    << (void*)&Data::FuncDefinedByLib <<  " "
    << data.FuncDefinedByLib() << std::endl;
}

它定义了全局data对象,FuncDefinedByLib函数,和一个函数 PrintA打印 data 的地址对象,FuncDefinedByLib的地址, 以及 FuncDefinedByLib 的返回值.

libB几乎与 libA 相同除了名字 PrintA更改为 PrintBFuncDefinedByLib返回 2 而不是 1。

然后我创建一个链接到这两个库的程序并调用 PrintAPrintB .在遇到崩溃问题之前,我认为这两个库都会创建自己的 class Data 版本。 .然而,实际输出

Constructor
Constructor
LibB:0x7efceaac0079 0x7efcea8bed60 1
LibB:0x7efceaac0079 0x7efcea8bed60 1
Destructor
Destructor

表示两个库只使用一个版本的class Data而且只有一个版本的 const Data data即使类和对象的定义不同,这来自 libA (我明白这是因为 libA 是第一个链接的)。双重破坏清楚地解释了我的崩溃问题。

这是我的问题

  1. 这是怎么发生的?我知道链接到这两个库的主要代码可能只链接到它看到的第一个符号。但是一个共享库在创建的时候应该已经在内部链接了(或者不是?我对共享库的知识真的不多),他们怎么知道其他库中有一个孪生类并在他们有之后链接到那个是自己创建的?

  2. 我知道在共享库之间使用重复代码通常是一种不好的做法。但是是否有一个条件可以满足库之间的复制是安全的?还是有一种系统的方法可以使我的代码无风险地复制?或者它永远不安全,应该始终严格禁止?我不想总是为了共享一小段代码而拆分另一个共享库。

  3. 这种行为看起来很神奇。有没有人利用这种行为来做一些神奇的事情?

最佳答案

第 1 部分:关于链接器

这是 C 和 C++ 中的一个已知问题,它是当前编译模型的结果。 如何它发生的完整解释超出了这个答案的范围,但是this talk by Matt Godbolt为初学者提供了过程的深入解释。另见 this article on the linker .

C++ 的新版本将在 2020 年推出,它将引入一种新的编译模型(称为模块)来避免此类问题。您将能够从模块导入和导出内容,类似于包在 Java 中的工作方式。

第 2 部分:解决您的问题

有几种不同的解决方案。

神奇的解决方案 1:一个唯一的全局变量

这个很漂亮。如果将全局变量作为静态变量放在函数中,它将总是只构造一次,并且这是由标准确保的(即使在多线程环境中)。

#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};
Data& getMyDataExactlyOnce() {
    // The compiler will ensure
    // that data only gets constructed once
    static Data data;
    // Because data is static, it's fine to return a reference to it
    return data; 
}

// Here, the global variable is a reference
extern const Data& data = getMyDataExactlyOnce();

#endif

神奇的解决方案 2:多个不同的全局变量,每个翻译单元 1 个

如果您在 C++17 中将全局变量标记为内联变量,则包含 header 的每个翻译单元都会在内存中的自己位置获取自己的拷贝。请参阅:https://en.cppreference.com/w/cpp/language/inline

#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};
// Everyone gets their own copy of data
inline extern const Data data;

#endif

第 3 部分:我们可以用它来施展黑魔法吗?

有点。如果你真的,真的想用全局变量做黑魔法,C++14 引入了模板化全局变量:

template<class Key, class Value>
std::unordered_map<Key, Value> myGlobalMap; 

void foo() {
    myGlobalMap<int, int>[10] = 20;
    myGlobalMap<std::string, std::string>["Hello"] = "World"; 
}

随心所欲。我对模板化的全局变量没有太多用处,尽管我想如果你正在做一些事情,比如计算一个函数被调用的次数,或者一个类型被创建的次数,这样做会很有用。

关于c++ - 当两个共享库定义相同的符号时实际发生了什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55191553/

相关文章:

python - 使用 python c api 获取共享对象的完整路径

c - 尽管提供了库,但编译后 .so 中的 undefined symbol

shared-libraries - 具有共享库支持的 Grub 引导加载程序

c# - Linux/C++ 程序员到 C#/Windows 程序员

c++ - 无法为数组指定显式初始值设定项

c++ - 代码块中的 SDL_ShowSimpleMessageBox 令人困惑的错误、内存不足和 undefined reference

c++ - 每次保存后如何进行自动g++编译?

c++ - static_cast<int>(var) 和 *(int*)&var 有什么区别?

c++ - CMake 将依赖项复制到可执行输出路径

android-ndk - Android NDK 共享对象 undefined reference 错误