什么是应用程序二进制接口(ABI)?

我从来没有弄清楚ABI是什么。请不要给我指出维基百科的文章。如果我能理解它,我就不会在这里发布这么长的帖子了。

这是我对不同界面的看法:

电视遥控器是用户和电视之间的接口。它是一个现有的实体,但本身毫无用处(不提供任何功能)。遥控器上每个按钮的所有功能都在电视机中实现。

接口说明它是一个“现有实体”层,位于 functionalityconsumer的功能。一个界面本身 不做任何事情。它只是调用背后的功能。

现在,根据用户是谁,有不同类型的界面。

命令行界面(CLI)命令是现有实体, 消费者是用户,功能在后面。

functionality:我的软件功能,解决了一些问题 我们描述这个接口的目的。

existing entities:命令

consumer:用户

图形用户界面(GUI)窗口、按钮等是现有的 实体,消费者是用户,功能在后面。

functionality:我的软件功能,它解决了我们描述这个接口的一些问题。

existing entities:窗口,按钮等。

consumer:用户

应用程序编程接口(API)函数(或 更正确的)接口(在基于接口的编程中)是 现有实体,这里的消费者是另一个程序而不是用户,并且再次 功能在这一层后面。

functionality:我的软件功能,解决了一些问题 我们正在描述这个接口的问题。

existing entities:函数,接口(函数数组)。

consumer:另一个程序/应用程序。

应用程序二进制接口(ABI)这就是我的问题开始的地方。

functionality:???

existing entities:???

consumer:???

  • 我用不同的语言编写过软件,并提供了不同类型的接口(CLI、GUI和API),但我不确定我是否提供过任何ABI。

维基百科说:

ABI涵盖细节,例如

  • 数据类型、大小和对齐方式;
  • 调用约定,它控制函数参数的方式 传递并返回检索到的值;
  • 系统调用号码以及应用程序如何进行系统调用 到操作系统;

其他ABI标准化细节,例如

  • C++名字,
  • 异常传播,以及
  • 在同一平台上的编译器之间调用约定,但 不需要跨平台兼容性。
  • 谁需要这些细节?请不要说操作系统。我知道汇编编程。我知道链接和加载是如何工作的。我确切地知道里面发生了什么。

  • 为什么会出现C++名称修饰?我以为我们是在二进制级别上讨论。为什么会出现语言?

无论如何,我已经下载了[PDF]System V Application二进制接口Edition 4.1(1997-03-18),看看它到底包含了什么。好吧,大部分都没有任何意义。

  • 为什么它包含两章(第4章和第5章)来描述ELF文件格式?事实上,这是该规范仅有的两个重要章节。其余的章节是“特定于处理器的”。无论如何,我认为这是一个完全不同的话题。请不要说ELF文件格式规范是ABI。根据定义,它不符合接口的条件。

  • 我知道,既然我们谈论的是如此低的层次,它必须非常具体。但我不确定它是如何“指令集架构(ISA)”特定的?

  • 在哪里可以找到Microsoft Windows的ABI?

所以,这些是困扰我的主要问题。

180580 次浏览

调用者和被调用者之间的ABI需要保持一致,以确保调用成功。堆栈使用、寄存器使用、例程结束堆栈弹出。所有这些都是ABI最重要的部分。

让我至少回答你问题的一部分。以LinuxABI如何影响系统调用为例,以及为什么这是有用的。

系统调用是用户空间程序向内核空间请求某些东西的一种方式。它的工作原理是将调用的数字代码和参数放入某个寄存器并触发中断。然后内核发生切换,内核查找数字代码和参数,处理请求,将结果放回寄存器并触发切换回用户空间。例如,当应用程序想要分配内存或打开文件时(系统调用“brk”和“open”),这是需要的。

现在系统调用有短名称“brk”等和相应的操作码,这些是在系统特定的头文件中定义的。只要这些操作码保持不变,你就可以使用不同的更新内核运行相同的编译用户区程序,而无需重新编译。所以你有一个预编译二进制文件使用的接口,因此是ABI。

如果你知道组装以及操作系统级别的工作方式,那么你就遵守了某个ABI。ABI管理诸如如何传递参数、返回值放置在何处之类的事情。对于许多平台来说,只有一个ABI可供选择,在这种情况下,ABI只是“事物如何工作”。

