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

cla*_*aws 444 compiler-construction api binary operating-system 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 Binary Interface Edition 4.1(1997-03-18),看看它究竟包含了什么.好吧,大部分都没有任何意义.

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

  • 我知道,因为我们谈论的水平很低,所以必须非常具体.但我不确定它是如何"指令集架构(ISA)"具体的?

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

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

bta*_*bta 471

理解"ABI"的一种简单方法是将其与"API"进行比较.

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

ABI非常相似.可以将其视为API的编译版本(或作为机器语言级别的API).编写源代码时,可以通过API访问库.编译代码后,您的应用程序将通过ABI访问库中的二进制数据.ABI定义了编译的应用程序将用于访问外部库的结构和方法(就像API一样),仅在较低级别上.

对于使用外部库的应用程序,ABI很重要.如果构建程序以使用特定库并且稍后更新该库,则您不希望必须重新编译该应用程序(并且从最终用户的角度来看,您可能没有源代码).如果更新的库使用相同的ABI,那么您的程序将不需要更改.即使内部工作可能已经改变,库的接口(这是您的所有程序真正关心的)也是相同的.具有相同ABI的库的两个版本有时被称为"二进制兼容",因为它们具有相同的低级接口(您应该能够用新版本替换旧版本并且没有任何重大问题).

有时,ABI的变化是不可避免的.发生这种情况时,任何使用该库的程序都将无法运行,除非重新编译它们以使用新版本的库.如果ABI发生变化但API没有变化,则新旧库版本有时称为"源兼容".这意味着虽然为一个库版本编译的程序不能与另一个库版本一起工作,但为一个编写的源代码在重新编译时将适用于另一个.

出于这个原因,图书馆编写者倾向于试图保持他们的ABI稳定(以尽量减少中断).保持ABI稳定意味着不改变函数接口(返回类型和数量,类型和参数的顺序),数据类型或数据结构的定义,定义的常量等.可以添加新函数和数据类型,但现有函数和数据类型必须保持不变相同.如果将16位数据结构字段扩展为32位字段,则使用该数据结构的已编译代码将无法正确访问该字段(或任何后续字段).访问数据结构成员在编译期间转换为内存地址和偏移量,如果数据结构发生变化,那么这些偏移量将不会指向代码期望它们指向的内容,并且结果最多也是不可预测的.

除非您希望人们使用汇编与您的代码进行交互,否则ABI不一定是您明确提供的内容.它也不是特定于语言的,因为(例如)C应用程序和Pascal应用程序在编译后将使用相同的ABI.

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

IIRC,Windows目前使用可移植可执行(或PE)格式.该维基百科页面的"外部链接"部分中有链接,其中包含有关PE格式的更多信息.

另外,关于你关于C++名称修改的注释:ABI可以为C++编译器定义一种"标准化"方式,以便为了兼容性而进行名称修改.也就是说,如果我创建一个库并且您开发了一个使用该库的程序,那么您应该能够使用与我不同的编译器,而不必担心由于不同的名称修改方案而导致的二进制文件不兼容.如果您要定义新的二进制文件格式或编写编译器或链接器,那么这只是有用的.

  • 很好的答案.除了这不是ABI是什么.ABI是一组确定调用约定的规则,以及用于布局结构的规则.Pascal以与C应用程序相反的顺序在堆栈上传递参数,因此pascal和C编译器不会编译到同一个ABI.C和Pascal编译器的相应标准隐含地确保了这种情况.C++编译器无法定义一种"标准"方式来破坏名称,因为没有标准方法.当Windows上存在竞争的C++编译器时,C++名称修改约定在C++编译器之间不兼容. (29认同)
  • 对于其他用户:还阅读JesperE的答案. (9认同)
  • @bta,感谢您的出色回答。调用约定是一种 ABI 吗?谢谢 (5认同)
  • 当然也可以看到 https://autotools.io/libtool/version.html 和 http://fedoramagazine.org/gcc-5-in-fedora-whats-an-abi-and-what-happens-when-we-change -它/ (2认同)

Jes*_*erE 135

如果您知道程序集以及操作系统级别的工作方式,那么您就符合某个ABI.ABI管理诸如如何传递参数,放置返回值的位置.对于许多平台,只有一个ABI可供选择,在这些情况下,ABI只是"如何工作".

但是,ABI也管理如何在C++中布置类/对象.如果您希望能够跨模块边界传递对象引用,或者如果要混合使用不同编译器编译的代码,则这是必需的.

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

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

我认为你很难将不同类型的接口挤入一组固定的特性中.例如,界面不一定必须拆分为消费者和生产者.接口只是两个实体交互的约定.

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

定义良好的ABI对于编写编译器的人来说非常重要.如果没有明确定义的ABI,就不可能生成可互操作的代码.

编辑:一些说明澄清:

  • ABI中的"二进制"不排除使用字符串或文本.如果要链接导出C++类的DLL,则必须对其中的方法和类型签名进行编码.这就是C++名称变形的地方.
  • 你从未提供过ABI的原因是绝大多数程序员都不会这样做.ABI由设计平台(即操作系统)的人提供,很少有程序员有权设计广泛使用的ABI.

  • 这是*正确和*精确*的答案,没有冗长(而不是*噪音*)! (4认同)
  • @jesperE,“ABI 管理诸如参数传递方式、返回值放置位置之类的事情。”是指“cdecl、stdcall、fastcall、pascal”,对吗? (2认同)
  • 是.正确的名称是"调用约定",它是ABI的一部分.http://en.wikipedia.org/wiki/X86_calling_conventions (2认同)

