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

时间:2019-03-15 22:40:46

标签: c++ shared-libraries

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

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

#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,它们都包含此标头。 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对象的地址,即{{1}的地址},以及返回值FuncDefinedByLib

FuncDefinedByLiblibB几乎相同,除了名称libA更改为PrintA并且PrintB返回2而不是1。

然后,我创建一个程序,该程序链接到两个库并调用FuncDefinedByLibPrintA。在遇到崩溃问题之前,我认为两个库都将创建自己的PrintB版本。但是,实际输出

class Data

表示这两个库都只使用Constructor Constructor LibB:0x7efceaac0079 0x7efcea8bed60 1 LibB:0x7efceaac0079 0x7efcea8bed60 1 Destructor Destructor 的一个版本,并且class Data的仅一个版本,即使类和对象的定义不同,也不同于const Data data(我理解是因为libA首先被链接)。两次毁灭清楚地说明了我的撞车问题。

这是我的问题

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

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

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

1 个答案:

答案 0 :(得分:1)

第1部分:关于链接器

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

2020年将有一个新版本的C ++发行,它将引入一个新的编译模型(称为模块),以避免此类问题。您将能够从模块导入和导出内容,类似于程序包在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中将全局变量标记为内联,则每个包含标头的翻译单元都会在内存中的位置获取自己的副本。请参阅: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部分:我们可以用它来做黑暗魔法吗?

种类。如果您真的想使用全局变量来做Dark Magic,C ++ 14会引入 templated 全局变量:

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"; 
}

做到这一点。我对模板化的全局变量没有多大用处,尽管我想象如果您正在做一些事情,例如计算一个函数被调用的次数或创建一个类型的次数,这样做会很有用。