wake-up-neo.com

Was ist eine Application Binary Interface (ABI)?

Ich habe nie klar verstanden, was ein ABI ist. Bitte verweisen Sie mich nicht auf einen Wikipedia-Artikel. Wenn ich es verstehen könnte, wäre ich nicht hier, um einen so langen Beitrag zu posten.

Das ist meine Einstellung zu verschiedenen Schnittstellen:

Eine TV-Fernbedienung ist eine Schnittstelle zwischen dem Benutzer und dem Fernseher. Es ist eine vorhandene Entität, aber für sich nutzlos (bietet keine Funktionalität). Die gesamte Funktionalität für jede dieser Tasten auf der Fernbedienung ist im Fernsehgerät implementiert.

Schnittstelle: Es handelt sich um eine "existierende Entität" zwischen den functionality und consumer dieser Funktionalität. Eine Schnittstelle alleine macht nichts. Es ruft nur die dahinter liegende Funktionalität auf.

Jetzt gibt es je nachdem, wer der Benutzer ist, verschiedene Arten von Schnittstellen.

CLI-Befehle (Command Line Interface, Befehlszeilenschnittstelle) sind die vorhandenen Entitäten, der Konsument ist der Benutzer und die Funktionalität steckt dahinter.

functionality: meine Softwarefunktionalität, die einen Zweck erfüllt, für den wir diese Schnittstelle beschreiben.

existing entities: Befehle

consumer: Benutzer

Grafische Benutzeroberfläche (GUI) Fenster, Schaltflächen usw. sind die vorhandenen Entitäten, und wieder ist der Verbraucher der Benutzer, und die Funktionalität steckt dahinter.

functionality: meine Softwarefunktionalität, die ein Problem löst, für das wir diese Schnittstelle beschreiben.

existing entities: Fenster, Knöpfe etc ..

consumer: Benutzer

Application Programming Interface (API) Funktionen (oder um genauer zu sein) Schnittstellen (bei schnittstellenbasierter Programmierung) sind die vorhandenen Entitäten, Verbraucher hier ist ein anderes Programm nicht Ein Benutzer und wieder Funktionalität liegt hinter dieser Schicht.

functionality: meine Softwarefunktionalität, die ein Problem löst, für das wir diese Schnittstelle beschreiben.

existing entities: Funktionen, Schnittstellen (Array von Funktionen).

consumer: ein anderes Programm/eine andere Anwendung.

Application Binary Interface (ABI) Hier setzt mein Problem an.

functionality: ???

existing entities: ???

consumer: ???

  • Ich habe Software in verschiedenen Sprachen geschrieben und verschiedene Arten von Schnittstellen (CLI, GUI und API) bereitgestellt, bin mir jedoch nicht sicher, ob ich jemals eine ABI bereitgestellt habe.

Wikipedia sagt:

ABIs umfassen Details wie

  • datentyp, Größe und Ausrichtung;
  • die Aufrufkonvention, die steuert, wie Argumente von Funktionen übergeben und Rückgabewerte abgerufen werden;
  • die Systemrufnummern und wie eine Anwendung Systemaufrufe an das Betriebssystem tätigen soll;

Andere ABIs standardisieren Details wie

  • die C++ Name Mangling,
  • ausnahmeausbreitung und
  • aufrufkonvention zwischen Compilern auf derselben Plattform, jedoch keine plattformübergreifende Kompatibilität erforderlich.
  • Wer braucht diese Angaben? Bitte sagen Sie nicht das Betriebssystem. Ich kenne mich mit Assembly-Programmierung aus. Ich weiß, wie das Verlinken und Laden funktioniert. Ich weiß genau, was drinnen passiert.

  • Warum kam C++ Name Mangling? Ich dachte, wir reden auf der binären Ebene. Warum kommen Sprachen herein?

Wie auch immer, ich habe das [PDF] System V Application Binary Interface Edition 4.1 (1997-03-18) heruntergeladen, um zu sehen, was genau es enthält. Nun, das meiste ergab keinen Sinn.

  • Warum enthält es zwei Kapitel (4. und 5.) zur Beschreibung des Dateiformats ELF ? Tatsächlich sind dies die einzigen zwei bedeutenden Kapitel dieser Spezifikation. Die restlichen Kapitel sind "prozessorspezifisch". Jedenfalls denke ich, dass es ein ganz anderes Thema ist. Bitte sagen Sie nicht, dass die ELF-Dateiformatspezifikationen sind der ABI. Es qualifiziert sich nicht als Schnittstelle gemäß der Definition.

  • Ich weiß, da wir auf einem so niedrigen Niveau sprechen, muss es sehr spezifisch sein. Aber ich bin nicht sicher, wie es "Befehlssatzarchitektur (ISA)" spezifisch ist?

  • Wo finde ich das ABI von Microsoft Windows?

Das sind also die wichtigsten Fragen, die mich nerven.

418
claws

Eine einfache Möglichkeit, "ABI" zu verstehen, besteht darin, es mit "API" zu vergleichen.

Sie sind bereits mit dem Konzept einer API vertraut. Wenn Sie beispielsweise die Funktionen einer Bibliothek oder Ihres Betriebssystems nutzen möchten, verwenden Sie eine API. Die API besteht aus Datentypen/Strukturen, Konstanten, Funktionen usw., die Sie in Ihrem Code verwenden können, um auf die Funktionalität dieser externen Komponente zuzugreifen.

Ein ABI ist sehr ähnlich. Stellen Sie es sich als kompilierte Version einer API vor (oder als API auf Maschinensprachenebene). Wenn Sie Quellcode schreiben, greifen Sie über eine API auf die Bibliothek zu. Sobald der Code kompiliert ist, greift Ihre Anwendung über die ABI auf die Binärdaten in der Bibliothek zu. Die ABI definiert die Strukturen und Methoden, die Ihre kompilierte Anwendung verwendet, um auf die externe Bibliothek zuzugreifen (genau wie die API), jedoch nur auf einer niedrigeren Ebene.

