本文由知乎答主我是龙套小果丁提供
前注:笔者在暑假时偶然关注到C++的ABI问题,对此进行了比较长时间的探究。事实上距离现在,这已经有比较长的时间;而写这篇文章的目的,一方面可以给其他想了解这个话题的人一点思路,权当抛砖引玉;另一方面更想与大家做以探讨,以防止对此概念产生误解而不自知,希望大家可以指正这篇文章的错误。我也十分希望可以有人推荐给我相关的精彩文章,让我更进一步地理解这一概念。
按照Titus Winters在提案P2028中所解释的概念,ABI是指在一个翻译单元中的实体(如函数、类型等)如何交互,平台相关、(编译器)供应商相关。
原文:ABI is the platform-specific, vendor-specified, not-controlled-by-WG21 specification of how entities (functions, types) built in one translation unit interact with entities from another. ABI本身并没有在C++标准中出现过,这导致C++的ABI问题比较混乱;这也是C++相关提案出现的原因——"not controlled by WG21"。事实上C标准也没有这个概念。
翻译单元(TU)在标准中有明确的概念;以笔者的理解,大概可以认为生成的每个object file都是一个翻译单元。
具体地,C++的ABI可以分为两个方面,我们也会按两方面讨论:
这是笔者之前在reddit的一个帖子上看到的分类,觉得很合理,但当时居然没有标记下来,如果有人确实需要看原帖,笔者可以找找。
自然的,因为库本身是由语言编写的,通常情况下语言ABI的改变都会使库的ABI不兼容。
C++的ABI由编译器、操作系统和硬件的体系结构共同决定;按照道理来说C应该也是,但是由于操作系统本身具有了底层的C ABI,因此相应平台上的编译器都会遵循这个ABI,于是C的ABI一般不由编译器的诸多选项等决定。
当然,这不意味着不同的C编译器产生的object file可以一起link。如果两个编译器产生可互相辨认的object file(即格式一致),这应该是可行的;但反之,像MSVC和MinGW的gcc,它们编译产生的符号表完全不一致,因此不能链接。如下图:
MSVC 19.29编译出的目标文件
MinGW gcc 8.1.0编译出的可执行文件
解析工具见GitHub - gitGNU/objconv。 如果使用相同的库,clang和gcc的C编译器应该可以产生可链接的object file。
C的ABI主要包括以下5个方面:
但是对于C++,它的ABI还十分取决于编译器(我想这也是为什么Language ABI也称作compiler ABI)。也就是说,就算两个目标文件在以上方面都一致,而且符号表等也可互相识别,但他们仍可能链接出一个错误的可执行文件。这通常出现在用一个更早版本的编译器去链接更晚版本的编译器产生的目标文件,或者相同版本但选择了某些改变ABI的编译器选项的目标文件。
具体地,C++由编译器决定的ABI主要包括:
namespace Namespace {int function(int x);}
,在GCC中会修饰为_ZN9NameSpace8functionEi
,而在MSVC中会修饰为?function@NameSpace@@YAHH@Z
。将修饰后的名称转化会原名称的过程称为demangle;一个demangle的网站是demangler.com/ 编译器决定的ABI的分类主要来自于GCC manual about compatibility.
C++的主流语言ABI应该有两套:
特别地,Clang好像有一些选项可以尽量(但不完全)兼容MSVC的ABI;见clang.llvm.org/docs/MSV。不知道GCC/MSVC有没有兼容其他ABI的选项?
由于编译器一般都使用供应商所提供的标准库实现,因此标准库的ABI也事实上成为了C++ABI的一部分。具体地,如果一个动态库在更新后,原来的可执行文件仍然能正常地使用动态库的函数,而不需要让源代码重新编译,则称库的ABI保持了下去 / 二进制兼容。静态库本身应该不需要考虑这个问题,因为静态库更新之后总是需要重新编译。
std::string
和std::list
的ABI改变了(为了适应C++11关于COW的规定),造成在新编译器中链接之前的代码会运行崩溃(我觉得这是很多公司维持gcc版本在4.9的重要原因,防止老的库用不了,但似乎有些因噎废食)。这给库程序员造成很大的麻烦,因为C++程序员几乎不可避免使用标准库;如果要兼容所有版本,保险起见就需要每个ABI break的版本都提供新的库。如果想跨平台,还要考虑操作系统的问题;甚至可能需要考虑编译器选项的问题,之前笔者遇到过VS中Release模式编译的库在Debug模式使用会报warning。
如果注意前面提到的几个方面,那么我们可以编写出一个二进制兼容的库。也就是说,在库更新后,一个实体根据它原来的索引方式仍然能索引到正确的实体:
__stdcall
和__cdecl
在Windows中不要混用;这是为了让语言ABI维持统一。class A { public: int a; int b;};
变为class A{ public: int b; int a;};
,由于用户代码实际上使用偏移量索引的,改变之后会让用户代码想索引a
时索引到b
,想索引b
时索引到a
。或者增加了类的成员,使得栈的分配出现问题。std::string
就是因为改变了成员造成了不兼容。有两篇文章详述了维持库ABI时需要注意的事项,说的很到位,见
KDE ABI regulationcommunity.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B
20 ABI breaking changeswww.acodersjourney.com/20-abi-breaking-changes
其次注意一下标准库的使用版本,也就考虑了标准库的ABI。
一种比较常见的维护ABI的技术是PImpl,这是一个比较重要的技术,像图形学中重要的模型库assimp就在代码中使用了这项技术,但是总体上来说比较简单,暂不是本文讨论的重点;如果有人想看,笔者可以单独写另外一篇文章。