应用程序二进制接口ABI (Application Binary Interface)是一套规则,规定了二进制可执行文件在运行时应如何与某些服务(诸如内核或函数库所提供的服务)交换信息。ABI特别规定了哪些使用寄存器和栈地址来交换信息以及所交换值的含义,一旦针对某个规定的ABI进行了编译,其二进制可执行文件应能在ABI相同的任何系统上运行。与之相反,标准的API(如SUSv3)仅能通过编译源代码来保证应用程序的可移植性。
C++中允许重载方法,这些方法在源代码中有相同的名字,却有不同的参数列表。链接器需要区别这些不同的重载函数之间的差异,编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字。这种编码过程叫做重整(mangling),而相反的过程叫做恢复(demangling) 。
比如具体的重整策略。一个被重整的类名字是由名字中字符的整数数量,后面跟原始名字组成的。比如,类Foo 被编码成3Foo 。方法被编码为原始方法名,后面加上_,加上被重整的类名,再加上每个参数的单字母编码。比如,Foo: :bar(int, long) 被编码为bar__3Fooil。
下面从编译、链接的角度看ABI。
我们知道,每个编译器都能将源代码编译成目标文件,那么有没有不同编译器编译出来的目标文件是能够相互链接的呢?比如,有没有可能将MSVC编译出来的目标文件和 GCC编译出来的目标文件链接到一起,形成一个可执行文件呢?
对于这个问题,首先我们可以想到的是,如果要将两个不同编译器的编译结果链接到一起,那么,首先链接器必须支持这两个编译器产生的目标文件的格式。比如MSVC编译的目标文件是PE/COFF格式的,而GCC编译的结果是ELF格式的,链接器必须同时认识这两种格式才行。
那么这是充分条件吗?事实并不像我们想象的那么简单,如果要使两个编译器编译出来的目标文件能够相互链接,那么这两个目标文件必须满足下面这些条件:采用同样的目标文件格式、拥有同样的符号修饰标准、变量的内存分布方式相同、函数的调用方式相同等等。其中,我们把符号修饰标准、变量内存布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI(Application Binary Interface)。
很多时候我们会碰到API( Application Programming Interface )这个概念,它与ABI只有一字之差,而且非常类似,很多人经常将它们的概念搞混。那么它们之间有什么区别呢?实际上,它们都是所谓的应用程序接口,只是它们所描述的接口所在的层面不一样。API往往是指源代码级别的接口,比如我们可以说POSIX是一个API标准、Windows所规定的应用程序接口是一个API;而ABI是指二进制层面的接口,ABI的兼容程度比API要更为严格,比如我们可以说C++的对象内存分布(Object MemoryLayout )是C++ABI的一部分。API更关注源代码层面的,比如POSIX规定 printf这个函数的原型,它能保证这个函数定义在所有遵循POSIX标准的系统之间都是一样的,但是它不保证printf在实际的每个系统中执行时,是否按照从右到左将参数压入堆栈,参数在堆栈中如何分布等这些实际运行时的二进制级别的问题。比如有两台机器,一台是Intel x86,另外一台是 MIPS的,它们都安装了Linux系统,由于Linux支持 POSIX标准,所以它们的C运行库都应该有printf函数。但实际上printf在被调用过程中,这些关于参数和堆栈分布的细节在不同的机器上肯定是不一样的,甚至调用printf的指令也是不一样的(x86是call指令,MIPS是jal指令),这就是说,API相同并不表示ABl相同。
ABI 的概念其实从开始至今一直存在,因为人们总是希望程序能够在不经任何修改的情况下得到重用,最好的情况是二进制的指令和数据能够不加修改地得到重用。人们始终在朝这个方向努力,但是由于现实的因素,二进制级别的重用还是很难实现。最大的问题之一就是各种硬件平台、编程语言、编译器、链接器和操作系统之间的ABI相互不兼容,由于ABI的不兼容,各个目标文件之间无法相互链接,二进制兼容性更加无从谈起。
影响ABI的因素非常多,硬件、编程语言、编译器、链接器、操作系统等都会影响ABI。我们可以从C语言的角度来看一个编程语言是如何影响ABI的。对于C语言的目标代码来说,以下几个方面会决定目标文件之间是否二进制兼容:
- 内置类型(如int、float、char 等)的大小和在存储器中的放置方式(大端、小端、对齐方式等)。
- 组合类型(如struct、union、数组等)的存储方式和内存分布。
- 外部符号(external-linkage)与用户定义的符号之间的命名方式和解析方式,如函数名func在C语言的目标文件中是否被解析成外部符号_func。
- 函数调用方式,比如参数入栈顺序、返回值如何保持等。
- 堆栈的分布方式,比如参数和局部变量在堆栈里的位置,参数传递方法等。寄存器使用约定,函数调用时哪些寄存器可以修改,哪些须要保存,等等。
当然这只是一部分因素,还有其他因素我们在此不一一列举了。到了C++的时代,语言层面对ABI的影响又增加了很多额外的内容,可以看到,正是这些内容使C++要做到二进制兼容比C来得更为不易:
- 继承类体系的内存分布,如基类,虚基类在继承类中的位置等。
- 指向成员函数的指针( pointer-to-member)的内存分布,如何通过指向成员函数的指针来调用成员函数,如何传递this 指针。
- 如何调用虚函数,vtable的内容和分布形式,vtable指针在 object 中的位置等。template 如何实例化。
- 外部符号的修饰。
- 全局对象的构造和析构。
- 异常的产生和捕获机制。
- 标准库的细节问题,RTTI 如何实现等。
- 内嵌函数访问细节。
C++一直为人诟病的一大原因是它的二进制兼容性不好,或者说比起C语言来更为不易。不仅不同的编译器编译的.二进制代码之间无法相互兼容,有时候连同一个编译器的不同版本之间兼容性也不好。比如我有一个库A是公司Company A用 Compiler A编译的,我有另外一个库B是公司Company B用Compiler B编译的,当我想写一个C++程序来同时使用库A和B将会很是棘手。有人说,那么我每次只要用同一个编译器编译所有的源代码就能解决问题了。不错,对于小型项目来说这个方法的确可行,但是考虑到一些大型的项目,以上的方法实际上并不可行。
很多时候,库厂商往往不希望库用户看到库的源代码,所以一般是以二进制的方式提供给用户。这样,当用户的编译器型号与版本与编译库所用的编译器型号和版本不同时,就可能产生不兼容。如果让库的厂商提供所有的编译器型号和版本编译出来的库给用户,这基本上不现实,特别是厂商对库已经停止了维护后,使用这样陈年老“库”实在是一件令人头痛的事。以上的情况对于系统中已经存在的静态库或动态库须要被多个应用程序使用的情况也几乎相同,或者一个程序由多个公司或多个部门起开发,也有类似的问题。
所以人们一直期待着能有统一的C++二进制兼容标准(C++ ABl),诸多的团体和社区都在致力于C++ABI标准的统一。但是目前情况还是不容乐观,基本形成以微软的VISUALC++和GNU阵营的GCC(采用Intel Itanium C++ ABI标准)为首的两大派系,各持己见互不兼容。早先时候,*NIX系统下的ABI 也十分混乱,这个情况一直延续到LSB(LinuxStandard Base)和 Intel的 Itanium C++ABI标准出来后才有所改善,但并未彻底解决ABI的问题,由于现实的因素,这个问题还会长期地存在。
题主的问题和描述如下。
你们说的ABI,Application Binary Interface到底是什么东西?
英文的解释看过了一些(比如维基百科的介绍 https://www.wikiwand.com/en/Application_binary_interface),中文的也看过几个,但感觉还是很抽象。什么name mangling啊,什么calling convention啊这些,大概知道是个什么概念但感觉很抽象。
能不能举个/些简单的例子说明?
学习哪一部分知识,可以对这个有实践上的了解?
或者说,做什么工作,才需要对这个有实际上的了解?
上文是对问题的回答,其中包含了一些例子。
另外,学习编译、链接的知识,可以对这个有实践上的了解。具体到某一个工作,C/C++软件开发工程师需要对这个有实际上的了解。
这里另外推荐书籍:深入探索 C++ 对象模型