当两个共享库定义相同的符号时,实际会发生什么? [英] What actually happens when two shared libraries define the same symbol?

查看:58
本文介绍了当两个共享库定义相同的符号时,实际会发生什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I recently encountered a crash issue when I linked two shared libraries (both made by myself) together. I eventually found it was because of one source file duplicated between the two files. In that source file a global std::vector was defined (in fact a static member of a class), and it ended up with being freed twice -- one by each library.

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

I then wrote some test code to verify my thought. Here in a header I declare a class and a global object of this class:

#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 看起来像这样

The FuncDefinedByLib function is left undefined. I then created two libraries, libA and libB, both include this header. libA looks like this

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函数和一个打印data地址的函数PrintA/code> 对象,FuncDefinedByLib 的地址,FuncDefinedByLib 的返回值.

It defines the global data object, the FuncDefinedByLib function, and a function PrintA that prints the address of the data object, the address of FuncDefinedByLib, and the return value of FuncDefinedByLib.

libBlibA 几乎相同,除了名称 PrintA 改为 PrintBFuncDefinedByLib 返回 2 而不是 1.

libB is almost same as libA except the name PrintA is changed to PrintB and FuncDefinedByLib returns 2 instead of 1.

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

Then I create a program that links to both of the libraries and calls PrintA and PrintB. Before encountering the crash issue I thought both libraries would create their own versions of class Data. However, the actual output

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

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

Indicates that both libraries use only one version of class Data and only one version of const Data data even if the class and the object are defined differently, which is from libA (I understand it is because libA is linked first). And the double destruction clearly explains my crash problem.

这是我的问题

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

  1. How does this happen? I understand the main code linking against the two libraries may only link to the first symbol it sees. But a shared library should has been linked internally when it is created (or it is not? I really have no much knowledge of shared library), how can they know there is a twin class in other libraries and link to that when after they have been created on their own?

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

I know having duplicate code between shared libraries is generally a bad practice. But is there a condition that by satisfying it duplication between libraries is safe? Or is there a systematic way to make my code duplicabale without risk? Or it is never safe and should always be strictly prohibited? I don't want to always split another shared library just to share a tiny piece of code.

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

This behavior looks magical. Does anyone utilize this behavior to do some good magical things?

推荐答案

第 1 部分:关于链接器

这是 C 和 C++ 中的一个已知问题,它是当前编译模型的结果.对如何发生的完整解释超出了本答案的范围,但是Matt Godbolt 的这个演讲 为初学者提供了对该过程的深入解释.另请参阅这篇关于链接器的文章.

Part 1: About the Linker

This is a known problem in both C and C++, and it's the result of the current compilation model. A full explanation of how it happens is beyond the scope of this answer, however this talk by Matt Godbolt provides an in-depth explanation of the process for beginners. See also this article on the linker.

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

There's a new version of C++ coming out in 2020, and it'll introduce a new compilation model (called Modules) that avoids problems like this. You'll be able to import and export stuff from a module, similar to the way packages work in Java.

有几种不同的解决方案.

There are a few different solutions.

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

This one's pretty slick. If you stick the global variable inside a function as a static variable, it will always get constructed only once, and this is ensured by the standard (even in a multithreaded environment).

#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 部分:我们可以用它来做黑魔法吗?

有点.如果你真的,真的想用全局变量来做黑魔法,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"; 
}

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

Make of that what you will. I haven't had much use for templated global variables, although I imagine if you were doing something like counting the number of times a function was called, or the number of times a type was created, it'd be useful to do this.

这篇关于当两个共享库定义相同的符号时,实际会发生什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