ABIs sind wichtig, wenn es um Anwendungen geht, die externe Bibliotheken verwenden. Wenn ein Programm für die Verwendung einer bestimmten Bibliothek erstellt und diese Bibliothek später aktualisiert wird, müssen Sie diese Anwendung nicht neu kompilieren (und aus Sicht des Endbenutzers verfügen Sie möglicherweise nicht über die Quelle). Wenn die aktualisierte Bibliothek dieselbe ABI verwendet, muss Ihr Programm nicht geändert werden. Die Schnittstelle zur Bibliothek (für die sich alle Ihre Programme wirklich interessieren) ist dieselbe, auch wenn sich die internen Abläufe möglicherweise geändert haben. Zwei Versionen einer Bibliothek mit demselben ABI werden manchmal als "binärkompatibel" bezeichnet, da sie dieselbe Benutzeroberfläche auf niedriger Ebene haben (Sie sollten in der Lage sein, die alte Version durch die neue zu ersetzen und keine größeren Probleme zu haben).

Manchmal sind ABI-Änderungen unvermeidlich. In diesem Fall funktionieren alle Programme, die diese Bibliothek verwenden, nur dann, wenn sie neu kompiliert werden, um die neue Version der Bibliothek zu verwenden. Wenn sich die ABI ändert, die API jedoch nicht, werden die alte und die neue Bibliotheksversion manchmal als "quellkompatibel" bezeichnet. Dies impliziert, dass ein Programm, das für eine Bibliotheksversion kompiliert wurde, mit dem anderen nicht funktioniert, der für eine Version geschriebene Quellcode jedoch für die andere Version, wenn er neu kompiliert wird.

Aus diesem Grund versuchen Bibliotheksschreiber, ihre ABI stabil zu halten (um Störungen zu minimieren). Ein ABI stabil zu halten bedeutet, Funktionsschnittstellen (Rückgabetyp und -nummer, -typen und -reihenfolge der Argumente), Definitionen von Datentypen oder Datenstrukturen, definierte Konstanten usw. nicht zu ändern. Neue Funktionen und Datentypen können hinzugefügt werden, vorhandene müssen jedoch bestehen bleiben das Gleiche. Wenn Sie beispielsweise ein 16-Bit-Datenstrukturfeld in ein 32-Bit-Feld erweitern, greift der bereits kompilierte Code, der diese Datenstruktur verwendet, nicht ordnungsgemäß auf dieses Feld (oder ein darauf folgendes) zu. Der Zugriff auf Datenstrukturelemente wird während der Kompilierung in Speicheradressen und Offsets konvertiert. Wenn sich die Datenstruktur ändert, zeigen diese Offsets nicht auf das, was der Code von ihnen erwartet, und die Ergebnisse sind bestenfalls unvorhersehbar.

Ein ABI ist nicht unbedingt etwas, das Sie explizit bereitstellen, es sei denn, Sie erwarten, dass Benutzer mithilfe von Assembly eine Schnittstelle zu Ihrem Code herstellen. Es ist auch nicht sprachspezifisch, da (zum Beispiel) eine C-Anwendung und eine Pascal-Anwendung nach dem Kompilieren dieselbe ABI verwenden.

Bearbeiten: Bezüglich Ihrer Frage zu den Kapiteln bezüglich des ELF-Dateiformats in den SysV ABI-Dokumenten: Der Grund, warum diese Informationen enthalten sind, liegt darin, dass das ELF-Format das ELF-Format definiert Schnittstelle zwischen Betriebssystem und Anwendung. Wenn Sie das Betriebssystem anweisen, ein Programm auszuführen, erwartet es, dass das Programm auf eine bestimmte Art und Weise formatiert wird, und (zum Beispiel), dass der erste Abschnitt der Binärdatei ein ELF-Header ist, der bestimmte Informationen zu bestimmten Speicher-Offsets enthält. Auf diese Weise übermittelt die Anwendung wichtige Informationen über sich selbst an das Betriebssystem. Wenn Sie ein Programm in einem Nicht-ELF-Binärformat (z. B. a.out oder PE) erstellen, kann ein Betriebssystem, das ELF-formatierte Anwendungen erwartet, die Binärdatei nicht interpretieren oder die Anwendung nicht ausführen. Dies ist ein wichtiger Grund, warum Windows-Apps nicht direkt auf einem Linux-Computer (oder umgekehrt) ausgeführt werden können, ohne dass sie neu kompiliert oder in einer Art Emulationsebene ausgeführt werden, die von einem Binärformat in ein anderes konvertiert werden kann.

IIRC, Windows verwendet derzeit das Format Portable Executable (oder PE). Im Abschnitt "Externe Links" dieser Wikipedia-Seite finden Sie Links mit weiteren Informationen zum PE-Format.

Auch in Bezug auf Ihre Anmerkung zur C++ - Namensverknüpfung: Die ABI kann aus Kompatibilitätsgründen eine "standardisierte" Möglichkeit für einen C++ - Compiler definieren, die Namensverknüpfung durchzuführen. Das heißt, wenn ich eine Bibliothek erstelle und Sie ein Programm entwickeln, das die Bibliothek verwendet, sollten Sie in der Lage sein, einen anderen Compiler zu verwenden als ich und sich nicht darum sorgen müssen, dass die resultierenden Binärdateien aufgrund unterschiedlicher Namensverwaltungsschemata inkompatibel sind. Dies ist nur dann von Nutzen, wenn Sie ein neues Binärdateiformat definieren oder einen Compiler oder Linker schreiben.

448
bta

Wenn Sie mit Assembly vertraut sind und wissen, wie die Dinge auf Betriebssystemebene funktionieren, müssen Sie sich an eine bestimmte ABI halten. Die ABI regeln Dinge wie die Übergabe von Parametern und die Platzierung von Rückgabewerten. Für viele Plattformen steht nur eine ABI zur Auswahl, und in diesen Fällen ist die ABI nur "Funktionsweise".

