API和ABI的区别

我是Linux系统编程的新手,在阅读时遇到了API和ABI Linux系统编程

API的定义:

API定义了接口 一个软件进行通信

ABI的定义:

而API定义了一个源 接口时,ABI定义了 两个之间的低级二进制接口 或者更多的软件 特定的体系结构。它定义了 应用程序如何与 本身,应用程序如何交互 与内核,以及如何一个 应用程序与库交互

程序如何在源级进行通信?什么是源级别?它是否与源代码有关?或者库的源代码包含在主程序中?

我所知道的唯一区别是API主要由程序员使用,而ABI主要由编译器使用。

94344 次浏览

这是我的外行解释:

  • API——想想include文件。它们提供编程接口。
  • ABI——想想内核模块。当你在某个内核上运行它时,它必须同意如何在没有包含文件的情况下进行通信,即作为低级二进制接口。

API是人类使用的。我们编写源代码。当我们编写程序并想要使用一些库函数时,我们编写如下代码:

long howManyDecibels = 123L;
int ok = livenMyHills(howManyDecibels);

我们需要知道有一个方法livenMyHills(),它接受一个长整型参数。所以作为一个编程接口,它都是用源代码表示的。编译器将其转换为可执行指令,这些指令符合该语言在特定操作系统上的实现。在这种情况下,会导致音频单元上的一些低级操作。所以特定的比特和字节被喷到一些硬件上。所以在运行时有很多二进制级别的动作,我们通常看不到。

在二进制级别,必须对在二进制级别传递的字节有一个精确的定义,例如4字节整数中的字节顺序,或者复杂数据结构的布局——是否有填充字节来对齐一些值。这个定义就是ABI。

API:应用程序接口

这是您从应用程序/库中公开的公共类型/变量/函数集。

在C/ c++中,这是在应用程序附带的头文件中公开的内容。

ABI:应用程序二进制接口

这是编译器构建应用程序的方式 它定义的东西(但不限于):

  • 参数如何传递给函数(寄存器/堆栈)。
  • 谁从堆栈中清除参数(调用方/被调用方)。
  • 返回值的位置。
  • 异常如何传播。

我经常在api不兼容的更改或abi不兼容的更改的意义上遇到这些术语。

API更改本质上是使用以前版本编译的代码将不再工作。这可能是因为您向函数添加了参数,或者更改了在本地代码之外可访问的内容的名称。任何时候,当您更改一个头文件时,它就会迫使您更改.c/.cpp文件中的某些内容,您就做了一个api更改。

ABI更改是指已经根据版本1编译的代码将不再使用版本2的代码库(通常是库)。这通常比与api不兼容的更改更难跟踪,因为像向类添加虚拟方法这样简单的事情可能与ABI不兼容。

我发现了两种非常有用的资源,用于了解什么是ABI兼容性以及如何保存它:

(__abc0application Binary nterface)结合操作系统的特定硬件平台的规范。它是API (__abc0application __abc4program nterface)的一步,API定义了从应用程序到操作系统的调用。ABI为特定的CPU系列定义API和机器语言。API不能确保运行时兼容性,但ABI可以,因为它定义了机器语言或运行时格式。

enter image description here

礼貌

让我举一个具体的例子,说明在Java中ABI和API是如何不同的。

ABI不兼容的更改是如果我将方法a# m()从String作为参数更改为String...参数。这是不兼容ABI,因为你必须重新编译调用它的代码,但它是API兼容,因为你可以通过重新编译来解决它,而不需要在调用者中修改任何代码。

这里有一个例子。我的Java库有类A

    // Version 1.0.0
public class A {
public void m(String string) {
System.out.println(string);
}
}

我有一个类使用这个库

    public class Main {
public static void main(String[] args) {
(new A()).m("string");
}
}

现在,库作者编译了他们的类A,我编译了我的类Main,一切都运行得很好。想象一个新版本的a出现了

    // Version 2.0.0
public class A {
public void m(String... string) {
System.out.println(string[0]);
}
}

如果我只是使用新编译的类A,并将其与之前编译的类Main一起删除,那么在试图调用该方法时就会得到异常

Exception in thread "main" java.lang.NoSuchMethodError: A.m(Ljava/lang/String;)V
at Main.main(Main.java:5)

如果我重新编译Main,这是固定的,所有工作再次。

你的程序(源代码)可以用提供适当API的模块来编译。

你的程序(二进制)可以在提供适当ABI的平台上运行。

API限制了类型定义、函数定义、宏,有时还有库应该公开的全局变量。

ABI限制了一个“平台”应该为您的程序运行提供什么。我喜欢从三个层面来考虑:

  • 处理器级——指令集,调用约定

  • 内核级——系统调用约定,特殊的文件路径约定(例如Linux中的/proc/sys文件),等等。

  • 操作系统级别——对象格式、运行时库等。

考虑一个名为arm-linux-gnueabi-gcc的交叉编译器。“arm”表示处理器架构,“linux”表示内核,“gnu”表示其目标程序使用gnu的libc作为运行时库,不同于arm-linux-androideabi-gcc使用Android的libc实现。

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