然而,ABI还控制着类/对象在C++中的布局方式。如果您希望能够跨模块边界传递对象引用,或者如果您想混合使用不同编译器编译的代码,这是必要的。

此外,如果您有一个可以执行32位二进制文件的64位操作系统,您将对32位和64位代码有不同的ABI。

通常,您链接到同一可执行文件中的任何代码都必须符合相同的ABI。如果您想在使用不同ABI的代码之间进行通信,您必须使用某种形式的RPC或序列化协议。

我认为你太努力了,把不同类型的接口挤成一组固定的特征。例如,一个接口不一定要分成消费者和生产者。接口只是两个实体交互的约定。

ABI可以(部分)与ISA无关。某些方面(例如调用约定)依赖于ISA,而其他方面(例如C++类布局)则不依赖。

定义良好的ABI对于编写编译器的人非常重要。没有定义良好的ABI,就不可能生成可互操作的代码。

编辑:一些说明要澄清:

  • ABI中的“二进制”不排除使用字符串或文本。如果您想链接导出C++类的DLL,则必须在其中的某个地方对方法和类型签名进行编码。这就是C++名称修饰的用武之地。
  • 你从未提供过ABI的原因是绝大多数程序员永远不会这样做。ABI是由设计平台(即操作系统)的同一个人提供的,很少有程序员有特权设计一个广泛使用的ABI。

为了调用共享库中的代码,或者在编译单元之间调用代码,目标文件需要包含调用的标签。C++修改方法标签的名称,以便强制隐藏数据并允许重载方法。这就是为什么不能混合来自不同C++编译器的文件,除非它们明确支持相同的ABI。

简而言之,在哲学上,只有善良的东西才能相处得很好,ABI可以被看作是软件东西一起工作的善良

理解“ABI”的一个简单方法是将其与“API”进行比较。

您已经熟悉API的概念。如果您想使用某个库或您的操作系统的功能,您将针对API进行编程。API由数据类型/结构、常量、函数等组成,您可以在代码中使用它们来访问该外部组件的功能。

ABI非常相似。可以将其视为API的编译版本(或机器语言级别的API)。当你编写源代码时,你通过API访问库。代码编译后,你的应用程序通过ABI访问库中的二进制数据。ABI定义了编译后的应用程序将用来访问外部库(就像API一样)的结构和方法,但只在较低级别上。你的API定义了你将参数传递给函数的顺序。你的ABI定义了如何的机制这些参数被传递(寄存器、堆栈等)。您的API定义了哪些函数是库的一部分。您的ABI定义了您的代码如何存储在库文件中,以便使用您的库的任何程序都可以找到所需的函数并执行它。

当涉及到使用外部库的应用程序时,ABI很重要。库中充满了代码和其他资源,但是您的程序必须知道如何在库文件中找到它需要的内容。您的ABI定义了库的内容如何存储在文件中,您的程序使用ABI搜索文件并找到它需要的内容。如果您系统中的所有内容都符合相同的ABI,那么任何程序都可以使用任何库文件,无论谁创建了它们。Linux和Windows使用不同的ABI,因此Windows程序不知道如何访问为Linux编译的库。

有时,ABI的更改是不可避免的。发生这种情况时,任何使用该库的程序都将无法工作,除非重新编译以使用该库的新版本。如果ABI发生了变化,但API没有变化,那么新旧库版本有时被称为“源代码兼容”。这意味着虽然为一个库版本编译的程序不能与另一个库版本一起工作,但为一个库版本编写的源代码在重新编译时可以为另一个库版本工作。

因此,开发人员倾向于保持他们的ABI稳定(以尽量减少中断)。保持ABI稳定意味着不更改函数接口(返回类型和数量,类型和参数顺序)、数据类型或数据结构的定义、定义的常量等。可以添加新函数和数据类型,但现有的必须保持不变。例如,如果你的库使用32位整数来指示函数的偏移量,而你切换到64位整数,那么使用该库的已编译代码将无法正确访问该字段(或任何后续字段)。访问数据结构成员在编译期间被转换为内存地址和偏移量,如果数据结构发生变化,那么这些偏移量将不会指向代码期望它们指向的内容,结果充其量是不可预测的。