Die ABI regeln jedoch auch Dinge wie das Layout von Klassen/Objekten in C++. Dies ist erforderlich, wenn Sie Objektreferenzen über Modulgrenzen hinweg übergeben oder mit verschiedenen Compilern kompilierten Code mischen möchten.

Wenn Sie ein 64-Bit-Betriebssystem haben, das 32-Bit-Binärdateien ausführen kann, haben Sie auch unterschiedliche ABIs für 32- und 64-Bit-Code.

Im Allgemeinen muss jeder Code, den Sie in dieselbe ausführbare Datei verlinken, mit demselben ABI übereinstimmen. Wenn Sie mit verschiedenen ABIs zwischen Code kommunizieren möchten, müssen Sie eine Form von RPC- oder Serialisierungsprotokollen verwenden.

Ich denke, Sie sind zu sehr bemüht, verschiedene Arten von Schnittstellen in einen festen Satz von Merkmalen einzudrücken. Beispielsweise muss eine Schnittstelle nicht notwendigerweise in Konsumenten und Produzenten aufgeteilt werden. Eine Schnittstelle ist nur eine Konvention, mit der zwei Entitäten interagieren.

ABIs können (teilweise) ISA-agnostisch sein. Einige Aspekte (z. B. Aufrufkonventionen) hängen vom ISA ab, andere (z. B. C++ - Klassenlayout) nicht.

Ein gut definiertes ABI ist sehr wichtig für Leute, die Compiler schreiben. Ohne eine genau definierte ABI wäre es unmöglich, interoperablen Code zu generieren.

EDIT: Einige Hinweise zur Verdeutlichung:

  • "Binär" in ABI schließt die Verwendung von Zeichenfolgen oder Text nicht aus. Wenn Sie eine DLL exportierende C++ - Klasse verknüpfen möchten, müssen die Methoden und Typensignaturen irgendwo darin codiert werden.
  • Der Grund, warum Sie nie ein ABI angegeben haben, ist, dass die überwiegende Mehrheit der Programmierer dies niemals tun wird. ABIs werden von denselben Personen bereitgestellt, die die Plattform (d. H. Das Betriebssystem) entwerfen, und nur sehr wenige Programmierer werden jemals das Privileg haben, ein weit verbreitetes ABI zu entwerfen.
129
JesperE

Sie brauchen nicht überhaupt einen ABI, wenn--

  • Ihr Programm hat keine Funktionen und--
  • Ihr Programm ist eine einzelne ausführbare Datei, die alleine ausgeführt wird (d. H. Ein eingebettetes System), auf dem buchstäblich nur sie ausgeführt wird und mit keiner anderen Datei gesprochen werden muss.

Eine vereinfachte Zusammenfassung:

API: "Hier sind alle Funktionen, die Sie aufrufen können."

ABI: "Dies ist , wie eine Funktion aufgerufen wird . "

Die ABI besteht aus Regeln, die Compiler und Linker einhalten, um Ihr Programm so zu kompilieren, dass es ordnungsgemäß funktioniert. ABIs decken mehrere Themen ab:

  • Der wohl größte und wichtigste Teil eines ABI ist der Prozeduraufrufstandard , der manchmal als "Aufrufkonvention" bezeichnet wird. Aufrufkonventionen standardisieren, wie "Funktionen" in Assembly-Code übersetzt werden.
  • ABIs schreiben auch vor, wie die names von exponierten Funktionen in Bibliotheken dargestellt werden sollen, damit anderer Code diese Bibliotheken aufrufen und wissen kann, welche Argumente übergeben werden sollen. Dies nennt man "Name Mangling".
  • ABIs bestimmen auch, welche Datentypen verwendet werden können, wie sie ausgerichtet werden müssen und andere Details auf niedriger Ebene.

Ein tieferer Blick auf die Calling Convention, die ich für den Kern eines ABI halte:

Die Maschine selbst kennt keine "Funktionen". Wenn Sie eine Funktion in einer höheren Sprache wie c schreiben, generiert der Compiler eine Zeile mit Assembly-Code wie _MyFunction1:. Dies ist ein label, das vom Assembler schließlich in eine Adresse aufgelöst wird. Dieses Etikett markiert den "Start" Ihrer "Funktion" im Assembly-Code. Wenn Sie im Code auf hoher Ebene diese Funktion "aufrufen", veranlassen Sie die CPU, jump zur Adresse dieses Etiketts zu springen und dort weiter auszuführen.

In Vorbereitung auf den Sprung muss der Compiler einige wichtige Dinge tun. Die Aufrufkonvention ist wie eine Checkliste, die der Compiler befolgt, um all diese Dinge zu erledigen:

  • Zunächst fügt der Compiler ein wenig Assembly-Code ein, um die aktuelle Adresse zu speichern, sodass die CPU nach Abschluss Ihrer "Funktion" an die richtige Stelle zurückspringen und die Ausführung fortsetzen kann.
  • Als Nächstes generiert der Compiler Assembly-Code, um die Argumente zu übergeben.
    • Einige Aufrufkonventionen schreiben vor, dass Argumente auf dem Stack abgelegt werden sollen ( natürlich in einer bestimmten Reihenfolge).
    • Andere Konventionen schreiben vor, dass die Argumente in bestimmten Registern abgelegt werden sollen ( je nach Datentyp natürlich).
    • Wieder andere Konventionen schreiben vor, dass eine bestimmte Kombination aus Stapel und Registern verwendet werden sollte.
  • Wenn in diesen Registern zuvor etwas Wichtiges vorlag, werden diese Werte jetzt überschrieben und gehen für immer verloren. Einige Aufrufkonventionen können daher vorschreiben, dass der Compiler einige dieser Register speichern sollte, bevor die Argumente in sie eingefügt werden.
  • Jetzt fügt der Compiler einen Sprungbefehl ein, der die CPU auffordert, zu dem zuvor erstellten Label zu wechseln (_MyFunction1:). An dieser Stelle können Sie die CPU als "in" Ihrer "Funktion" betrachten.
  • Am Ende der Funktion gibt der Compiler einen Assembly-Code ein, der die CPU veranlasst, den Rückgabewert an die richtige Stelle zu schreiben. Die Aufrufkonvention bestimmt, ob der Rückgabewert in ein bestimmtes Register (abhängig von seinem Typ) oder in den Stapel geschrieben werden soll.
  • Jetzt ist es Zeit für Aufräumarbeiten. Die Aufrufkonvention bestimmt, wo der Compiler den Bereinigungs-Assembly-Code ablegt.
    • Einige Konventionen besagen, dass der Aufrufer den Stack aufräumen muss. Dies bedeutet, dass, nachdem die "Funktion" ausgeführt wurde und die CPU dorthin zurückspringt, wo sie zuvor war, der nächste auszuführende Code ein sehr spezifischer Bereinigungscode sein sollte.
    • Andere Konventionen besagen, dass sich einige bestimmte Teile des Bereinigungscodes am Ende der "Funktion" befinden sollten vor beim Zurückspringen.

