为什么库 API + 编译器 ABI 足以确保 objects 与不同版本的 gcc 之间的兼容性?

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

我遇到过这样一种情况,我可能想使用一个用一个版本的 gcc 编译的 C++ 共享 object 库和一些将用另一个版本的 gcc 编译的代码。特别是,我想使用 return 一些 STL 容器的方法,例如 std::stringstd::map.

gcc website and many old Whosebug posts (e.g. )讨论这个问题。我目前的理解是

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

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

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

(class X 的唯一目的是在我测试其工作原理时将更多 name-mangling 引入共享 object 库。 )

现在我编译这些如下:

/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 中的 header 的 std::string 的定义。但是 X().f() 生成的 object 依赖于使用来自 gcc version_a 的 header 副本编译的机器代码。

我不太了解 low-level 编译器、链接器和机器指令的机制。但在我看来,我们在这里打破了一个基本规则,即每次使用时 class 的定义都必须相同,否则,我们无法保证上述情况会发生工作。

编辑: 我认为解决我的困惑的主要方法是短语 "library API" 在这种情况下的含义比它在使用中的含义要普遍得多我习惯的"API"这个词。 gcc 文档似乎以一种非常模糊的方式表明,几乎所有对实现标准库的包含文件的更改都可以被视为库中的更改 API。有关详细信息,请参阅关于 Mohan 答案的评论中的讨论。

It seems like it is possible that gcc can change the header implementing std::string

不能随意更改。那会(正如您所猜测的那样)破坏事情。但只有对 std::string 的某些更改会影响 class 的内存布局,而这些才是最重要的。

一个不影响内存布局的优化示例:他们可以更改里面的代码

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

使用更高效的算法。那不会改变字符串的内存布局。

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

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

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

GCC 必须尽一切努力使我们的程序运行。如果在不同的 t运行slation 单元中使用 std::string 的不同实现意味着我们的程序被破坏,则不允许 gcc 这样做。

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

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

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

另一种方法是默默地生成损坏的可执行文件,这是没人想要的。

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

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