除非您正在进行非常低级的系统设计工作,否则您不一定会明确提供ABI。它也不是特定于语言的,因为(例如)C应用程序和Pascal应用程序可以在编译后使用相同的ABI。

编辑:关于SysV ABI文档中有关ELF文件格式章节的问题:包含此信息的原因是ELF格式定义了操作系统和应用程序之间的接口。当您告诉操作系统运行程序时,它期望程序以某种方式格式化,并且(例如)期望二进制文件的第一部分是包含特定内存偏移量的某些信息的ELF标头。这就是应用程序向操作系统传达有关自身的重要信息的方式。如果您以非ELF二进制格式(例如a.out或PE)构建程序,那么期望ELF格式的应用程序的操作系统将无法解释二进制文件或运行应用程序。这是Windows应用程序不能直接在Linux机器上运行(反之亦然)而无需重新编译或在可以从一种二进制格式转换为另一种二进制格式的某种仿真层中运行的一个重要原因。

IIRC,Windows目前使用可移植可执行文件(或PE)格式。该维基百科页面的“外部链接”部分有关于PE格式的更多信息的链接。

此外,关于你关于C++名称修饰的说明:在库文件中定位函数时,通常按名称查找该函数。C++允许你重载函数名称,因此仅名称不足以识别函数。C++编译器有自己的内部处理方式,称为名称修饰。ABI可以定义一种标准的函数名称编码方式,以便使用不同语言构建的程序或编译器可以找到他们需要的内容。当你在C++程序中使用extern "c"时,你是在指示编译器使用其他软件可以理解的标准化记录名称的方式。

应用程序二进制接口(ABI)类似于API,但调用者在源代码级别无法访问该函数。只有二进制表示可访问/可用。

ABI可以在处理器架构级别或操作系统级别定义。 ABI是编译器代码生成器阶段要遵循的标准。该标准由操作系统或处理器固定。

功能:定义独立于实现语言或特定编译器/链接器/工具链进行函数调用的机制/标准。提供允许JNI或Python-C接口等的机制。

现有实体:机器代码形式的函数。

消费者:另一个函数(包括另一种语言的函数、由另一个编译器编译或由另一个链接器链接)。

函数性:一组影响编译器、程序集编写器、链接器和操作系统的契约。契约规定了函数如何布局、参数传递到哪里、参数如何传递、函数返回如何工作。这些通常特定于(处理器架构、操作系统)元组。

现有实体:参数布局、函数语义学、寄存器分配。例如,ARM架构有许多ABI(APCS、EABI、GNU-EABI,更不用说一堆历史案例了)-使用混合ABI将导致你的代码在跨边界调用时根本无法工作。

消费者:编译器、汇编编写器、操作系统、CPU特定架构。

谁需要这些细节?编译器、汇编编写器、进行代码生成(或对齐要求)的链接器、操作系统(中断处理、系统调用接口)。如果你做了汇编编程,你就符合ABI!

C++名称修饰是一个特例——它是一个以链接器和动态链接器为中心的问题——如果名称修饰不标准化,那么动态链接将不起作用。从今以后,C++ABI就叫C++ABI。它不是链接器级别的问题,而是代码生成问题。一旦你有了C++的二进制文件,如果不从源代码重新编译,就不可能让它与另一个C++ABI兼容(名称修饰,异常处理)。

ELF是用于加载程序和动态链接器的文件格式。ELF是二进制代码和数据的容器格式,因此指定了一段代码的ABI。我不认为ELF是严格意义上的ABI,因为PE可执行文件不是ABI。

所有ABI都是指令集特定的。ARM ABI在MSP430或x86_64处理器上没有意义。

Windows有几个ABI——例如,快速调用和stdcall是两个常用的ABI。系统调用ABI又不同了。

应用程序二进制接口(ABI)

功能性:

  • 从程序员模型到底层系统域数据的转换 类型,大小,对齐,调用约定,控制如何 传递函数的参数并检索返回值;这 系统调用号码以及应用程序应如何进行系统调用 到操作系统;高级语言编译器的名称 修改方案、异常传播和调用约定 在同一平台上的编译器之间,但不需要 跨平台兼容性…