Es gibt viele verschiedene ABIs/Aufrufkonventionen. Einige der wichtigsten sind:

  • Für die x86- oder x86-64-CPU (32-Bit-Umgebung):
    • CDECL
    • STDCALL
    • SCHNELLANRUF
    • VECTORCALL
    • DIESER ANRUF
  • Für die x86-64-CPU (64-Bit-Umgebung):
    • SYSTEMV
    • MSNATIVE
    • VECTORCALL
  • Für die ARM CPU (32-Bit)
    • AAPCS
  • Für die ARM CPU (64-Bit)
    • AAPCS64

Here ist eine großartige Seite, die die Unterschiede in der Assembly zeigt, die beim Kompilieren für verschiedene ABIs entstehen.

Eine andere Sache zu erwähnen ist, dass ein ABI nicht nur relevant ist inside das ausführbare Modul Ihres Programms. Es wird auch vom Linker verwendet, um sicherzustellen, dass Ihr Programm die Bibliotheksfunktionen korrekt aufruft. Auf Ihrem Computer werden mehrere gemeinsam genutzte Bibliotheken ausgeführt. Solange Ihr Compiler weiß, welche ABI sie jeweils verwenden, kann er Funktionen von ihnen ordnungsgemäß aufrufen, ohne den Stapel zu sprengen.

Ihr Compiler zu verstehen, wie Bibliotheksfunktionen aufgerufen werden, ist äußerst wichtig . Auf einer gehosteten Plattform (dh einer Plattform, auf der ein Betriebssystem Programme lädt) kann Ihr Programm nicht einmal blinken, ohne einen Kernel-Aufruf zu tätigen.

28
Lakey

Eine Application Binary Interface (ABI) ähnelt einer API, die Funktion ist jedoch auf Quellcodeebene für den Aufrufer nicht verfügbar. Es ist nur eine binäre Darstellung zugänglich/verfügbar.

ABIs können auf Prozessorarchitekturebene oder auf Betriebssystemebene definiert werden. Die ABIs sind Standards, die von der Code-Generator-Phase des Compilers befolgt werden müssen. Der Standard wird entweder vom Betriebssystem oder vom Prozessor festgelegt.

Funktionalität: Definieren Sie den Mechanismus/Standard, um Funktionsaufrufe unabhängig von der Implementierungssprache oder einem bestimmten Compiler/Linker/Toolchain durchzuführen. Geben Sie den Mechanismus an, der JNI oder eine Python-C-Schnittstelle usw. ermöglicht.

Bestehende Entitäten: Funktionen in Maschinencode-Form.

Consumer: Eine andere Funktion (einschließlich einer in einer anderen Sprache, kompiliert von einem anderen Compiler oder verlinkt von einem anderen Linker).

17
alvin

Funktionalität: Eine Reihe von Verträgen, die sich auf den Compiler, die Assembly-Writer, den Linker und das Betriebssystem auswirken. Die Verträge legen fest, wie Funktionen angeordnet sind, wo Parameter übergeben werden, wie Parameter übergeben werden und wie Funktionsrückgaben funktionieren. Diese sind in der Regel spezifisch für ein Tuple (Prozessorarchitektur, Betriebssystem).

Vorhandene Entitäten: Parameterlayout, Funktionssemantik, Registerzuordnung. Zum Beispiel hat die ARM Architektur zahlreiche ABIs (APCS, EABI, GNU-EABI, ungeachtet einer Reihe historischer Fälle) - die Verwendung eines gemischten ABIs führt dazu, dass Ihr Code einfach nicht funktioniert, wenn über Grenzen hinweg telefonieren.

Consumer: Der Compiler, Assembly Writer, Betriebssystem, CPU-spezifische Architektur.

Wer braucht diese Angaben? Der Compiler, Assembly-Writer, Linker, die Code generieren (oder Ausrichtungsanforderungen erfüllen), das Betriebssystem (Interrupt-Behandlung, Syscall-Schnittstelle). Wenn Sie die Assembly-Programmierung durchgeführt haben, haben Sie sich an eine ABI gewöhnt!

C++ - Namensverknüpfung ist ein Sonderfall - es handelt sich um ein Linker- und dynamisches Linker-zentriertes Problem. Wenn die Namensverknüpfung nicht standardisiert ist, funktioniert die dynamische Verknüpfung nicht. Von nun an wird das C++ ABI genau so genannt, das C++ ABI. Es handelt sich nicht um ein Problem auf Linker-Ebene, sondern um ein Problem bei der Codegenerierung. Sobald Sie eine C++ - Binärdatei haben, ist es nicht möglich, sie mit einer anderen C++ - ABI-Datei (Name Mangling, Ausnahmebehandlung) kompatibel zu machen, ohne sie erneut aus dem Quellcode zu kompilieren.

ELF ist ein Dateiformat für die Verwendung eines Loaders und eines Dynamic Linkers. ELF ist ein Containerformat für Binärcode und Daten und gibt als solches die ABI eines Codeteils an. Ich würde ELF nicht als ABI im engeren Sinne betrachten, da ausführbare PE-Dateien keine ABI sind.

