为什么库API+编译器ABI足以确保具有不同版本gcc的对象之间的兼容性
Why is library API + compiler ABI enough to ensure compatibility between objects with different versions of gcc?
我遇到过一种情况,我可能想使用一个用一个版本的gcc编译的C++共享对象库,其中一些代码将用另一个版本gcc编译。特别是,我希望使用返回一些STL容器(如std::string
和std::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::string
和std::list
类来做到这一点。这会传播到具有std::string
或std::list
参数的所有函数和模板。如果你试图在针对不兼容的ABI版本编译的翻译单元之间传递例如std::string
,你的程序将无法链接。该机制并非100%万无一失,但它能捕捉到许多常见情况。
另一种选择是静默地生成损坏的可执行文件,这是没有人想要的。
双ABI是GCC标准库二进制的新版本与旧的可执行文件保持兼容的一种方式。基本上,它有两个版本的所有涉及std::string
和std::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。)
- 在clang++预处理器中确定gcc工具链版本
- 如何指示 cmake 的 gcc 版本?
- 不同版本的编译器(例如GCC)是否会产生不同的性能?
- g++ libstdc++.so.6:从 4.8.5 升级到 GCC 版本 7.3.0 后找不到版本 'CXXABI_1.3.9'
- 为什么库API+编译器ABI足以确保具有不同版本gcc的对象之间的兼容性
- GCC:--静态链接到pthread的整个存档配方在最近的GCC版本中停止工作
- glibc 版本比 gcc 版本和 -wl,-rpath 不工作
- 三元运算符 '?:' 在 4.9.0 之前的 GCC 版本中推断出不正确的类型?
- 使用不同版本的 gcc 和 g++ 进行编译时出现问题
- 在CentOS 7上安装GCC 7.4.0不会更新我的LIBSTDC 版本
- 与NVIDIA链接的错误GCC版本
- 是否有编译器标志可以使较新的 gcc 版本像旧版本一样构建
- GCC编译器,为较低版本的GCC编译应用程序
- 所有版本的 GCC 都与默认成员初始值设定项作斗争,该初始值设定项捕获了这一点,并结合了继承的构造函数
- 不同的内在行为取决于 GCC 版本
- 所有版本的 GCC 都在与定义中具有默认类型的模板作斗争
- 如何在使用旧版本gcc的系统上动态链接到libc.so.6、libstdc++.so.6的本地副本
- 暂时禁用特定版本GCC上的警告
- 在使用旧版本gcc / glibc / libstdc++的计算机上运行用gcc 4.7编译的c++ 11可执行文件的问
- 对于使用旧版本gcc的系统,使用gcc进行构建