现有实体:

  • 直接参与程序执行的逻辑块:ALU, 通用寄存器,用于I/O的内存/I/O映射的寄存器等…

消费者:

  • 语言处理器链接器、汇编器…

如果你用汇编语言编写一个模块,另一个用Python编写,而不是你自己的引导加载程序想要使用操作系统,那么你的“应用程序”模块正在跨越“二进制”边界工作,并需要此类“接口”的协议。

C++名称修饰,因为可能需要在应用程序中链接来自不同高级语言的目标文件。考虑使用GCC标准库对使用VisualC++构建的Windows进行系统调用。

ELF是对来自对象文件的链接器进行解释的一种可能期望,尽管JVM可能有其他想法。

对于Windows RT Store应用程序,如果您真的希望使一些构建工具链协同工作,请尝试搜索ARM ABI。

区分ABI和API的最佳方法是了解原因以及它的用途:

对于x86-64,通常有一个ABI(对于x86 32位,还有另一组):

Linux+FreeBSD+MacOSX有一些细微的变化。Windows x64有自己的ABI:

了解ABI并假设其他编译器也遵循它,那么二进制文件理论上知道如何相互调用(特别是库API)并通过堆栈或寄存器等传递参数。或者在调用函数时将更改哪些寄存器等。本质上,这些知识将有助于软件相互集成。了解寄存器/堆栈布局的顺序,我可以轻松地将用程序集编写的不同软件拼凑在一起,而不会有太大问题。

但API不同:

它是一个高级函数名称,定义了参数,这样如果不同的软件部分使用这些API构建,可能能够相互调用。但必须遵守SAME ABI的附加要求。

例如,Windows曾经是POSIX API兼容的:

https://en.wikipedia.org/wiki/Windows_Services_for_UNIX

Linux也兼容POSIX。但是二进制文件不能简单地移动并立即运行。但是因为它们在符合POSIX的API中使用了相同的名称,您可以在C中使用相同的软件,在不同的操作系统中重新编译它,并立即让它运行。

API旨在简化软件的集成-预编译阶段。因此,如果ABI不同,编译后的软件看起来可能完全不同。

ABI旨在在二进制/汇编级别定义软件的精确集成。

你实际上不要需要一个ABI,如果--

  • 你的程序没有功能而且…
  • 您的程序是一个单独运行的可执行文件(即嵌入式系统),它实际上是唯一运行的东西,它不需要与其他任何东西交谈。

简单的总结:

API:“这是您可以调用的所有函数。

ABI:"这是如何调用函数。"

ABI是编译器和链接器为了编译您的程序以使其正常工作而遵守的一组规则。ABI涵盖多个主题:

  • 可以说,ABI中最大和最重要的部分是过程调用标准,有时也被称为“调用约定”。调用约定标准化了“函数”如何转换为汇编代码。
  • ABI还规定了库中暴露函数的名字应该如何表示,以便其他代码可以调用这些库并知道应该传递什么参数。这称为“名称修饰”。
  • ABI还规定了可以使用什么类型的数据类型、它们必须如何对齐以及其他低级细节。

深入了解调用约定,我认为它是ABI的核心:

机器本身没有“函数”的概念。当你用像c这样的高级语言写一个函数时,编译器会生成一行类似_MyFunction1:的汇编代码。这是一个标签,最终会被汇编器解析成一个地址。这个标签标志着你的“函数”在汇编代码中的“开始”。在高级代码中,当你“调用”那个函数时,你真正做的是导致CPU到该标签的地址并继续在那里执行。