Alle ABIs sind befehlssatzspezifisch. Ein ARM ABI ist auf einem MSP430- oder x86_64-Prozessor nicht sinnvoll.

Windows verfügt über mehrere ABIs - Fastcall und Stdcall sind beispielsweise zwei häufig verwendete ABIs. Das Syscall ABI ist wieder anders.

10
Yann Ramin

Lassen Sie mich wenigstens einen Teil Ihrer Frage beantworten. An einem Beispiel, wie sich das Linux-ABI auf die Systemaufrufe auswirkt und warum das nützlich ist.

Ein Systemcall ist eine Möglichkeit für ein Userspace-Programm, den Kernelspace nach etwas zu fragen. Es funktioniert, indem der numerische Code für den Aufruf und das Argument in ein bestimmtes Register eingegeben und ein Interrupt ausgelöst wird. Dann wird zu Kernelspace gewechselt und der Kernel sucht nach dem numerischen Code und dem Argument, verarbeitet die Anforderung, legt das Ergebnis wieder in einem Register ab und löst einen Wechsel zurück zu Userspace aus. Dies wird beispielsweise benötigt, wenn die Anwendung Speicher zuweisen oder eine Datei öffnen möchte (syscalls "brk" und "open").

Jetzt haben die Syscalls Kurznamen "brk" usw. und entsprechende Opcodes, diese sind in einer systemspezifischen Header-Datei definiert. Solange diese Opcodes gleich bleiben, können Sie dieselben kompilierten Userland-Programme mit verschiedenen aktualisierten Kerneln ausführen, ohne sie neu kompilieren zu müssen. Sie haben also eine Schnittstelle, die von vorkompilierten Binärdateien verwendet wird, daher ABI.

7
snies

Der beste Weg, zwischen ABI und API zu unterscheiden, ist zu wissen, warum und wofür es verwendet wird:

Für x86-64 gibt es im Allgemeinen eine ABI (und für x86 32-Bit gibt es eine andere Gruppe):

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 folgen mit einigen geringfügigen Abweichungen. Und Windows x64 hat ein eigenes ABI:

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

Wenn man das ABI kennt und davon ausgeht, dass auch andere Compiler folgen, wissen die Binärdateien theoretisch, wie sie sich gegenseitig aufrufen (insbesondere Bibliotheks - API) und Parameter über den Stack oder über Register usw. übergeben. Oder welche Register werden beim Aufrufen der Funktionen usw. geändert Im Wesentlichen hilft dieses Wissen der Software, sich ineinander zu integrieren. Wenn ich die Reihenfolge der Register/Stapel-Layouts kenne, kann ich problemlos verschiedene in Baugruppen geschriebene Software zusammenfügen.

Aber API sind anders:

Hierbei handelt es sich um Funktionsnamen auf hoher Ebene, für die ein Argument definiert ist. Wenn verschiedene Softwareteile mithilfe dieser API erstellt werden, können sie möglicherweise ineinander aufgerufen werden. Eine zusätzliche Anforderung von SAME ABI muss jedoch eingehalten werden.

Beispielsweise war Windows früher mit POSIX API kompatibel:

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

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

Und Linux ist auch POSIX-kompatibel. Die Binärdateien können jedoch nicht einfach verschoben und sofort ausgeführt werden. Da in der POSIX-kompatiblen API dieselben NAMES verwendet wurden, können Sie dieselbe Software in C verwenden, sie in einem anderen Betriebssystem neu kompilieren und sofort zum Laufen bringen.

APIs sollen die Integration von Software erleichtern - Phase vor der Kompilierung. Nach der Kompilierung kann die Software also völlig anders aussehen - wenn die ABI anders sind.

Mit ABI soll die exakte Integration von Software auf Binär-/Assembly-Ebene definiert werden.

4
Peter Teoh

Um Code in gemeinsam genutzten Bibliotheken oder Code zwischen Kompilierungseinheiten aufzurufen, muss die Objektdatei Beschriftungen für die Aufrufe enthalten. In C++ werden die Namen von Methodenbezeichnungen unkenntlich gemacht, um das Ausblenden von Daten zu erzwingen und überladene Methoden zuzulassen. Aus diesem Grund können Sie keine Dateien von verschiedenen C++ - Compilern mischen, es sei denn, sie unterstützen explizit dasselbe ABI.

3
Justin Smith

Zusammenfassung

Es gibt verschiedene Interpretationen und Meinungen über die genaue Schicht, die eine ABI (Application Binary Interface) definieren.

Meiner Ansicht nach ist ein ABI eine subjektive Konvention dessen, was als gegeben/Plattform für eine bestimmte API angesehen wird. Die ABI ist der "Rest" der Konventionen, die sich für eine bestimmte API "nicht ändern" oder die von der Laufzeitumgebung angesprochen werden: Executoren, Tools, Linker, Compiler, JVM und Betriebssystem.

Definieren einer Schnittstelle: ABI, API

Wenn Sie eine Bibliothek wie joda-time verwenden möchten, müssen Sie eine Abhängigkeit von joda-time-<major>.<minor>.<patch>.jar deklarieren. Die Bibliothek folgt den Best Practices und verwendet Semantic Versioning . Dies definiert die API-Kompatibilität auf drei Ebenen:

  1. Patch - Sie müssen Ihren Code nicht ändern. Die Bibliothek behebt nur einige Fehler.
  2. Klein - Sie müssen Ihren Code seit den Ergänzungen nicht mehr ändern
  3. Major - Die Schnittstelle (API) wurde geändert und Sie müssen möglicherweise Ihren Code ändern.

Damit Sie eine neue Hauptversion derselben Bibliothek verwenden können, müssen noch viele andere Konventionen beachtet werden:

  • Die für die Bibliotheken verwendete Binärsprache (in Java Fällen die JVM-Zielversion, die den Java Bytecode definiert)
  • Konventionen aufrufen
  • JVM-Konventionen
  • Verknüpfungskonventionen
  • Laufzeitkonventionen Alle diese Konventionen werden von den von uns verwendeten Tools definiert und verwaltet.

