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


492

我从不清楚什么是ABI。请不要将我指向Wikipedia文章。如果我能理解的话,我就不会在这里发表如此冗长的文章。

这是我对不同接口的看法:

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

接口:是该功能的functionality和之间的“现有实体”层 consumer。接口本身不执行任何操作。它只是调用后面的功能。

现在,取决于用户是谁,有不同类型的界面。

命令行界面(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应用程序二进制接口版本4.1(1997-03-18),以查看其确切包含了什么。好吧,大多数都没有任何意义。

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

  • 我知道,由于我们的讨论水平很低,因此必须非常具体。但是我不确定“指令集体系结构(ISA)”具体如何?

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

因此,这些是困扰我的主要查询。


7
“请不要说,操作系统”编译器需要了解ABI。链接器需要了解ABI。内核需要知道ABI才能在RAM中设置程序以使其正常运行。至于C ++,请参见下文,由于重载和私有方法,它有意将标签变成乱码,并且链接器和任何其他编译器都需要具有兼容的名称修饰才能使用它,换句话说,就是相同的ABI。
贾斯汀·史密斯

8
我认为问题很明确。准确描述预期的答案格式,但没有一个可以接受的令人满意的答案。
legends2k 2010年

3
@ legends2k我对这个问题的看法是,OP确实知道什么是ABI,但没有意识到这一点。绝大多数程序员永远不会设计或提供ABI,因为这是OS /平台设计人员的工作。
JesperE

4
@JesperE:我同意你的观点。但OP可能想以他/她认为合适的格式清楚地知道它,即使他/他可能不需要提供ABI。
legends2k 2010年

2
我很无知 最近在处理所有这些事情时。我意识到ABI实际上是什么。是的,我同意我的模板有问题。不适合将ABI放入我的模板中。谢谢@ JasperE。只是需要工作经验才能实现您的答案。
抓取

Answers:


533

理解“ 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格式定义了操作系统和应用程序之间的接口。当您告诉OS运行程序时,它期望程序以某种方式进行格式化,并且(例如)期望二进制文件的第一部分是一个ELF标头,其中包含位于特定内存偏移处的某些信息。这是应用程序将有关其自身的重要信息传达给操作系统的方式。如果以非ELF二进制格式(例如a.out或PE)构建程序,则要求ELF格式的应用程序的操作系统将无法解释该二进制文件或运行该应用程序。

IIRC,Windows当前使用可移植可执行(或PE)格式。该Wikipedia页面的“外部链接”部分中包含一些链接,其中包含有关PE格式的更多信息。

另外,关于您有关C ++名称修改的注释:在库文件中定位函数时,通常会按名称查找该函数。C ++允许您重载函数名称,因此仅使用名称不足以标识函数。C ++编译器在内部有自己的处理方式,称为名称修改。ABI可以定义对函数名称进行编码的标准方法,以便使用其他语言或编译器构建的程序可以找到所需的内容。当您使用extern "c"的C ++程序,你告诉编译器使用记录的名字这是其他软件理解的标准化方式。


2
@bta,谢谢您的出色回答。调用约定是一种ABI吗?谢谢
卡米诺2014年

37
好答案。除此之外,这不是ABI。ABI是一组确定调用约定的规则,以及用于布局结构的规则。Pascal以与C应用程序相反的顺序在堆栈上传递参数,因此pascal和C编译器不会编译为相同的ABI。C和Pascal编译器的各自标准隐含地确保确实如此。C ++编译器无法定义名称的“标准”方式,因为没有标准方式。当Windows上存在竞争的C ++编译器时,C ++名称转换约定在C ++编译器之间不兼容。
罗宾·戴维斯


1
@RobinDavies:在Pascal编译器将调用其调用者给定的函数pop参数的平台上,C编译器通常会定义一种方式,程序员可以通过该方式指示特定函数应使用或应该使用与调用函数相同的调用约定。 Pascal编译器即使C编译器通常在默认情况下通常会使用约定,即被调用函数将调用方放置在堆栈上的所有内容保留在堆栈上。
超级猫

我可以说C编译器生成的obj文件包含ABI吗?
米图拉吉

144

如果您知道汇编以及它们在OS级别上的工作方式,那么您就符合某种ABI。ABI控制诸如如何传递参数,放置返回值的地方。对于许多平台,只有一个ABI可供选择,在这种情况下,ABI只是“事情的工作方式”。

但是,ABI还可以控制诸如用C ++布置类/对象的方式。如果您希望能够跨模块边界传递对象引用,或者要混合使用其他编译器编译的代码,则这是必需的。

另外,如果您具有可以执行32位二进制文​​件的64位OS,则对于32位和64位代码,您将具有不同的ABI。

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

我认为您正在努力尝试将不同类型的接口压缩为一组固定的特征。例如,接口不一定必须分为消费者和生产者。接口只是两个实体进行交互的约定。

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

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

编辑:一些说明,以澄清:

  • ABI中的“二进制”不排除使用字符串或文本。如果要链接导出C ++类的DLL,则必须在其中某处对方法和类型签名进行编码。这就是C ++名称处理出现的地方。
  • 您从未提供ABI的原因是,绝大多数程序员永远都不会这样做。ABI是由设计平台(即操作系统)的同一个人提供的,并且几乎没有程序员能够设计出广泛使用的ABI。

我根本不相信我的模板有问题。因为此接口的每个模板都适用。所以,是的,我希望我也希望ABI也适合此模板,但事实并非如此。重要的事情是我还是不明白。我不知道我是不是很笨或其他什么,但是只是没有进入我的脑海。我无法实现答案和维基文章。
爪子

2
@jesperE,“ ABI控制如何传递参数,放置返回值的地方。”是指“ cdecl,stdcall,fastcall,pascal”对吗?
卡米诺2014年

3
是。专有名称是“呼叫约定”,它是ABI的一部分。en.wikipedia.org/wiki/X86_calling_conventions
JesperE

4
这是正确和准确,而不冗长的答案(而噪音)!
Nawaz

我建议编写一些汇编。这将帮助人们更切实地理解ABI。
蔡坤瑜

40

在以下情况下,您实际上根本不需要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
    • 快讯
    • 矢量呼叫
    • 此电话
  • 对于x86-64 CPU(64位环境):
    • SYSTEMV
    • MSNATIVE
    • 矢量呼叫
  • 对于ARM CPU(32位)
    • 亚太会计准则委员会
  • 对于ARM CPU(64位)
    • AAPCS64

是一个很棒的页面,实际显示了针对不同的ABI进行编译时所生成的程序集的差异。

要提到的另一件事是,ABI不仅与程序的可执行模块内部相关。链接器使用它来确保您的程序正确调用库函数。您在计算机上运行着多个共享库,只要您的编译器知道它们各自使用的ABI,它就可以正确地从它们调用函数而不会耗尽堆栈。

您的编译器了解如何调用库函数非常重要。在托管平台(即操作系统在其中加载程序的平台)上,如果不进行内核调用,您的程序甚至无法闪烁。


19

应用程序二进制接口(ABI)与API相似,但是调用者无法在源代码级别访问该函数。仅二进制表示形式可访问/可用。

可以在处理器体系结构级别或OS级别定义ABI。ABI是编译器的代码生成器阶段之后的标准。该标准由操作系统或处理器固定。

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

现有实体:机器代码形式的功能。

使用者:另一个功能(包括另一种语言的功能,由另一种编译器编译,或由另一种链接器链接)。


为什么用该架构定义ABI?为什么同一体系结构上的不同OS无法定义不同的ABI?
安德烈亚斯·哈弗堡

10

功能:一组影响编译器,程序集编写器,链接器和操作系统的协定。合同规定了函数的布局方式,参数的传递位置,参数的传递方式,函数的返回方式。这些通常是特定于(处理器体系结构,操作系统)元组的。

现有实体:参数布局,函数语义,寄存器分配。例如,ARM体系结构具有许多ABI(APCS,EABI,GNU-EABI,不用管很多历史案例)-使用混合的ABI将导致您的代码在跨边界调用时根本无法正常工作。

使用者:编译器,程序集编写器,操作系统,CPU特定体系结构。

谁需要这些细节?编译器,程序集编写器,执行代码生成(或对齐要求)的链接器,操作系统(中断处理,syscall接口)。如果您进行了汇编编程,那么您正在遵循ABI!

C ++名称处理是一种特殊情况-它是链接器和动态链接器中心的问题-如果名称处理未标准化,则动态链接将不起作用。从此以后,C ++ ABI简称为C ++ ABI。这不是链接程序级别的问题,而是代码生成的问题。一旦有了C ++二进制文件,就无法不从源重新编译而使其与另一个C ++ ABI(名称处理,异常处理)兼容。

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

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

Windows有几个ABI-例如,fastcall和stdcall是两个常用的ABI。syscall ABI再次不同。


9

让我至少回答您问题的一部分。举例说明Linux ABI如何影响系统调用,以及为什么这样做有用。

系统调用是用户空间程序向内核空间询问某些内容的一种方式。通过将用于调用的数字代码和参数放在某个寄存器中并触发中断来工作。与切换到内核空间相比,内核查找数字代码和参数,处理请求,将结果放回寄存器并触发切换回用户空间。例如,当应用程序要分配内存或打开文件(系统调用“ brk”和“ open”)时,这是必需的。

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


4

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


4

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

对于x86-64,通常存在一个ABI(对于x86 32位,则存在另一组):

http://www.x86-64.org/documentation/abi.pdf

https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/140-x86-64_Function_Calling_Conventions/x86_64.html

http://people.freebsd.org/~obrien/amd64-elf-abi.pdf

Linux + FreeBSD + MacOSX稍有变化。Windows x64具有自己的ABI:

http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/

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

但是API有所不同:

这是一个高级函数名称,具有定义的参数,因此,如果使用这些API构建不同的软件,则可以相互调用。但是必须遵守SAME ABI的其他要求。

例如,Windows过去一直兼容POSIX API:

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

https://zh.wikipedia.org/wiki/POSIX

而且Linux也符合POSIX。但是二进制文件不能仅移至立即运行。但是,因为他们在POSIX兼容API中使用了相同的名称,所以您可以在C中使用相同的软件,在不同的OS中重新编译它,然后立即使其运行。

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

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


Windows x86-64调用约定不使用所有其他x86-64 OS使用的SysV调用约定。Linux / OS X / FreeBSD都共享相同的调用约定,但是它们共享完整的ABI。操作系统的ABI包含系统调用号码。例如freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/…表示SYS_execve在32位linux上为11,而在FreeBSD上为59。
彼得·科德斯

感谢您的评论,我修改了评论以更好地回答ABI和API之间的区别。
Peter Teoh

您仍然缺少调用约定和完整的ABI(系统调用以及所有内容)之间的区别。您可以在Linux上运行某些FreeBSD二进制文件,因为Linux(内核)提供了FreeBSD兼容性层。即使这样,它也仅限于不尝试使用Linux未提供的FreeBSD ABI的任何部分的二进制文件。(例如,任何仅FreeBSD的系统调用)。与ABI兼容意味着您可以在两个系统上运行相同的二进制文件,而不仅仅是它们的编译方式类似。
彼得·科德斯

“ FreeBSD兼容性层”,从未听说过。您可以指向相关的Linux内核源代码吗?但是确实存在相反的情况: freebsd.org/doc/en_US.ISO8859-1/books/handbook/linuxemu.html
Peter Teoh

这不是我用的东西。我以为这样的东西已经存在了,但也许现在不复存在了。 tldp.org/HOWTO/Linux+FreeBSD-6.html说它是未维护的,该方法是从2000开始的。xDunix.stackexchange.com/questions/172038/…确认它已被放弃并且从未重做(因为没有人非常想要它来完成它)。 personality(2)可以设置PER_BSD。我想我一直记得personality(PER_LINUX)strace输出中看到过,但是现代的64位Linux二进制文件不再这样做了。
彼得·科德斯

4

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

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

因此,例如:

  • 如果您要出售共享库,则可以避免用户为每个新版本重新编译依赖于库的所有内容的烦恼

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

    对于C标准库,这尤其重要,系统中许多程序都链接到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。

但是,如果new_fieldold_field以下位置添加:

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

然后int,由于我们保持了ABI的稳定,因此旧生成的程序集仍然会访问该结构的第一个,程序仍然可以正常工作。

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

保持此ABI稳定的另一种方法是将其mylib_mystruct视为不透明的结构,并且只能通过方法助手来访问其字段。这样可以更轻松地保持ABI稳定,但是会增加性能开销,因为我们需要进行更多的函数调用。

API与ABI

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

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

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

mylib_mystruct* mylib_init(int old_field, int new_field);

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

语义API与编程API

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

语义API通常是API应该执行的自然语言描述,通常包含在API文档中。

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

例如,如果我们修改了

myobject->old_field = old_field;

至:

myobject->old_field = old_field + 1;

那么这将不会破坏编程API或ABI,但是main.c语义API会破坏。

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

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

TODO:查找/创建最终列表:

Java最小可运行示例

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

在Ubuntu 18.10,GCC 8.2.0中进行了测试。



3

摘要

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

在我看来,ABI是针对特定API的给定/平台的主观约定。对于特定的API或将由运行时环境解决的约定,ABI是“其余”约定,即执行程序,工具,链接器,编译器,jvm和OS。

定义接口: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 VM需要与Java字节码不同类型的字节码。Dalvik库是通过转换Dalvik的Java字节码(具有相同的API)获得的。通过这种方式,您可以获得相同API的两个版本:由original定义joda-time-1.7.2.jar。我们可以打电话给我joda-time-1.7.2.jarjoda-time-1.7.2-dalvik.jar。他们为面向堆栈的标准Java vm使用了不同的ABI:Oracle的,IBM的,开放式Java或其他任何一种。第二个ABI是围绕Dalvik的那个。

Scala后续版本不兼容

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知道如何-target为所有其他版本运行经过编译/定位的代码(javac的选项),而JVM 4则不知道如何运行针对JVM 5的代码。所有这些,只有一个joda库。由于采用了不同的解决方案,这种不兼容性使雷达望而却步:

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

为什么从API定义开始?

API和ABI只是有关定义兼容性的约定。在众多高级语义方面,较低的层是通用的。这就是为什么容易制定一些约定的原因。第一种约定是关于内存对齐,字节编码,调用约定,大尾数编码和小尾数编码等。在它们之上,您还可以像其他描述的文件那样获得可执行的约定,链接约定,中间字节代码(例如Java或Java所使用的)。 GCC使用的LLVM IR。第三,您获得了有关如何查找库,如何加载它们的约定(请参阅Java类加载器)。随着概念的发展,您将拥有一些新的约定,这些约定被认为是给定的。这就是为什么他们没有使用语义版本控制的原因版。我们可以使用修改语义版本<major>-<minor>-<patch>-<platform/ABI>。这就是实际发生的已经:平台已经是一个rpmdlljar(JVM字节码),war(JVM + Web服务器)apk2.11(具体斯卡拉版)等。当您说APK时,您已经在谈论API的特定ABI部分。

API可以移植到不同的ABI

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

假设我有一些rxscala的资源。如果更改了Scala工具,我可以将其重新编译为该工具。如果JVM发生了变化,我可以在不打扰高级概念的情况下,从旧机器自动转换到新机器。虽然移植可能很困难,但会帮助任何其他客户端。如果使用完全不同的汇编代码创建新的操作系统,则可以创建翻译器。

跨语言移植的API

有一些API可以使用多种语言进行移植,例如反应式流。通常,它们定义到特定语言/平台的映射。我认为API是用人类语言甚至特定的编程语言正式定义的主规范。从某种意义上说,所有其他“映射”都是ABI,比通常的ABI还要多的API。REST接口也发生了同样的情况。


1

简而言之,从哲学上讲,只有一种事物能够相处得很好,而ABI可以看作是一种软件可以协同工作的事物。


1

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

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

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

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

  1. 相同/向后兼容的指令集体系结构(处理器指令,寄存器文件结构,堆栈组织,内存访问类型,以及处理器可以直接访问的基本数据类型的大小,布局和对齐方式)
  2. 相同的调用约定
  3. 同名处理约定(如果说Fortran程序需要调用某些C ++库函数,则可能需要)。

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

更具体地说,为回答您的问题,从以上我们可以得出:

ABI功能:二进制兼容性

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

使用者:库,操作系统

希望这可以帮助!


1

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

功能:

  • 从程序员模型到底层系统的域数据类型,大小,对齐方式,调用约定的转换,后者控制如何传递函数的自变量以及如何获取返回值;系统调用号以及应用程序应如何对操作系统进行系统调用;高级语言编译器的名称处理方案,异常传播和同一平台上的编译器之间的调用约定,但是不需要跨平台兼容性...

现有实体:

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

消费者:

  • 语言处理器链接器,汇编器...

那些必须确保构建工具链可以整体工作的人都需要这些。如果您用汇编语言编写一个模块,然后用Python编写另一个模块,而不是自己的引导加载程序想要使用操作系统,那么您的“应用程序”模块将跨越“二进制”边界工作,并且需要此类“接口”达成一致。

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

尽管JVM可能还有其他想法,但ELF可能是目标文件对链接器的一种解释。

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


1

术语“ 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的结构的知识,这些知识都可以隐藏在库中。您为此付出的代价是涉及堆操作。


0

ABI- Application Binary Interface关于运行时两个二进制程序部分(例如,应用程序,库,OS)之间的机器代码通信,ABI描述了对象如何保存在内存中以及如何调用函数(calling convention

API和ABI的一个很好的例子是带有Swift语言的iOS生态系统

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

  • Application - OS-运行- Swift runtimestandard libraries是OS的部件并且它们不应被包括到每个束(例如应用程序,框架)。和Objective-C的用法一样

  • Library- Module Stability案例- 编译时 -你将能够导入其与斯威夫特的编译器的另一个版本建立了一个框架。这意味着创建一个封闭源代码(预构建)二进制文件是安全的,该二进制文件将由其他版本的编译器.swiftinterface使用(与一起使用.swiftmodule),并且您不会

    Module compiled with _ cannot be imported by the _ compiler
    
  • Library- Library Evolution情况

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

[API vs ABI]

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.