在准备跳转时,编译器必须做一些重要的事情。调用约定就像编译器遵循的清单来完成所有这些事情:

  • 首先,编译器插入一点汇编代码来保存当前地址,这样当你的“功能”完成时,CPU就可以跳回正确的位置继续执行。
  • 接下来,编译器生成汇编代码来传递参数。
    • 一些调用约定规定参数应该放在堆栈上(当然是以特定的顺序)。
    • 其他约定规定参数应该放在特定的寄存器中(当然是取决于他们的数据类型)。
    • 还有一些约定规定应该使用堆栈和寄存器的特定组合。
  • 当然,如果这些寄存器中以前有任何重要的东西,那么这些值现在会被覆盖并永远丢失,因此一些调用约定可能会要求编译器在将参数放入其中之前保存其中的一些寄存器。
  • 现在编译器插入一条跳转指令,告诉CPU转到它之前制作的标签(_MyFunction1:)。此时,您可以认为CPU在您的“函数”中。
  • 在函数结束时,编译器放入一些汇编代码,这些代码将使CPU将返回值写入正确的位置。调用约定将决定返回值是应该放入特定寄存器(取决于其类型)还是堆栈。
  • 现在是清理的时候了。调用约定将决定编译器将清理汇编代码放置在哪里。
    • 一些约定说调用者必须清理堆栈。这意味着在“函数”完成并且CPU跳回之前的位置后,要执行的下一个代码应该是一些非常具体的清理代码。
    • 其他约定说,清理代码的某些特定部分应该在“函数”之前的末尾跳回。

有许多不同的ABI/调用约定。一些主要的是:

  • 对于x86或x86-64 CPU(32位环境):
    • CDECL
    • STDCALL
    • FASTCALL
    • VECTORCALL
    • 这个电话
  • 对于x86-64 CPU(64位环境):
    • SYSTEMV
    • 硕士
    • VECTORCALL
  • 对于ARM CPU(32位)
    • AAPCS
  • 对于ARM CPU(64位)
    • AAPCS64

这里是一个很棒的页面,它实际上显示了为不同的ABI编译时生成的程序集的差异。

另一件需要提及的事情是,ABI不仅与程序的可执行模块里面相关。链接器使用它来确保你的程序正确调用库函数。你的计算机上运行着多个共享库,只要你的编译器知道它们各自使用的ABI,它就可以正确地从它们中调用函数,而不会炸毁堆栈。

您的编译器了解如何调用库函数是非常重要的。在托管平台(即操作系统加载程序的平台)上,您的程序甚至不能在不调用内核的情况下闪烁。

总结

对定义ABI(应用程序二进制接口)的确切层有各种解释和强烈的意见。

在我看来,ABI是特定API的给定/平台的主观约定。ABI是特定API“不会改变”或将由运行时环境处理的“其余”约定:执行器、工具、链接器、编译器、jvm和操作系统。

定义接口:ABI、API

如果您想使用像joda-time这样的库,您必须声明对joda-time-<major>.<minor>.<patch>.jar的依赖。该库遵循最佳实践并使用语义版本控制。这在三个级别定义了API兼容性:

  1. 补丁-您根本不需要更改代码。该库只是修复了一些错误。
  2. 未成年人-你不需要改变你的代码,因为添加的东西(开放封闭原则得到尊重)
  3. 主要-接口(API)已更改,您可能需要更改代码。

为了让您使用同一个库的新主要版本,仍然需要尊重许多其他约定:

  • 用于库的二进制语言(在Java情况下是定义Java字节码的JVM目标版本)
  • 召集公约
  • JVM约定
  • 链接约定
  • 运行时约定 所有这些都由我们使用的工具定义和管理。

示例

Java案例研究

例如,Java在一个正式的JVM规范中而不是在一个工具中对所有这些约定进行了标准化,该规范允许其他供应商提供一组可以输出兼容库的不同工具。

Java为ABI提供了另外两个有趣的案例研究:Scala版本和Dalvik虚拟机。

Dalvik虚拟机打破了ABI

Dalvik虚拟机需要与Java字节码不同类型的字节码。Dalvik库是通过为Dalvik转换Java字节码(具有相同API)获得的。通过这种方式,你可以获得相同API的两个版本:由原始joda-time-1.7.2.jar定义。我们可以称之为joda-time-1.7.2.jarjoda-time-1.7.2-dalvik.jar。它们使用不同的ABI用于面向堆栈的标准Java虚拟机:Oracle的一个、IBM的一个、开放Java或任何其他;第二个ABI是Dalvik周围的ABI。

Scala后续版本不兼容

Scala次要版本之间没有二进制兼容性:2. X。因此,同一个API“io.reactivex”%%“rxscala”%“0.26.5”有三个版本(将来会有更多):用于Scala 2.10、2.11和2.12。有什么变化?我现在还不知道,但二进制文件不兼容。可能最新版本添加了一些东西,使库在旧虚拟机上无法使用,可能与链接/命名/参数约定有关。