Lak*_*key 30

如果 - 你实际上根本不需要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
    • THISCALL
  • 对于x86-64 CPU(64位环境):
    • SYSTEMV
    • MSNATIVE
    • VECTORCALL
  • 对于ARM CPU(32位)
    • AAPCS
  • 对于ARM CPU(64位)
    • AAPCS64

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

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

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

  • 最佳答案恕我直言。 (3认同)

alv*_*vin 17

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

可以在处理器架构级别或OS级别定义ABI.ABI是编译器的代码生成器阶段遵循的标准.该标准由OS或处理器修复.

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

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

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


Cir*_*四事件 16

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

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

例如:

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

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

    这在 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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

mylib.h

#ifndef MYLIB_H
#define MYLIB_H

typedef struct {
    int old_field;
} mylib_mystruct;

mylib_mystruct* mylib_init(int old_field);

#endif
Run Code Online (Sandbox Code Playgroud)

编译并运行良好:

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
Run Code Online (Sandbox Code Playgroud)

现在,假设对于库的 v2,我们要添加一个新字段来mylib_mystruct调用new_field

如果我们之前添加了该字段,old_field如下所示:

typedef struct {
    int new_field;
    int old_field;
} mylib_mystruct;
Run Code Online (Sandbox Code Playgroud)

并重建库但没有重建main.out,然后断言失败!

这是因为该行:

myobject->old_field == 1
Run Code Online (Sandbox Code Playgroud)

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

因此,此更改破坏了 ABI。

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

typedef struct {
    int old_field;
    int new_field;
} mylib_mystruct;
Run Code Online (Sandbox Code Playgroud)

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

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

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

API 与 ABI

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

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

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

mylib_mystruct* mylib_init(int old_field, int new_field);
Run Code Online (Sandbox Code Playgroud)

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

语义 API 与编程 API

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

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

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

例如,如果我们修改了

myobject->old_field = old_field;
Run Code Online (Sandbox Code Playgroud)

到:

myobject->old_field = old_field + 1;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Java 最小可运行示例

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

在 Ubuntu 18.10、GCC 8.2.0 中测试。


Yan*_*min 10

功能:影响编译器,汇编编写器,链接器和操作系统的一组契约.合同规定了如何布置函数,传递参数的位置,参数的传递方式,函数返回的工作方式.这些通常特定于(处理器体系结构,操作系统)元组.

现有实体:参数布局,函数语义,寄存器分配.例如,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 - 例如,fastcall和stdcall是两种常用的ABI.系统调用ABI再次不同.


sni*_*ies 7

让我至少回答你问题的一部分.以Linux ABI如何影响系统调用为例,以及为什么它有用.

systemcall是一种用户空间程序向内核空间询问内容的方法.它的工作原理是将调用的数字代码和参数放在某个寄存器中并触发中断.比内核空间发生切换,内核查找数字代码和参数,处理请求,将结果放回寄存器并触发切换回用户空间.例如,当应用程序想要分配内存或打开文件(syscalls"brk"和"open")时,这是必需的.

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


Jus*_*ith 7

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


Pet*_*eoh 7

区分 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://en.wikipedia.org/wiki/Windows_Services_for_UNIX

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

Linux 也兼容 POSIX。但二进制文件不能直接移动并立即运行。但因为它们在 POSIX 兼容 API 中使用了相同的名称,所以您可以使用 C 语言编写相同的软件,在不同的操作系统中重新编译它,并立即使其运行。

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

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


rai*_*tin 6

概括

对于定义 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 VM 需要与 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 的。

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 或GCC 使用的 LLVM IR。第三,您将获得有关如何查找库、如何加载它们的约定(请参阅 Java 类加载器)。当你在概念上越来越高时,你会有新的约定,你认为这些约定是给定的。这就是为什么他们没有进入语义版本控制版本。我们可以用<major>-<minor>-<patch>-<platform/ABI>. 这就是实际已经发生的事情:平台已经是一个rpm, dll, jar(JVM 字节码), war(jvm+web server), apk, 2.11(特定的 Scala 版本) 等等。当您说 APK 时,您已经在谈论 API 的特定 ABI 部分。

API可以移植到不同的ABI

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

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

跨语言移植的 API

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


plu*_*ash 5

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

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

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

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

void initfoo(FOO * foo)
int usefoo(FOO * foo, int bar)
void cleanupfoo(FOO * foo)
Run Code Online (Sandbox Code Playgroud)

应用程序程序员编写如下代码

int dostuffwithfoo(int bar) {
  FOO foo;
  initfoo(&foo);
  int result = usefoo(&foo,bar)
  cleanupfoo(&foo);
  return result;
}
Run Code Online (Sandbox Code Playgroud)

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

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

FOO * newfoo(void)
int usefoo(FOO * foo, int bar)
void deletefoo((FOO * foo, int bar))
Run Code Online (Sandbox Code Playgroud)

应用程序程序员编写如下代码

int dostuffwithfoo(int bar) {
  FOO * foo;
  foo = newfoo();
  int result = usefoo(foo,bar)
  deletefoo(foo);
  return result;
}
Run Code Online (Sandbox Code Playgroud)

那么应用程序二进制文件不需要知道任何有关 FOO 结构的信息,它们都可以隐藏在库中。但为此付出的代价是涉及堆操作。


归档时间:

查看次数:

95766 次

最近记录:

5 年,10 月 前