战国无双-策略国战活动站

ABI 应用程序二进制接口规范​

- 国战实录

ABI 应用程序二进制接口规范 ​我们经常可以遇到这样的问题——一个编译完毕的第三方库无法链接到我们自己的应用或者库里面, 而且如果你观察的比较仔细,很多库会在发布的二进制包的名字里面说明这是通过什么编译器生成的。

可是二进制文件这种东西,不是只要编译的目标指令集(机器码)一致就能够正确运行吗?为什么我们 需要关注它是什么编译器生成的?

如果你是从概述那个位置来的,那么,你应该已经了解了什么是名称混淆和调用约定,如果你不了解也 没关系,这一篇笔记我会详细描述他们,以及它们所属的规范——ABI。

什么是ABI ​ABI,也可以称作应用程序二进制接口(Application Binary Interface),它是二进制应用组件 相互协作的基础规范,我们可能经常听说这个概念,但是并没有详细了解过,其实这是一个相当重要的东西, 它规定了二进制组件里面非常多的关键要素:

基本类型 ​ABI规范指定了二进制组件如何处理基本类型,主要体现在它指定了基本类型的尺寸, 表达方式,内存对齐方式。

尺寸很好理解,一个基本类型需要多少字节,例如int需要4个字节,内存对齐方式则是另一个经常提及但是 非常抽象的概念,也就是一个数据结构的内存必须满足的某种要求,例如,Windows MSVC要求Double型 数据的内存地址8的倍数(但是不要以为对齐的要求和内存的尺寸有什么关系,Linux只要求它满足4的倍数) , 满足这种要求的过程就是内存对齐的过程。

对于C语言来说,它的数据类型要求在内部对齐到字段自身类型的尺寸, 举个例子:

Cstruct Astruct {

int fieldA; // 占用第 0, 1, 2, 3 字节

char fieldB; // 占用第 4 字节

// [Padding] 自动塞入第 5, 6, 7 字节(共 3 个字节)

};

// 最终总大小 = 8 字节(正好是 4 的倍数)就像这样,C语言的struct通过这种填充Padding的方式达成了ABI的对齐规范,对齐之所以重要,是因为 操作系统会按照一定的规则读写内存数据,满足这种规则的情况下效率会达到最高。

可执行文件的格式 ​ABI指定了一个二进制的可执行文件应该如何组织,具备什么样的结构,通常来说,一个可执行文件,具备 一个文件头,一个表区以及一个数据区,但是其中的细节会有很多区别,这些的具体内容就是ABI的规范所规定的了。

系统调用方式 ​系统调用的方式,与函数的调用约定的主要区别在于它在调用系统内核,而切入系统内核的规则和调用其他的库和 函数的规则有一定的区别,所以ABI单独把它拿出来讨论。

函数调用方式 ​函数的调用方式,既调用约定,函数的参数应该如何填充,使用寄存器还是使用栈,参数按照什么顺序装入,按照 什么顺序被访问,以及最终函数调用结束后,参数的资源应该如何回收。

C语言使用统一的函数调用方式,而C++或者其他语言的调用约定各有不同。

名称混淆方式 ​所谓的MangledName,简单来说是把其他的必要信息也整合到函数的导出名称中,例如函数的参数表 等,不同的C++编译器导出它们的规则是不同的,因此,一个库如果是使用微软的Microsoft MSVC进行编译 ,就很可能无法和使用GNU编译器的项目进行链接,因为GNU无法解析出正确的MSVC函数。

那么为什么编译器需要这样一个步骤,把名称搞成看不懂的样子呢,这其实是因为很多语言本身的特性会导致出现 不同函数使用同一个名称的问题,例如C++类的函数重载,它的函数名称是相同的,不同的只有参数,因此只导出 函数名称就会导致一个函数名称对应多个函数的情况,这就是所谓的符号冲突。

顺便一提,其他的编译型语言通常也有自己的Name Mangling规则,例如Rust,Go等,但这里不作为重点。

C语言在所有编译器中是一个例外,MSVC和GNU编译的C语言库非常通用,或者说,所有的C语言库几乎都是通用的,因为C语言 导出符号的规则非常简单,只导出函数名,没有任何额外的信息。

除了以上的内容,其实还有很多细节,不同的编译器的ABI标准都会有一定的区别。

什么是ABI兼容性 ​有的时候我们分明使用同一种编译器,也就是同一套ABI规范,但是偶尔还是会提到所谓的“ABI”不兼容, 所谓的ABI不兼容,说的是能不能相互替换,如果一个二进制文件的版本之间可以直接相互替换,而不需要重新编译 就可以说它们是ABI兼容的。

所谓的“ABI兼容”并不是指“是否遵守同一套ABI规范”,而是它的内存布局等细节是否与前一代一致。

这里有《西游记》中出现的通天河
我在 UC 的四年管理经验:从 0 到 200 人团队,不同阶段我都用了哪些姿势?