Java连续版本不兼容

Java在JVM的主要版本上也有问题:4,5,6,7,8,9。它们只提供向后兼容性。Jvm9知道如何为所有其他版本运行编译/目标代码(javac的-target选项),而JVM 4不知道如何运行针对JVM 5的代码。所有这些都是在你有一个joda库的情况下发生的。由于不同的解决方案,这种不兼容性在雷达下飞翔:

  1. 语义版本控制:当库面向更高的JVM时,它们通常会更改主要版本。
  2. 使用JVM 4作为ABI,您是安全的。
  3. Java9添加了关于如何在同一个库中包含特定目标JVM的字节码的规范。

为什么我从API定义开始?

API和ABI只是关于如何定义兼容性的约定。较低层在大量高级语义学方面是通用的。这就是为什么制定一些约定很容易。第一种约定是关于内存对齐、字节编码、调用约定、大端和小端编码等。在它们之上,你会得到像其他描述的可执行约定,链接约定,中间字节码,就像Java使用的那样,或者GCC使用的LLVM IR。第三,你会得到关于如何查找库、如何加载它们的约定(参见Java类加载器)。随着概念越来越高,你会有新的约定,你认为这些约定是给定的。这就是为什么他们没有进入语义版本控制。它们在主要版本中是隐式的或折叠的。我们可以用<major>-<minor>-<patch>-<platform/ABI>修改语义版本控制。这实际上已经发生了:平台已经是rpmdlljar(JVM字节码)、war(jvm+Web服务器)、apk2.11(特定的Scala版本)等。当你说APK时,你已经谈论了API的特定ABI部分。

API可以移植到不同的ABI

