什么是应用程序二进制接口(ABI)?
我从来不清楚ABI是什么。 请不要指向我的维基百科文章。 如果我能理解的话,我不会在这里发表如此冗长的post。
这是我对不同接口的看法:
电视遥控器是用户和电视机之间的接口。 它是一个现有的实体,但它本身没有用(不提供任何function)。 遥控器上每个button的所有function都在电视机中实现。
接口:它是该
functionality
和consumer
之间的“现有实体”层。 一个接口本身是不会做任何事情的。 它只是调用后面的function。现在取决于谁是用户是有不同types的接口。
命令行界面(CLI)命令是现有的实体,消费者是用户,function在后面。
functionality:
我的软件function,它解决了我们正在描述这个接口的一些目的。
existing entities:
命令
consumer:
用户graphics用户界面(GUI)窗口,button等是现有的实体,消费者也是用户,function在后面。
functionality:
我的软件function,它解决了我们正在描述这个接口的一些目的。
existing entities:
窗口,button等。
consumer:
用户应用程序编程接口(API)函数或更正确的接口(在基于接口的编程中)是现有的实体,这里的消费者是另一个程序而不是用户,并且function也在这个层之后。
functionality:
我的软件function,它解决了我们正在描述这个接口的一些目的。
existing entities:
函数,接口(函数的数组)。
consumer:
另一个程序/应用程序应用程序二进制接口(ABI)这是我的问题开始的地方。
functionality:
???
existing entities:
???
consumer:
???
- 我用不同的语言编写了软件,提供了不同的界面(CLI,GUI和API),但是我不确定是否曾经提供过任何ABI。
维基百科说:
ABI涵盖了诸如
- 数据types,大小和alignment;
- 调用约定,它控制如何传递函数的参数和返回值检索;
- 系统调用号码以及应用程序如何对操作系统进行系统调用;
其他ABI标准化的细节,如
- C ++的名字改变,
- exception传播和
- 在同一平台上调用编译器之间的约定,但不要求跨平台兼容性。
-
谁需要这些细节? 请不要说操作系统。 我知道程序集编程。 我知道如何链接和加载工作。 我知道里面到底发生了什么。
-
为什么C ++的名字来了? 我以为我们在二元层面上谈论。 为什么语言进来?
无论如何,我已经下载了[PDF] System V应用程序二进制接口版4.1(1997-03-18) ,看看它究竟包含了什么。 那么,其中大部分没有任何意义。
-
为什么它包含两章(第四和第五)来描述ELF文件格式? 事实上,这是规范中唯一的两个重要章节。 其余章节是“处理器具体”。 无论如何,我认为这是一个完全不同的话题。 请不要说ELF文件格式规范是 ABI。 根据定义,它没有资格成为一个接口 。
-
我知道,由于我们的谈话水平如此之低,所以必须非常具体。 但我不确定它是如何“指令集架构(ISA)”具体?
-
我在哪里可以findMicrosoft Windows ABI?
所以,这些是困扰我的主要疑问。
理解“ABI”的一个简单方法是将其与“API”进行比较。
您已经熟悉API的概念。 如果你想使用某些库或你的操作系统的function,你将使用一个API。 API由数据types/结构,常量,函数等组成,您可以在代码中使用这些数据来访问该外部组件的function。
ABI非常相似。 把它看作API的编译版本(或者是机器语言级别的API)。 当您编写源代码时,您可以通过API访问该库。 代码编译完成后,应用程序通过ABI访问库中的二进制数据。 ABI定义了你编译的应用程序将用来访问外部库的结构和方法(就像API一样),只是在较低的层次上。
当涉及到使用外部库的应用程序时,ABI很重要。 如果一个程序被构build为使用特定的库,并且该库稍后更新,则不需要重新编译该应用程序(并且从最终用户的angular度来看,您可能没有该源)。 如果更新的库使用相同的ABI,那么您的程序将不需要更改。 即使内部工作可能已经改变,图书馆的界面(这是你的程序真正关心的)是一样的。 具有相同ABI的库的两个版本有时被称为“二进制兼容”,因为它们具有相同的低级接口(您应该能够用新版本replace旧版本,而不存在任何主要问题)。
有时候,ABI的变化是不可避免的。 发生这种情况时,使用该库的任何程序将不能工作,除非重新编译为使用该库的新版本。 如果ABI改变但是API不改变,那么旧的和新的库版本有时被称为“源兼容的”。 这意味着虽然为一个库版本编译的程序将不能与另一个编译器一起工作,但如果重新编译,为其中一个编写的源代码将为另一个编译。
出于这个原因,图书馆作家往往试图保持ABI稳定(以尽量减less干扰)。 保持ABI稳定意味着不改变函数接口(返回types和数量,types和参数的顺序),数据types或数据结构的定义,定义的常量等。可以添加新的函数和数据types,但是现有的函数和数据types必须停留一样。 如果将16位数据结构字段扩展为32位字段,则使用该数据结构的已编译代码将无法正确访问该字段(或其后的任何字段)。 在编译过程中访问数据结构成员会被转换成内存地址和偏移量,如果数据结构发生变化,那么这些偏移量就不会指向代码期望它们指向的内容,结果是不可预测的。
ABI不一定是你明确提供的东西,除非你希望人们使用汇编来与你的代码进行交互。 它也不是语言特定的,因为(例如)一个C应用程序和一个Pascal应用程序在编译之后将使用相同的ABI。
编辑:关于您在SysV ABI文档中关于ELF文件格式的章节的问题:包含此信息的原因是因为ELF格式定义了操作系统和应用程序之间的接口。 当你告诉OS运行一个程序时,它希望程序以某种方式被格式化,并且(例如)期望二进制的第一部分是在特定内存偏移处包含特定信息的ELF头部。 这是应用程序如何将有关自身的重要信息传递给操作系统。 如果使用非ELF二进制格式(如a.out或PE)构build程序,那么期望ELF格式应用程序的操作系统将无法解释二进制文件或运行该应用程序。 这是为什么Windows应用程序无法在Linux机器上直接运行(或反之亦然),而无需重新编译或在可从一种二进制格式转换为另一种types的仿真层内运行的一个重要原因。
IIRC,Windows当前使用便携式可执行文件 (或PE)格式。 维基百科页面的“外部链接”部分提供了更多关于PE格式的信息。
此外,关于您关于C ++名称修改的说明:ABI可以为C ++编译器定义一个“标准化”的方式来进行名称修改,以实现兼容性。 也就是说,如果我创build了一个库,并且开发了一个使用该库的程序,那么应该可以使用与我不同的编译器,而不必担心由于不同的名称修改scheme而导致产生的二进制文件不兼容。 如果你正在定义一个新的二进制文件格式或编写一个编译器或链接器,这实际上是唯一的使用。
如果您知道程序集以及操作系统级别的工作方式,那么您就符合某个ABI。 ABIpipe理参数如何传递,返回值放置在哪里。 对于许多平台,只有一个ABI可供select,在这种情况下,ABI只是“如何工作”。
然而,ABI也控制着类如何在C ++中进行布局。 如果您希望能够跨模块边界传递对象引用,或者如果要混合使用不同编译器编译的代码,则这是必需的。
此外,如果您有一个可以执行32位二进制文件的64位操作系统,则32位和64位代码将具有不同的ABI。
通常,链接到相同可执行文件的任何代码都必须符合相同的ABI。 如果您想使用不同的ABI在代码之间进行通信,则必须使用某种forms的RPC或序列化协议。
我认为你正在努力将不同types的接口挤进一组固定的特性。 例如,一个接口不一定要分成消费者和生产者。 一个接口只是两个实体交互的约定。
ABIs可以(部分)ISA不可知的。 某些方面(例如调用约定)依赖于ISA,而其他方面(例如C ++类布局)则不这样做。
一个明确的ABI对编写编译器的人来说非常重要。 没有明确定义的ABI,就不可能生成可互操作的代码。
编辑:一些说明澄清:
- ABI中的“二进制”不排除使用string或文本。 如果要链接导出C ++类的DLL,则其中的某个位置必须对方法和types签名进行编码。 这就是C ++的名字来了。
- 你从来没有提供ABI的原因是绝大多数程序员永远不会这样做。 ABI由devise平台的人员(即操作系统)提供,很less有程序员有权devise一个广泛使用的ABI。
应用程序二进制接口(ABI)类似于API,但函数不能在源代码级调用。 只有二进制表示可以访问/可用。
ABI可以在处理器架构级别或OS级别上定义。 ABI是编译器的代码生成器阶段所遵循的标准。 标准是由操作系统或处理器来解决的。
function:定义独立于实现语言或特定编译器/链接器/工具链的函数调用的机制/标准。 提供允许JNI或Python-C接口等的机制
现有实体:机器代码forms的函数。
消费者:另一个function(包括另一种语言的编译器,由另一个编译器编译,或由另一个链接器链接)。
function:影响编译器,汇编编写器,链接器和操作系统的一组合约。 契约指定了函数的布局,parameter passing的位置,参数的传递方式,函数返回的工作方式。 这些通常特定于(处理器架构,操作系统)元组。
现有实体:参数布局,函数语义,寄存器分配。 例如,ARM体系结构有许多ABI(APCS,EABI,GNU-EABI,不用介意一堆历史案例) – 使用混合的ABI会导致你的代码在跨越边界时不起作用。
消费者:编译器,汇编编写者,操作系统,CPU特定的体系结构。
谁需要这些细节? 编译器,汇编编写器,链接器,代码生成(或alignment要求),操作系统(中断处理,系统调用接口)。 如果你进行了汇编编程,那么你就符合ABI!
C ++名字的修改是一个特殊情况 – 它是一个链接器和dynamic链接器中心的问题 – 如果名称修改不规范,那么dynamic链接将不起作用。 从此以后,C ++ ABI就被称为C ++ ABI。 这不是链接器级别的问题,而是代码生成问题。 一旦你有了一个C ++二进制文件,就不可能使它与另一个C ++ ABI(名字encryption,exception处理)兼容,而不需要从源代码重新编译。
ELF是使用加载器和dynamic链接器的文件格式。 ELF是二进制代码和数据的容器格式,因此指定了一段代码的ABI。 我不认为ELF是严格意义上的ABI,因为PE可执行文件不是ABI。
所有ABI都是指令集特定的。 ARM ABI在MSP430或x86_64处理器上没有意义。
Windows有几个ABI – 例如,fastcall和stdcall是两个常用的ABI。 系统调用ABI又是不同的。
如果你真的不需要ABI,
- 你的程序没有function,
- 你的程序是单独运行的一个单独的可执行文件(即embedded式系统),它实际上是唯一运行的程序,不需要与其他任何东西进行通信。
一个简单的总结:
API: “这是您可能调用的所有function。”
ABI: “这是如何调用一个函数。”
ABI是编译器和链接器遵循的规则,以便编译你的程序,以便正常工作。 ABI涵盖多个主题:
- 可以说,ABI最大也是最重要的部分是程序调用标准,有时被称为“调用约定”。 调用约定标准化了“函数”如何转换为汇编代码。
- ABI还规定如何表示库中公开函数的名称 ,以便其他代码可以调用这些库,并知道应该传递哪些参数。 这被称为“名称捣毁”。
- ABI也规定可以使用哪种types的数据types,如何alignment以及其他低级细节。
深入研究一下我认为是ABI核心的呼叫公约:
机器本身没有“function”的概念。 当用c这样的高级语言编写函数时,编译器会生成一行汇编代码,如_MyFunction1:
这是一个标签 ,最终会被汇编器parsing成地址。 这个标签标记汇编代码中“函数”的“开始”。 在高级代码中,当你“调用”那个函数的时候,你真正在做的是让CPU 跳转到那个标签的地址,并继续在那里执行。
为了准备跳转,编译器必须做一堆重要的事情。 调用约定就像编译器遵循的所有这些东西的清单:
- 首先,编译器插入一些汇编代码来保存当前地址,这样当你的“function”完成时,CPU可以跳回到正确的位置继续执行。
- 接下来,编译器生成汇编代码来传递参数。
- 一些调用约定规定参数应该放在堆栈上( 按特定的顺序 )。
- 其他的约定规定,论据应该放在特定的寄存器( 取决于它们的数据types当然)。
- 还有一些其他的约定规定应该使用栈和寄存器的特定组合。
- 当然,如果之前在这些寄存器中有任何重要的东西,这些值现在会被覆盖并永远丢失,所以一些调用约定可能会指示编译器在将参数放入其中之前应该保存一些这样的寄存器。
- 现在,编译器插入一条跳转指令,告诉CPU去前面的标签(
_MyFunction1:
。 在这一点上,你可以认为CPU是“在”你的“function”。 - 在函数结束时,编译器会放入一些汇编代码,使CPU将返回值写入正确的位置。 调用约定将决定返回值是否应放入特定的寄存器(取决于其types)还是堆栈中。
- 现在是清理的时候了。 调用约定将决定编译器放置清理汇编代码的位置。
- 一些约定认为,调用者必须清理堆栈。 这意味着在“函数”完成之后,CPU跳回到之前的位置,下一个要执行的代码应该是一些非常具体的清理代码。
- 其他约定表示清理代码的某些特定部分应该在跳回之前的“function”的末尾。
有许多不同的ABI /调用约定。 一些主要的是:
- 对于x86或x86-64 CPU(32位环境):
- CDECL
- STDCALL
- FASTCALL
- VECTORCALL
- THISCALL
- 对于x86-64 CPU(64位环境):
- SYSTEMV
- MSNATIVE
- VECTORCALL
- 对于ARM CPU(32位)
- AAPCS
- 对于ARM CPU(64位)
- AAPCS64
这是一个很棒的页面,它显示了编译不同ABI时生成的程序集中的差异。
另外要提到的是,ABI不仅与程序的可执行模块相关。 链接器也使用它来确保您的程序正确调用库函数。 您的计算机上运行着多个共享库,只要您的编译器知道它们各自使用了哪个ABI,就可以正确调用它们的函数而不会造成堆栈。
您的编译器了解如何调用库函数是非常重要的。 在托pipe平台上(即操作系统加载程序的平台),如果不进行内核调用,程序甚至不能闪烁。
让我至less回答你的问题的一部分。 以Linux ABI如何影响系统调用为例,以及为什么这是有用的。
系统调用是用户空间程序向内核空间寻求某种东西的一种方式。 它的工作方式是将呼叫的数字代码和参数放在某个寄存器中,并触发一个中断。 内核空间发生切换,内核查找数字代码和参数,处理请求,将结果放回寄存器并触发切换回用户空间。 例如,当应用程序想要分配内存或打开一个文件(系统调用“brk”和“打开”)时,这是需要的。
现在系统调用具有短名称“brk”等和相应的操作码,这些在系统特定的头文件中定义。 只要这些操作码保持不变,您就可以运行与不同更新的内核相同的编译用户程序,而无需重新编译。 所以你有一个预编译二进制使用的接口,因此ABI。
区分ABI和API的最好方法是知道为什么和用于什么:
对于x86-64通常有一个ABI(对于x86 32位还有另一个集合):
http://www.x86-64.org/documentation/abi.pdf
Linux + FreeBSD + MacOSX跟随它一些轻微的变化。 而Windows x64有自己的ABI:
http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/
知道ABI并假设其他编译器也遵循它,那么理论上二进制文件就知道如何相互调用(特别是库API),并通过堆栈或寄存器等传递参数。或者调用函数时会改变哪些寄存器等从本质上讲,这些知识将帮助软件相互整合。 了解寄存器/堆栈布局的顺序,我可以很容易地将assembly在一起的不同软件组合在一起,而没有太多问题。
但API是不同的:
这是一个高层次的函数名称,定义了参数,这样,如果使用这些API构build不同的软件块,可以互相调用。 但是必须遵守SAME ABI的附加要求。
例如,Windows曾经是符合POSIX API的:
https://en.wikipedia.org/wiki/Windows_Services_for_UNIX
https://en.wikipedia.org/wiki/POSIX
而且Linux也兼容POSIX。 但是二进制文件不能被移动并立即运行。 但是因为他们在符合POSIX标准的API中使用了相同的NAMES,所以你可以在C中使用相同的软件,在不同的操作系统中重新编译它,然后立即运行它。
API旨在简化软件 – 预编译阶段的集成。 所以在编译之后,软件看起来完全不一样 – 如果ABI不同的话。
ABI是为了在二进制/汇编级定义软件的精确集成。
为了调用共享库中的代码,或者在编译单元之间调用代码,目标文件需要包含调用的标签。 C ++为了执行数据隐藏和允许重载的方法而改变了方法标签的名字。 这就是为什么你不能混合来自不同C ++编译器的文件,除非他们明确支持相同的ABI。
概要
定义ABI(应用程序二进制接口)的确切层有各种解释和强烈的意见。
在我看来,ABI是被认为是特定API的给定/平台的主观惯例 。 ABI是对于特定API“不会改变”或将由运行时环境(执行器,工具,链接器,编译器,jvm和OS)解决的约定的“其余”。
定义一个接口 :ABI,API
如果你想使用一个类似joda时间的库,你必须声明对joda-time-<major>.<minor>.<patch>.jar
的依赖。 图书馆遵循最佳实践并使用语义版本 。 这定义了三个级别的API兼容性:
- 补丁 – 你不需要改变你的代码。 图书馆只是修复了一些错误。
- 次要 – 你不需要改变你的代码,因为添加
- 主要 – 接口(API)已更改,您可能需要更改代码。
为了让你使用同一个图书馆的新主要版本,还有很多其他的约定仍然得到尊重:
- 用于库的二进制语言(在Java中,定义Java字节码的JVM目标版本)
- 调用约定
- JVM约定
- 链接约定
- 运行时约定所有这些都是由我们使用的工具定义和pipe理的。
例子
Java案例研究
例如,Java将所有这些约定标准化,而不是在工具中,而是在正式的JVM规范中。 该规范允许其他供应商提供一组不同的工具,可以输出兼容的库。
Java为ABI提供了另外两个有趣的案例:Scala版本和Dalvik虚拟机。
Dalvik虚拟机打破了ABI
Dalvik VM需要与Java字节码不同的字节码types。 Dalvik库是通过转换Dalvik的Java字节码(使用相同的API)获得的。 通过这种方式,您可以获得相同API的两个版本:由原始的joda-time-1.7.2.jar
。 我们可以称之为joda-time-1.7.2.jar
和joda-time-1.7.2-dalvik.jar
。 他们使用不同的ABI,一个是面向堆栈的标准Java虚拟机:Oracle的,IBM的,Java或其他; 而第二个ABI是Dalvik周围的。
斯卡拉连续发行是不兼容的
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库。 由于不同的解决scheme,这种不兼容性在雷达下方飞行:
- 语义版本化:当库定位到更高的JVM时,通常会更改主版本。
- 使用JVM 4作为ABI,你很安全。
- Java 9添加了一个规范,说明如何在同一个库中包含特定目标JVM的字节码。
为什么我从API定义开始?
API和ABI只是关于如何定义兼容性的约定。 较低层在大量高级语义方面是通用的。 这就是为什么做一些约定很容易。 The first kind of conventions are about memory alignment, byte encoding, calling conventions, big and little endian encodings, etc. On top of them you get the executable conventions like others described, linking conventions, intermediate byte code like the one used by Java or LLVM IR used by GCC. Third you get conventions on how to find libraries, how to load them (see Java classloaders). As you go higher and higher in concepts you have new conventions that you consider as a given. That's why they didn't made it to the semantic versioning . They are implicit or collapsed in the major version. We could amend semantic versioning with <major>-<minor>-<patch>-<platform/ABI>
. This is what is actually happening already: platform is already a rpm
, dll
, jar
(JVM bytecode), war
(jvm+web server), apk
, 2.11
(specific Scala version) and so on. When you say APK you already talk about a specific ABI part of your API.
API can be ported to different ABI
The top level of an abstraction (the sources written against the highest API can be recompiled/ported to any other lower level abstraction.
Let's say I have some sources for rxscala. If the Scala tools are changed I can recompile them to that. If the JVM changes I could have automatic conversions from the old machine to the new one without bothering with the high level concepts. While porting might be difficult will help any other client. If a new operating system is created using a totally different assembler code a translator can be created.
APIs ported across languages
There are APIs that are ported in multiple languages like reactive streams . In general they define mappings to specific languages/platforms. I would argue that the API is the master specification formally defined in human language or even a specific programming language. All the other "mappings" are ABI in a sense, else more API than the usual ABI. The same is happening with the REST interfaces.
The ABI needs to be consistent between caller and callee to be certain that the call succeeds. Stack use, register use, end-of-routine stack pop. All these are the most important parts of the ABI.
Application binary interface (ABI)
Functionality:
- Translation from the programmer's model to the underlying system's domain data type, size, alignment, the calling convention, which controls how functions' arguments are passed and return values retrieved; the system call numbers and how an application should make system calls to the operating system; the high-level language compilers' name mangling scheme, exception propagation, and calling convention between compilers on the same platform, but do not require cross-platform compatibility…
Existing entities:
- Logical blocks that directly participate in program's execution: ALU, general purpose registers, registers for memory/ I/O mapping of I/O, etc…
consumer:
- Language processors linker, assembler…
These are needed by whoever has to ensure that build tool-chains work as a whole. If you write one module in assembly language, another in Python, and instead of your own boot-loader want to use an operating system, then your "application" modules are working across "binary" boundaries and require agreement of such "interface".
C++ name mangling because object files from different high-level languages might be required to be linked in your application. Consider using GCC standard library making system calls to Windows built with Visual C++.
ELF is one possible expectation of the linker from an object file for interpretation, though JVM might have some other idea.
For a Windows RT Store app, try searching for ARM ABI if you really wish to make some build tool-chain work together.
In short and in philosophy, only things of a kind can get along well, and the ABI could be seen as the kind of which software stuff work together.
I was also trying to understand ABI and JesperE's answer was very helpful.
From a very simple perspective, we may try to understand ABI by considering binary compatibility.
KDE wiki defines a library as binary compatible “if a program linked dynamically to a former version of the library continues running with newer versions of the library without the need to recompile.” For more on dynamic linking, refer Static linking vs dynamic linking
Now, let's try to look at just the most basic aspects needed for a library to be binary compatibility (assuming there are no source code changes to the library):
- Same/backward compatible instruction set architecture (processor instructions, register file structure, stack organization, memory access types, along with sizes, layout, and alignment of basic data types the processor can directly access)
- Same calling conventions
- Same name mangling convention (this might be needed if say a Fortran program needs to call some C++ library function).
Sure, there are many other details but this is mostly what the ABI also covers.
More specifically to answer your question, from the above, we can deduce:
ABI functionality: binary compatibility
existing entities: existing program/libraries/OS
consumer: libraries, OS
希望这可以帮助!