API和ABI之间的区别


192

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

API的定义:

API定义了接口,软件可以通过这些接口与源级别的另一软件进行通信。

ABI的定义:

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

程序如何在源代码级别进行通信?什么是源代码级别?无论如何,它与源代码有关吗?还是库的源包含在主程序中?

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


2
在源代码级别上,它们表示类似包含文件以公开函数定义的内容
Anycorn 2010年

Answers:


49

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

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

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


308

API:应用程序接口

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

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

ABI:应用程序二进制接口

这就是编译器构建应用程序的方式。
它定义了事物(但不限于):

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

17
这可能是我见过的关于ABI是什么的最好的简明解释。j!
TerryP

3
你们需要决定这个答案是简洁还是详尽。:)
jrok

1
@jrok:事情可以简洁明了,它们并不相互排斥。
马丁·约克

@LokiAstari,所以ABI实际上不是API吗?
Pacerier,2015年

4
@Pacerier:它们都是接口。但是它们处于不同的抽象级别。API在应用程序开发人员级别。ABI处于编译器级别(某个应用程序开发人员从未去过的地方)。
马丁·约克

47

我主要是在与API不兼容的更改或与ABI不兼容的更改的意义上遇到这些术语的。

本质上,对API所做的更改是本来可以用以前的版本编译的代码不再起作用的地方。之所以会发生这种情况,是因为您在函数中添加了参数,或者更改了在本地代码之外可访问的名称。每当您更改标头,并迫使您更改.c / .cpp文件中的某些内容时,您都进行了API更改。

ABI更改是已经针对版本1编译的代码将不再与代码库的版本2(通常是库)一起使用的地方。与API不兼容的更改相比,这通常要棘手得多,因为像向类添加虚拟方法这样的简单操作可能与ABI不兼容。

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


4
+1指出他们的互斥。例如,Java对assert关键字的介绍是与API不兼容但与ABI兼容的更改docs.oracle.com/javase/7/docs/technotes/guides/language/…
Pacerier 2014年

您可以在资源文件中添加tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html的 “ 3.6。不兼容的库”部分,其中列出了可能导致ABI更改的原因。
Demi-Lune

20

这是我的外行解释:

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

13

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

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

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

正如我们将在下面的示例中看到的那样,即使API不变,也可以修改ABI,破坏程序。

main.c

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

#include "mylib.h"

int main(void) {
    mylib_mystrict *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_mystrict调用添加一个新字段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与ABI

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

例如,如果我们修改了

myobject->old_field = old_field;

至:

myobject->old_field = old_field + 1;

那么这将不会破坏API或ABI,但main.c仍然会破坏!

这是因为我们更改了该功能应该执行的“人工描述”,而不是程序上引人注目的方面。

我只是有一种哲学上的见解,即某种意义上的软件形式验证将更多的“语义API”转移到了一个“可程序验证的API”中。

语义API与编程API

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

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

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

例如,如果我们修改了

myobject->old_field = old_field;

至:

myobject->old_field = old_field + 1;

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

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

  • 测试一堆极端情况。容易做到,但您可能总是会错过一个。
  • 正式验证。难度较大,但会产生数学上正确性的证明,实质上是将文档和测试统一为“人为” /机器可验证的方式!只要您对课程的正式描述中没有错误;-)

已在Ubuntu 18.10,GCC 8.2.0中测试。


2
您的答案足够详尽,可以帮助我理解API和ABI之间的区别。谢谢!
Rakshith Ravi

9

pplication inary 覆盖整个院落)一种规范的特定硬件平台与操作系统相结合。它是一个步骤超出API( pplication P rogram 覆盖整个院落),其定义了从应用程序到操作系统的调用。ABI为特定CPU系列定义API以及机器语言。API不能确保运行时兼容性,但是ABI可以,因为它定义了机器语言或运行时格式。

在此处输入图片说明

礼貌


9

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

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

这是说明的示例。我有A类的Java库

// 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,则此问题已解决,并且所有代码都可以重新工作。


6

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

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

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

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

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

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

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

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


1
这是对它们之间差异的非常简洁的解释,并且具有非常独特的视角。
Sajuuk

1

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

ABI[关于] -Application Binary Interface运行时接口,程序在执行期间用于机器代码中组件之间的通信

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.