抽象的顶层(针对最高API编写的源代码可以重新编译/移植到任何其他较低级别的抽象。

假设我有一些rxscala的源代码。如果Scala工具改变了,我可以重新编译它们。如果JVM改变了,我可以自动从旧机器转换到新机器,而不用担心高级概念。虽然移植可能很困难,但对任何其他客户端都有帮助。如果使用完全不同的汇编代码创建了一个新的操作系统,就可以创建一个翻译器。

跨语言移植的API

有些API是以多种语言移植的,比如反应流。一般来说,它们定义了到特定语言/平台的映射。我认为API是以人类语言甚至特定编程语言正式定义的主规范。所有其他“映射”在某种意义上都是ABI,否则比通常的ABI更多的API。REST接口也是如此。

我也试图理解ABI,JesperE的回答非常有帮助。

从一个非常简单的角度来看,我们可以通过考虑二进制兼容性来理解ABI。

KDE wiki将库定义为二进制兼容的,“如果动态链接到库的旧版本的程序继续与库的新版本一起运行,而无需重新编译。”有关动态链接的更多信息,请参阅静态链接vs动态链接

现在,让我们尝试看看库二进制兼容性所需的最基本方面(假设库没有源代码更改):

  1. 相同/向下兼容的指令集架构(处理器指令、寄存器文件结构、堆栈组织、内存访问类型,以及处理器可以直接访问的基本数据类型的大小、布局和对齐)
  2. 相同的呼叫惯例
  3. 同名修饰约定(如果一个Fortran程序需要调用一些C++库函数,这可能是需要的)。

当然,还有许多其他细节,但这主要是ABI也涵盖的内容。

更具体地回答你的问题,从上面,我们可以推断:

ABI功能:二进制兼容性

现有实体:现有程序/库/操作系统

消费者:图书馆、操作系统

希望这有帮助!

术语ABI用于指两个不同但相关的概念。

当谈论编译器时,它指的是用于从源级结构转换为二进制结构的规则。数据类型有多大?堆栈如何工作?我如何将参数传递给函数?调用者和被调用者应该保存哪些寄存器?

当谈到库时,它指的是编译库提供的二进制接口。这个接口是许多因素的结果,包括库的源代码、编译器使用的规则以及在某些情况下从其他库中获取的定义。

对库的更改可以在不破坏API的情况下破坏ABI。例如,考虑一个具有类似接口的库。

void initfoo(FOO * foo)
int usefoo(FOO * foo, int bar)
void cleanupfoo(FOO * foo)

应用程序程序员编写代码,例如

int dostuffwithfoo(int bar) {
FOO foo;
initfoo(&foo);
int result = usefoo(&foo,bar)
cleanupfoo(&foo);
return result;
}

应用程序程序员不关心FOO的大小或布局,但应用程序二进制文件最终会以foo的硬编码大小结束。如果库程序员向foo添加了一个额外的字段,并且有人将新库二进制文件与旧应用程序二进制文件一起使用,那么库可能会进行越界内存访问。

OTOH如果库作者设计了他们的API。

FOO * newfoo(void)
int usefoo(FOO * foo, int bar)
void deletefoo((FOO * foo, int bar))

应用程序程序员编写代码,例如

int dostuffwithfoo(int bar) {
FOO * foo;
foo = newfoo();
int result = usefoo(foo,bar)
deletefoo(foo);
return result;
}

然后应用程序二进制文件不需要知道FOO的结构,这些都可以隐藏在库中。不过,您为此付出的代价是涉及堆操作。

Linux共享库最小可运行ABI示例

在共享库的上下文中,“拥有稳定的ABI”最重要的含义是,在库更改后,您不需要重新编译程序。

例如:

  • 如果您正在销售共享库,您可以为您的用户节省重新编译每个新版本依赖于您的库的所有内容的烦恼

  • 如果您正在销售依赖于用户发行版中存在的共享库的闭源程序,如果您确定ABI在目标操作系统的某些版本中是稳定的,则可以发布和测试较少的预构建。

    这在C标准库的情况下特别重要,您系统中的许多程序都链接到它。

现在我想提供一个最小的具体可运行的例子。

main. c

#include <assert.h>
#include <stdlib.h>


#include "mylib.h"


int main(void) {
mylib_mystruct *myobject = mylib_init(1);
assert(myobject->old_field == 1);
free(myobject);
return EXIT_SUCCESS;
}

mylib. c

#include <stdlib.h>


#include "mylib.h"


mylib_mystruct* mylib_init(int old_field) {
mylib_mystruct *myobject;
myobject = malloc(sizeof(mylib_mystruct));
myobject->old_field = old_field;
return myobject;
}

mylib. h

#ifndef MYLIB_H
#define MYLIB_H


typedef struct {
int old_field;
} mylib_mystruct;


mylib_mystruct* mylib_init(int old_field);


#endif

编译并运行良好:

cc='gcc -pedantic-errors -std=c89 -Wall -Wextra'
$cc -fPIC -c -o mylib.o mylib.c
$cc -L . -shared -o libmylib.so mylib.o
$cc -L . -o main.out main.c -lmylib
LD_LIBRARY_PATH=. ./main.out

现在,假设对于库的v2,我们想向mylib_mystruct添加一个名为new_field的新字段。

如果我们在old_field之前添加字段,如:

typedef struct {
int new_field;
int old_field;
} mylib_mystruct;

并重建了库,但不是main.out,则断言失败!

这是因为该行:

myobject->old_field == 1

生成的程序集试图访问结构的第一个int,现在是new_field而不是预期的old_field

这一变化打破了ABI。

但是,如果我们在old_field之后添加new_field

typedef struct {
int old_field;
int new_field;
} mylib_mystruct;

然后旧生成的程序集仍然访问结构的第一个int,并且程序仍然有效,因为我们保持了ABI的稳定。

这是一个GitHub上此示例的全自动版本

保持此ABI稳定的另一种方法是将mylib_mystruct视为不透明结构,并仅通过方法助手访问其字段。这使得保持ABI稳定更容易,但会产生性能开销,因为我们会进行更多函数调用。

api vs abi

在前面的示例中,有趣的是在old_field之前添加new_field只会破坏ABI,而不是API。

这意味着,如果我们针对库重新编译了我们的main.c程序,它无论如何都会工作。

但是,如果我们更改了例如函数签名,我们也会破坏API:

mylib_mystruct* mylib_init(int old_field, int new_field);

因为在这种情况下,main.c将完全停止编译。

语义API vs编程API

我们还可以将API更改分类为第三种类型:语义更改。

语义API通常是API应该做什么的自然语言描述,通常包含在API留档中。

因此,可以在不破坏程序构建本身的情况下破坏语义API。

例如,如果我们修改了

myobject->old_field = old_field;

到:

myobject->old_field = old_field + 1;

那么这既不会破坏编程API,也不会破坏ABI,但语义API会破坏main.c

有两种方法可以以编程方式检查合约API:

破坏C/C++共享库ABI的所有内容列表

待办事项:查找/创建最终列表:

Java最小可运行示例

什么是Java中的二进制兼容性?

在Ubuntu 18.10、GCC 8.2.0中测试。

应用程序二进制接口(ABI)

ABI-Application Binary Interface是关于运行时中两个二进制零件之间的机器代码通信-应用程序,库,操作系统…ABI描述了对象如何保存在内存中,函数如何调用(calling convention),修饰…

API和ABI的一个很好的例子是v5中的使用Swift语言的iOS生态系统

  • Application layer-当您使用不同的语言创建应用程序时。例如,您可以使用SwiftObjective-C[混合Swift和Objective-C]创建应用程序

  • Application - OS layer-runtime-Swift Standard LibrarySwift Run Time Library关于我们是操作系统的一部分,它们不应该包含在每个包(例如应用程序、框架)中。它与Objective-C的使用相同。可从iOSv12.2获得

  • Library layer-Module Stability case-编译时间-您将能够导入使用另一个版本的Swift编译器构建的框架。这意味着创建闭源(预构建)二进制文件是安全的,该二进制文件将被不同版本的编译器使用(.swiftinterface.swiftmodule关于我们一起使用),并且您将不会得到

    Module compiled with _ cannot be imported by the _ compiler
    //or
    Compiled module was created by a newer version of the compiler
    
  • Library layer-Library Evolution case

    1. 编译时-如果依赖项已更改,则客户端不得更改 重新编译。
    2. 运行时-系统库或动态框架可以 被一个新的热交换。

[API vs ABI]
Swift模块稳定性和库稳定性

答:简单地说,ABI与API的一个共同点是它是一个接口。可重用的程序公开了一个稳定的接口(API),可用于在另一个程序中重用该程序。

然而,ABI是为某种特定语言的特定处理器平台发布的接口。所有希望以该语言为目标的编译器供应商都必须确保不仅可重定位目标代码形式的编译代码符合该接口,以便能够相互链接和交叉链接,而且可执行文件也符合该接口,以便能够在该平台上运行。因此,ABI比典型的函数API更广泛的规范/标准集。它可能包括编译器强制执行给语言用户的一些API对象。编译器供应商必须在他们的发行版中包含对相同API的支持。不用说,平台供应商是为其平台发布ABI的合法权威。编译器供应商和ABI都需要遵守相应的语言标准(例如C++的ISO标准)。

C.平台供应商的ABI的定义是:

"1.为了在特定的执行环境中执行,可执行文件必须符合的规范。例如,Arm架构的LinuxABI。

  1. 独立生成的可重定位文件必须符合的规范的一个特定方面,才能静态链接和执行。例如,Arm架构的C++ABI、Arm架构的运行时ABI、Arm架构的C库ABI。”

例如,基于安腾架构的C++通用ABI也由一个联盟提出发布。平台供应商自己的C++ABI在多大程度上遵守它完全取决于平台供应商。

再举一个例子,Arm架构的C++ABI是这里

F.话虽如此,在幕后,处理器架构的ABI将确保一个可重用程序和另一个重用它的程序之间的API适用于该处理器架构。

这就把我们带到了面向服务的组件(例如基于SOAP的Web服务)。它们也需要一个API存在于基于SOAP的Web服务和客户端程序(可以是应用程序、前端或其他Web服务)之间,以便客户端程序重用Web服务。它不针对任何特定的处理器平台,因此它不是像ABI那样的“二进制”。任何一种平台类型并用任何语言编写的客户端程序都可以远程重用用任何其他语言编写并托管在完全不同的处理器平台上的Web服务。这是由于WSDL和SOAP都是基于文本的(XML)协议。对于RESTful Web服务,传输协议超文本传输协议-也是一个基于文本的协议-本身充当API(CRUD方法)。