这个答案是从我的另一个答案中提取出来的:什么是应用程序二进制接口(ABI)?,但我觉得它也直接回答了这个问题,而且这些问题不是重复的。

在共享库的背景下,“拥有稳定的abi”最重要的含义是:你不需要在库改变后重新编译你的程序。

正如我们将在下面的示例中看到的,即使API没有改变,也可以修改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;
}

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,但是main.c语义API会破坏。

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

  • 测试一些极端情况。这很简单,但你可能总是错过一个。
  • 形式验证。比较难做到,但是产生了正确性的数学证明,本质上是将文档和测试统一为一个“人”。/机器可验证方式!当然,前提是你的正式描述中没有bug;-)

在Ubuntu 18.10, GCC 8.2.0中测试。

API - Application Programming Interface是一个编译时间接口,可以被开发人员用来使用源代码中的库、操作系统、核心调用等非项目功能

ABI<一口>[对]< /一口> - Application Binary Interface是一个运行时接口,程序在执行过程中使用它在机器代码中的组件之间进行通信

ABI指的是目标文件/库和最终二进制文件的布局,从成功链接、加载和执行某些二进制文件的角度出发,而不会因为二进制文件不兼容而出现链接错误或逻辑错误。

  • 二进制格式规范(PE, COFF, ELF, .obj, .o, .a, .lib(导入库,静态库),.NET程序集,.pyc, COM .dll):标头,标头格式,定义节的位置以及导入/导出/异常表的位置以及它们的格式
  • 用于编码代码段中的字节的指令集,以及特定的机器指令
  • API中定义的函数和数据的实际签名(以及它们如何在二进制文件中表示(接下来的两点))
  • 代码部分中函数的调用约定,可以由其他二进制文件调用(与实际导出的函数的ABI兼容性特别相关)
  • 数据在数据部分中根据其类型表示和对齐的方式(与实际导出的数据的ABI兼容性特别相关)
  • 系统调用号或中断向量钩在代码中
  • 导出函数和数据的名称修饰
  • 目标文件中的链接器指令
  • API程序员使用的预处理器/编译器/汇编器/链接器标志和指令,以及如何解释它们以省略、优化、内联或更改库或最终二进制文件中某些符号或代码的链接(无论是二进制文件.dll还是静态链接中的可执行文件)

. net c#的字节码格式是一个ABI(通用),其中包括. net程序集的.dll格式。解释字节码的虚拟机有一个基于c++的特定ABI,当从本机代码调用字节码和从字节码调用本机代码时,需要在本机代码的特定ABI使用的本机c++类型和虚拟机ABI的盒装类型之间编组类型。在这里,我将特定程序的ABI称为特定ABI,而一般的ABI,如'MS ABI'或'C ABI'只是指调用约定和结构的组织方式,而不是通过引入新的ABI兼容性问题的特定二进制文件对ABI的具体体现。

一组API是指的类型定义特定库进口和出口的用于一个特定的翻译单元,从编译器的角度翻译单元,成功地解析和检查类型引用能够编译二进制文件,二进制将坚持目标ABI的标准,这样,如果库实际上实现API还编译一个兼容的ABI时,它将链接和工作。如果API更新了,应用程序仍然可以编译,但现在会有一个二进制不兼容,因此需要使用一个新的二进制。

API包括:

  • 函数,变量,类,对象,常量,它们的名称,类型和定义,以正确的语法和语义方式编码
  • 这些函数实际做什么,以及如何在源语言中使用它们
  • 需要包含的源代码文件/为了使用它们而需要链接到的二进制文件,以及它们的ABI兼容性

我先回答你们的具体问题。

1.什么是源级别?它是否与源代码有关?

是的,源代码级别这个术语指的是源代码的级别。术语级别指的是计算需求的语义级别,因为它们从应用程序域级别转换到源代码级别,又从源代码级别转换到机器代码级别(二进制代码)。应用程序域级别指的是软件的最终用户想要什么,并指定为他们的计算需求。源代码级是指程序员对应用程序级需求的理解,然后用某种语言指定为程序。

  1. 程序如何在源级进行通信?或者库的源代码包含在主程序中?

语言API专门指一种语言需要(指定)(即接口)用该语言编写可重用的模块。可重用程序符合这些接口(API)要求,以便在相同语言的其他程序中重用。每次重用都需要符合相同的API需求。所以,“交流”这个词;指重用。

是的,源代码(可重用模块的源代码;在C/ c++的情况下,.h文件)被包含(在预处理阶段复制)是重用 C/ c++的常用方式,因此是c++ API的一部分。即使你只是在c++程序的全局空间中编写了一个简单的函数foo(),然后调用该函数foo();,也可以根据c++语言API重用多次。Java包中的Java类是Java中可重用的模块。Java bean规范也是一个Java API,在运行时/容器(符合该规范)的帮助下,允许其他模块(可能是另一个bean)重用可重用程序(bean)。

关于语言API和ABI之间的区别,以及面向服务的API如何与语言API进行比较,我在SO上的回答应该会有帮助。