Beispiele

Java-Fallstudie

Zum Beispiel standardisierte Java alle diese Konventionen, nicht in einem Tool, sondern in einer formalen JVM-Spezifikation. Die Spezifikation ermöglichte es anderen Anbietern, einen anderen Satz von Tools bereitzustellen, mit denen kompatible Bibliotheken ausgegeben werden können.

Java bietet zwei weitere interessante Fallstudien für ABI: Scala Versionen und Dalvik virtuelle Maschine.

Die Dalvik Virtual Machine hat den ABI-Fehler behoben

Der Dalvik VM benötigt eine andere Art von Bytecode als der Java Bytecode. Die Dalvik-Bibliotheken werden durch Konvertieren des Bytecodes Java (mit derselben API) für Dalvik erhalten. Auf diese Weise erhalten Sie zwei Versionen derselben API: definiert durch den ursprünglichen joda-time-1.7.2.jar. Wir könnten mich joda-time-1.7.2.jar und joda-time-1.7.2-dalvik.jar nennen. Sie verwenden eine andere ABI-Version für den stapelorientierten Standard Java vms: Oracle-Version, IBM-Version, open Java oder eine andere; und der zweite ABI ist der um Dalvik.

Aufeinanderfolgende Scala-Versionen sind nicht kompatibel

Scala hat keine Binärkompatibilität zwischen kleineren Scala Versionen: 2.X. Aus diesem Grund hat dieselbe API "io.reactivex" %% "rxscala"% "0.26.5" drei Versionen (in Zukunft mehr): für Scala 2.10, 2.11 und 2.12. Was hat sich geändert? Ich weiß es momentan nicht , aber die Binärdateien sind nicht kompatibel. Wahrscheinlich fügt die neueste Version Dinge hinzu, die die Bibliotheken auf den alten virtuellen Maschinen unbrauchbar machen, wahrscheinlich Dinge, die mit Verknüpfungs-, Namens- und Parameterkonventionen zusammenhängen.

Aufeinanderfolgende Java-Versionen sind nicht kompatibel

Java hat auch Probleme mit den Hauptversionen der JVM: 4,5,6,7,8,9. Sie bieten nur Abwärtskompatibilität. Jvm9 kann kompilierten/gezielten Code (javacs Option -target) für alle anderen Versionen ausführen, während JVM 4 nicht weiß, wie für JVM 5 gezielter Code ausgeführt wird. In all diesen Fällen verfügen Sie über eine Joda-Bibliothek. Diese Inkompatibilität wird vom Radar durch verschiedene Lösungen überlagert:

  1. Semantische Versionierung: Wenn Bibliotheken auf eine höhere JVM abzielen, ändern sie normalerweise die Hauptversion.
  2. Verwenden Sie JVM 4 als ABI, und Sie sind sicher.
  3. Java 9 fügt eine Spezifikation hinzu, wie Sie Bytecode für eine bestimmte Ziel-JVM in dieselbe Bibliothek aufnehmen können.

Warum habe ich mit der API-Definition begonnen?

API und ABI sind nur Konventionen zur Definition der Kompatibilität. Die unteren Schichten sind im Hinblick auf eine Vielzahl von Semantiken auf hoher Ebene generisch. Deshalb ist es einfach, einige Konventionen zu treffen. Die erste Art von Konventionen befasst sich mit Speicherausrichtung, Bytecodierung, Aufrufkonventionen, Big- und Little-Endian-Codierungen usw. Darüber hinaus erhalten Sie die ausführbaren Konventionen wie die anderen beschriebenen, Verknüpfungskonventionen Intermediate-Bytecode wie von Java oder LLVM IR von GCC verwendet. Drittens erhalten Sie Konventionen zum Auffinden und Laden von Bibliotheken (siehe Java Klassenladeprogramme). Wenn Sie in Konzepten immer höher gehen, haben Sie neue Konventionen, die Sie als gegeben betrachten. Deshalb haben sie es nicht bis zur semantischen Versionierung geschafft. Sie sind implizit oder in der Hauptversion reduziert. Wir könnten die semantische Versionierung mit <major>-<minor>-<patch>-<platform/ABI> ändern. Dies ist, was tatsächlich bereits passiert: Plattform ist bereits ein rpm, dll, jar (JVM-Bytecode), war (JVM + Webserver), apk, 2.11 (spezifische Scala Version) und so weiter. Wenn Sie APK sagen, sprechen Sie bereits über einen bestimmten ABI-Teil Ihrer API.

API kann auf verschiedene ABI portiert werden

Die oberste Ebene einer Abstraktion (die Quellen, die für die höchste API geschrieben wurden, können erneut kompiliert/auf eine andere Abstraktion einer niedrigeren Ebene portiert werden).

Nehmen wir an, ich habe einige Quellen für rxscala. Wenn die Werkzeuge von Scala geändert werden, kann ich sie damit neu kompilieren. Wenn sich die JVM ändert, könnte ich automatisch von der alten auf die neue Maschine konvertieren, ohne mich um die übergeordneten Konzepte zu kümmern. Während die Portierung schwierig sein kann, hilft dies jedem anderen Client. Wenn ein neues Betriebssystem mit einem völlig anderen Assembler-Code erstellt wird, kann ein Übersetzer erstellt werden.

Sprachenübergreifende APIs

Es gibt APIs, die in mehreren Sprachen portiert sind, z. B. reaktive Streams . Im Allgemeinen definieren sie Zuordnungen zu bestimmten Sprachen/Plattformen. Ich würde argumentieren, dass die API die Hauptspezifikation ist, die formal in der menschlichen Sprache oder sogar in einer bestimmten Programmiersprache definiert ist. Alle anderen "Zuordnungen" sind in gewissem Sinne ABI, ansonsten mehr API als das übliche ABI. Das gleiche passiert mit den REST -Interfaces.

