为什么库API+编译器ABI足以确保具有不同版本gcc的对象之间的兼容性

Why is library API + compiler ABI enough to ensure compatibility between objects with different versions of gcc?

本文关键字:版本 gcc 之间 对象 兼容性 编译器 API+ ABI 确保 为什么      更新时间:2023-10-16

我遇到过一种情况,我可能想使用一个用一个版本的gcc编译的C++共享对象库,其中一些代码将用另一个版本gcc编译。特别是,我希望使用返回一些STL容器(如std::stringstd::map)的方法。

gcc网站和许多旧的stackoverflow帖子(例如这里)讨论了这个问题。我目前的理解是

  • 关于这个问题的大多数关注和帖子都是关于.so文件和.dll文件之间的交叉兼容性。由于编译器ABI不同,这是非常困难的。

  • 对于使用不同版本的gcc编译的.so文件之间的交叉兼容性(至少对于gcc版本>=3.4),您需要确保的是标准库API没有更改(如果更改了,则支持双重ABI)。

我的问题是关于这在机器级别上是如何工作的。似乎gcc可以更改实现std::string的头,即使库API没有更改,以便使其更高效或出于其他原因。如果是这样,那么两段不同的代码将使用两个不同的std::string头进行编译,并且基本上定义了两个具有相同名称的不同类。我们如何保证,当我们将std::string从使用一个标头的代码传递到使用另一标头的代码时,对象不会以某种方式被损坏或误读?

例如,假设我有以下文件:

// File a.h:
#ifndef FILE_A
#define FILE_A
#include <string>
class X {
public:
std::string f();
};
#endif  // FILE_A

// File a.cpp:
#include "a.h"
std::string X::f() {
return "hello world";
}

// File b.cpp:
#include <iostream>
#include <string>
#include "a.h"
int main() {
std::string x = X().f();
std::cout << x << std::endl;
}

(这里X类的唯一目的是在测试如何工作时,向共享对象库中引入更多的名称篡改。)

现在我编译如下:

/path/to/gcc/version_a/bin/g++ -fPIC -shared a.cpp -o liba.so
/path/to/gcc/version_b/bin/g++ -L. -la -o b b.cpp

当我执行b时,那么b具有来自version_b中的标头的std::string的定义。但是X().f()生成的对象依赖于使用来自gcc的version_a的头的副本编译的机器代码。

我不太了解编译器、链接器和机器指令的底层机制。但在我看来,我们在这里打破了一条基本规则,即每次使用类时,类的定义都必须相同,否则,我们无法保证上面的场景会起作用。

编辑:我认为解决我困惑的主要方法是,"库API"在本文中的含义比我习惯的"API"一词的使用更为笼统。gcc文档似乎以一种非常模糊的方式表明,实现标准库的include文件的任何更改都可以被视为库API的更改。有关详细信息,请参阅对Mohan回答的评论中的讨论。

GCC必须不惜一切代价让我们的程序正常工作。如果在不同的翻译单元中使用std::string的不同实现意味着我们的程序被破坏,那么gcc是不允许这样做的。

这适用于任何给定版本的GCC。

GCC竭尽全力保持向后兼容。也就是说,它努力使上述内容在GCC的不同版本中仍然适用,而不仅仅是在给定版本中。然而,它不能保证它的所有版本直到永恒都将保持兼容。当不再有可能保持向后兼容性时,引入ABI更改。

自从GCC-5 ABI发生重大变化以来,它的引入方式是,如果您将新旧二进制文件组合在一起,它会故意破坏您的构建。它通过在二进制级别重命名std::stringstd::list类来做到这一点。这会传播到具有std::stringstd::list参数的所有函数和模板。如果你试图在针对不兼容的ABI版本编译的翻译单元之间传递例如std::string,你的程序将无法链接。该机制并非100%万无一失,但它能捕捉到许多常见情况。

另一种选择是静默地生成损坏的可执行文件,这是没有人想要的。

双ABI是GCC标准库二进制的新版本与旧的可执行文件保持兼容的一种方式。基本上,它有两个版本的所有涉及std::stringstd::list的内容,链接器有不同的符号名称,因此使用旧版本名称的旧程序仍然可以加载和运行。

还有一个编译标志,允许GCC的新版本生成与旧ABI兼容的二进制文件(与没有兼容标志的新二进制文件不兼容)。除非万不得已,否则不建议使用它。

似乎gcc可以更改实现std::string 的头

它不能进行任意更改。这会(正如你所推测的)破坏一切。但是,只有对std::string的一些更改才会影响类的内存布局,而这些更改才是重要的。

例如,优化不会影响内存布局:他们可以更改内部的代码

size_t string::find (const string& str, size_t pos = 0) const;

以使用更有效的算法。这不会改变字符串的内存布局。

事实上,如果您暂时忽略了所有内容都是模板化的,因此必须在头文件中,则可以将string想象为在.h文件中定义并在.cpp文件中实现。内存布局仅根据头文件的内容确定。.cpp文件中的任何内容都可以安全地更改。

他们不能做的一个例子是向字符串添加一个新的数据成员。那肯定会打破局面。

你提到了双重ABI案例。那里发生的事情是,他们需要进行一个突破性的更改,因此他们不得不引入一个新的字符串类。其中一个类是std::string,另一个类为std::_cxx11::string。(混乱的事情发生在引擎盖下,所以大多数用户没有意识到他们在较新版本的编译器/标准库上使用std::_cxx11::string。)