【问题标题】:Why is library API + compiler ABI enough to ensure compatibility between objects with different versions of gcc?为什么库 API + 编译器 ABI 足以保证不同版本 gcc 的对象之间的兼容性?
【发布时间】:2019-01-29 06:38:45
【问题描述】:

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

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

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

  • 对于使用不同版本的 gcc(至少 gcc 版本 >= 3.4)编译的 .so 文件之间的交叉兼容性,您只需要确保标准库 API 没有更改(并且,如果有,有dual 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 文档似乎以一种非常模糊的方式表明,对实现标准库的包含文件的几乎任何更改都可以被视为库 API 中的更改。有关详细信息,请参阅 cmets 中有关 Mohan 答案的讨论。

【问题讨论】:

  • “基本上定义了两个不同的同名类”为了保持兼容性,gcc 几乎不修改类,只是在它知道不会引起问题的方式。有些错误会保留数十年,因为修复它们会破坏 ABI。

标签: c++ gcc stl abi


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 这是对双 ABI 系统的一个很好的解释,但它并没有真正解决我在二进制级别如何工作的概念问题。
  • @sasquires 双 ABI 机制合理地确保 要么 version_a 和 version_b 是二进制兼容的,您的代码将无法构建。如果您使用两个不兼容的版本而没有双 ABI 机制,那么 违反了规则,您的代码将无法工作。如果您仍然有概念问题,您可能需要更详细地解释它到底是什么,因为我不知道。
【解决方案2】:

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

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

举个不影响内存布局的优化例子:他们可以改变里面的代码

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

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

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

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

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

【讨论】:

  • 抱歉回复延迟太久;我一直在度假。我认为这个答案很有意义,除了一个主要的术语问题:那么“库 API”实际上是什么意思?这在 gcc 和 ABI 兼容性的上下文中必须具有与在更广泛的编程世界中不同的含义。对我来说,使用我习惯的“API”定义,完全可以将私有数据成员添加到类而不更改 API!
  • 我之前的评论是剩下的主要概念问题,但为了完整起见,让我在这一点上回顾一下我的理解。为了让事情起作用,我们真的只需要数据成员相同,因为对象实际上只包含数据成员。方法由编译器在其他地方实现。一般来说,一个字符串的一个副本调用一个方法的一个编译版本,而另一个副本调用另一个版本并不重要,只要这两个版本做同样的事情。所以唯一可能出错的地方就是复制数据成员。
  • @sasquires 我不能发誓我已经记下了这些术语。但我想说的是,在 C++ 中,如果添加私有数据成员,库 API 会发生变化。您也不一定只需要数据成员相同(并且顺序相同)。允许两个不同的编译器为具有相同成员的同一个结构生成不同的内存布局,如果您摆弄影响打包的开关,实际上单个编译器可以生成不同的内存布局。此外,还有一个叫做虚函数表的东西,它是为一个类生成的......
  • 如果你添加一个新的虚函数,该表的内容将会改变。阅读en.wikipedia.org/wiki/Virtual_method_table。这也会影响二进制兼容性。可能还有其他情况;我不经常考虑这一点,无法详尽地列出所有内容。
  • cmets 中塞进太多信息是件很困难的事,所以我给你一些链接,你可以在阅读后提出更具体的问题。 opensource.apple.com/source/gcc/gcc-5026.1/libstdc++-v3/docs/…(只需阅读第一部分,直到“版本控制”)
猜你喜欢
  • 1970-01-01
  • 2012-03-03
  • 2021-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多