3
raisercostin

Beispiel für eine gemeinsam genutzte Linux-Bibliothek mit minimal ausführbarem ABI

Im Zusammenhang mit gemeinsam genutzten Bibliotheken besteht die wichtigste Implikation für "ein stabiles ABI" darin, dass Sie Ihre Programme nach den Bibliotheksänderungen nicht neu kompilieren müssen.

Also zum Beispiel:

  • wenn Sie eine gemeinsam genutzte Bibliothek verkaufen, ersparen Sie Ihren Benutzern den Ärger, bei jeder neuen Version alles neu zu kompilieren, was von Ihrer Bibliothek abhängt

  • wenn Sie ein Closed-Source-Programm verkaufen, das von einer gemeinsam genutzten Bibliothek in der Distribution des Benutzers abhängt, können Sie weniger Prebuilts freigeben und testen, wenn Sie sicher sind, dass ABI in bestimmten Versionen des Zielbetriebssystems stabil ist.

    Dies ist besonders wichtig bei der C-Standardbibliothek, auf die viele, viele Programme in Ihrem System verweisen.

Jetzt möchte ich ein minimales konkretes lauffähiges Beispiel dafür liefern.

haupt 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

Kompiliert und läuft gut mit:

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

Angenommen, für Version 2 der Bibliothek möchten wir mylib_mystruct Ein neues Feld mit dem Namen new_field Hinzufügen.

Wenn wir das Feld vor old_field Hinzugefügt haben, wie in:

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

und die Bibliothek neu aufgebaut, aber nicht main.out, dann schlägt die Zusicherung fehl!

Dies liegt daran, dass die Zeile:

myobject->old_field == 1

assembly generiert, die versucht, auf das allererste int der Struktur zuzugreifen, das jetzt new_field anstelle des erwarteten old_field lautet.

Daher hat diese Änderung den ABI gebrochen.

Wenn wir jedoch new_field Nach old_field Einfügen:

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

dann greift die alte generierte Assembly immer noch auf das erste int der Struktur zu, und das Programm funktioniert immer noch, weil wir das ABI stabil gehalten haben.

Hier ist eine vollautomatische Version dieses Beispiels auf GitHub .

Eine andere Möglichkeit, dieses ABI stabil zu halten, wäre gewesen, mylib_mystruct Als ndurchsichtige Struktur zu behandeln und auf seine Felder nur mit Hilfe von Methoden zuzugreifen. Dies macht es einfacher, das ABI stabil zu halten, würde jedoch einen Leistungsaufwand verursachen, da wir mehr Funktionsaufrufe durchführen würden.

API vs ABI

Im vorherigen Beispiel ist es interessant zu bemerken, dass das Hinzufügen von new_field Vor old_field Nur den ABI, nicht aber die API brach.

Das heißt, wenn wir unser main.c - Programm für die Bibliothek neu kompiliert hätten, hätte es trotzdem funktioniert.

Wir hätten die API jedoch auch beschädigt, wenn wir zum Beispiel die Funktionssignatur geändert hätten:

mylib_mystruct* mylib_init(int old_field, int new_field);

da in diesem Fall main.c die Kompilierung vollständig einstellen würde.

Semantische API vs. Programmier-API

Wir können API-Änderungen auch in einen dritten Typ einteilen: semantische Änderungen.

Die semantische API ist normalerweise eine Beschreibung in natürlicher Sprache, die beschreibt, was die API tun soll. Sie ist normalerweise in der API-Dokumentation enthalten.

Es ist daher möglich, die semantische API zu unterbrechen, ohne den Programmaufbau selbst zu unterbrechen.

Zum Beispiel, wenn wir modifiziert hätten

myobject->old_field = old_field;

zu:

myobject->old_field = old_field + 1;

dann hätte dies weder die Programmierschnittstelle noch ABI beschädigt, aber main.c die semantische API würde beschädigt.

Es gibt zwei Möglichkeiten, die Vertrags-API programmgesteuert zu überprüfen:

  • testen Sie eine Reihe von Eckfällen. Einfach zu machen, aber Sie könnten immer eine verpassen.
  • formelle Überprüfung . Schwieriger zu machen, aber mathematische Korrektheitsnachweise zu erbringen, die Dokumentation und Tests auf eine "menschliche"/maschinell überprüfbare Art und Weise vereinen! Solange Ihre formale Beschreibung natürlich keinen Fehler enthält ;-)

    Dieses Konzept steht in engem Zusammenhang mit der Formalisierung der Mathematik selbst: https://math.stackexchange.com/questions/53969/what-does-formal-mean/3297537#3297537

Liste von allem, was C/C++ Shared Library ABIs kaputt macht

TODO: finde/erstelle die ultimative Liste:

Java minimal ausführbares Beispiel

Was ist Binärkompatibilität in Java?

Getestet in Ubuntu 18.10, GCC 8.2.0.

Ich habe auch versucht, ABI zu verstehen, und die Antwort von JesperE war sehr hilfreich.

Aus einer sehr einfachen Perspektive können wir versuchen, ABI unter Berücksichtigung der Binärkompatibilität zu verstehen.

KDE-Wiki definiert eine Bibliothek als binär kompatibel, „wenn ein Programm, das dynamisch mit einer früheren Version der Bibliothek verknüpft ist, weiterhin mit neueren Versionen der Bibliothek ausgeführt wird, ohne dass eine Neukompilierung erforderlich ist.“ Weitere Informationen zum dynamischen Verknüpfen finden Sie unter Statische Verknüpfung vs. dynamische Verknüpfung

Lassen Sie uns nun versuchen, nur die grundlegendsten Aspekte zu betrachten, die für die Binärkompatibilität einer Bibliothek erforderlich sind (vorausgesetzt, die Bibliothek enthält keine Quellcodeänderungen):

  1. Gleiche/abwärtskompatible Befehlssatzarchitektur (Prozessoranweisungen, Registerdateistruktur, Stapelorganisation, Speicherzugriffstypen sowie Größen, Layout und Ausrichtung der grundlegenden Datentypen, auf die der Prozessor direkt zugreifen kann)
  2. Gleiche Aufrufkonventionen
  3. Gleichnamige Mangling-Konvention (dies kann erforderlich sein, wenn ein Fortran-Programm beispielsweise eine C++ - Bibliotheksfunktion aufrufen muss).

