为什么库 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::string
和 std::map
.
gcc website and many old Whosebug posts (e.g. )讨论这个问题。我目前的理解是
关于这个问题的大多数关注和帖子都是关于.so文件和.dll文件之间的cross-compatibility。这非常困难,因为编译器 ABI 不同。
对于cross-compatibility之间用不同版本的gcc编译的.so文件(至少gcc版本>=3.4),你需要确保的是标准库API 没有改变(并且,如果有,则有 dual ABI 支持)。
我的问题与它在机器级别的工作方式有关。似乎 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::string
和 std::list
类 来实现。这会传播到所有具有 std::string
或 std::list
参数的函数和模板。如果你试图通过例如std::string
在针对不兼容的 ABI 版本编译的 t运行 翻译单元之间,您的程序将无法 link。该机制并非 100% 万无一失,但它可以捕获许多常见情况。
另一种方法是默默地生成损坏的可执行文件,这是没人想要的。
双 ABI 是新版本的 GCC 标准库 binary 与旧版可执行文件保持兼容的一种方式。基本上它有两个版本涉及 std::string
和 std::list
,linker 有不同的符号名称,因此仍然可以加载使用旧版本名称的旧程序并且 运行.
还有一个编译标志允许较新版本的 GCC 生成与旧 ABI 兼容的二进制文件(并且与没有兼容标志生成的较新二进制文件不兼容)。除非万不得已,否则不建议使用它。
我遇到过这样一种情况,我可能想使用一个用一个版本的 gcc 编译的 C++ 共享 object 库和一些将用另一个版本的 gcc 编译的代码。特别是,我想使用 return 一些 STL 容器的方法,例如 std::string
和 std::map
.
gcc website and many old Whosebug posts (e.g.
关于这个问题的大多数关注和帖子都是关于.so文件和.dll文件之间的cross-compatibility。这非常困难,因为编译器 ABI 不同。
对于cross-compatibility之间用不同版本的gcc编译的.so文件(至少gcc版本>=3.4),你需要确保的是标准库API 没有改变(并且,如果有,则有 dual ABI 支持)。
我的问题与它在机器级别的工作方式有关。似乎 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::string
和 std::list
类 来实现。这会传播到所有具有 std::string
或 std::list
参数的函数和模板。如果你试图通过例如std::string
在针对不兼容的 ABI 版本编译的 t运行 翻译单元之间,您的程序将无法 link。该机制并非 100% 万无一失,但它可以捕获许多常见情况。
另一种方法是默默地生成损坏的可执行文件,这是没人想要的。
双 ABI 是新版本的 GCC 标准库 binary 与旧版可执行文件保持兼容的一种方式。基本上它有两个版本涉及 std::string
和 std::list
,linker 有不同的符号名称,因此仍然可以加载使用旧版本名称的旧程序并且 运行.
还有一个编译标志允许较新版本的 GCC 生成与旧 ABI 兼容的二进制文件(并且与没有兼容标志生成的较新二进制文件不兼容)。除非万不得已,否则不建议使用它。