Sicher, es gibt viele andere Details, aber das ist meistens das, was der ABI auch abdeckt.

Um Ihre Frage genauer zu beantworten, können wir aus dem oben Gesagten Folgendes ableiten:

ABI-Funktionalität: Binärkompatibilität

bestehende Entitäten: Bestehende Programme/Bibliotheken/Betriebssysteme

verbraucher: Bibliotheken, OS

Hoffe das hilft!

1
blue_whale

Der Begriff ABI bezieht sich auf zwei unterschiedliche, aber verwandte Konzepte.

Wenn es um Compiler geht, bezieht es sich auf die Regeln, die zur Übersetzung von Konstrukten auf Quellenebene in Binärkonstrukte verwendet werden. Wie groß sind die Datentypen? Wie funktioniert der Stack? Wie übergebe ich Parameter an Funktionen? Welche Register sollten vom Anrufer gegen den Angerufenen gespeichert werden?

Wenn es sich um Bibliotheken handelt, bezieht es sich auf die binäre Schnittstelle, die von einer kompilierten Bibliothek dargestellt wird. Diese Schnittstelle ist das Ergebnis einer Reihe von Faktoren, einschließlich des Quellcodes der Bibliothek, der vom Compiler verwendeten Regeln und in einigen Fällen von anderen Bibliotheken übernommener Definitionen.

Änderungen an einer Bibliothek können die ABI beschädigen, ohne die API zu beschädigen. Betrachten Sie zum Beispiel eine Bibliothek mit einer Schnittstelle wie.

void initfoo(FOO * foo)
int usefoo(FOO * foo, int bar)
void cleanupfoo(FOO * foo)

und der Anwendungsprogrammierer schreibt Code wie

int dostuffwithfoo(int bar) {
  FOO foo;
  initfoo(&foo);
  int result = usefoo(&foo,bar)
  cleanupfoo(&foo);
  return result;
}

Der Anwendungsprogrammierer kümmert sich nicht um die Größe oder das Layout von FOO, aber die Anwendungsbinärdatei endet mit einer fest codierten Größe von foo. Wenn der Bibliotheksprogrammierer foo ein zusätzliches Feld hinzufügt und jemand die neue Bibliotheksbinärdatei mit der alten Anwendungsbinärdatei verwendet, greift die Bibliothek möglicherweise außerhalb der Grenzen auf den Speicher zu.

OTOH, wenn der Bibliotheksautor seine API so gestaltet hätte.

FOO * newfoo(void)
int usefoo(FOO * foo, int bar)
void deletefoo((FOO * foo, int bar))

und der Anwendungsprogrammierer schreibt Code wie

int dostuffwithfoo(int bar) {
  FOO * foo;
  foo = newfoo();
  int result = usefoo(&foo,bar)
  deletefoo(&foo);
  return result;
}

Dann muss die Anwendungsbinärdatei nichts über die Struktur von FOO wissen, die alle in der Bibliothek versteckt sein können. Der Preis, den Sie dafür zahlen, ist, dass Heap-Operationen beteiligt sind.

1
plugwash

Die ABI muss zwischen Anrufer und Angerufenen konsistent sein, um sicherzustellen, dass der Anruf erfolgreich ist. Stapelverwendung, Registerverwendung, Popup am Ende der Routine. All dies sind die wichtigsten Teile des ABI.

Anwendungsbinärschnittstelle (ABI)

Funktionalität:

  • Die Übersetzung vom Modell des Programmierers auf den Domänendatentyp, die Größe, die Ausrichtung und die Aufrufkonvention des zugrunde liegenden Systems, die steuert, wie die Argumente der Funktionen übergeben und die zurückgegebenen Werte abgerufen werden. die Systemrufnummern und wie eine Anwendung Systemaufrufe an das Betriebssystem tätigen soll; das Namensverwaltungsschema der Compiler für Hochsprachen, die Weitergabe von Ausnahmen und die Aufrufkonvention zwischen Compilern auf derselben Plattform, die jedoch keine plattformübergreifende Kompatibilität erfordern ...

Bestehende Entitäten:

  • Logische Blöcke, die direkt an der Programmausführung beteiligt sind: ALU, Universalregister, Register für die Speicher-/E/A-Zuordnung von E/A usw.

verbraucher:

  • Sprachprozessoren Linker, Assembler ...

Diese werden von jedem benötigt, der sicherstellen muss, dass Build-Toolketten als Ganzes funktionieren. Wenn Sie ein Modul in Assemblersprache schreiben, ein anderes in Python, und statt Ihres eigenen Bootloaders ein Betriebssystem verwenden möchten, arbeiten Ihre "Anwendungs" -Module über "binäre" Grenzen hinweg und erfordern die Zustimmung einer solchen "Schnittstelle".

Mangeln von C++ - Namen, da möglicherweise Objektdateien aus verschiedenen Hochsprachen in Ihrer Anwendung verknüpft werden müssen. Erwägen Sie die Verwendung der GCC-Standardbibliothek, um Systemaufrufe für Windows durchzuführen, die mit Visual C++ erstellt wurden.

ELF ist eine mögliche Erwartung an den Linker aus einer Objektdatei zur Interpretation, obwohl JVM möglicherweise eine andere Idee hat.

Versuchen Sie für eine Windows RT Store-App, nach ARM ABI zu suchen, wenn Sie wirklich möchten, dass einige Build-Toolketten zusammenarbeiten.

1
Chawathe Vipul

Kurz gesagt und in der Philosophie können nur Dinge einer Art gut miteinander auskommen, und das ABI könnte als das angesehen werden ) Art der Software, die zusammenarbeitet.

1
smwikipedia