web-dev-qa-db-fra.com

Qu'est-ce qu'une interface binaire d'application (ABI)?

Je n'ai jamais clairement compris ce qu'est un ABI. S'il vous plaît ne m'indiquez pas un article de Wikipedia. Si je pouvais le comprendre, je ne serais pas là en train de poster un si long post.

Ceci est mon état d'esprit à propos des différentes interfaces:

Une télécommande de téléviseur est une interface entre l'utilisateur et le téléviseur. C'est une entité existante, mais inutile (ne fournit aucune fonctionnalité) en soi. Toutes les fonctionnalités de chacun de ces boutons de la télécommande sont implémentées dans le téléviseur.

Interface: Il s'agit d'une couche "entité existante" située entre les functionality et consumer de cette fonctionnalité. Une interface en soi ne fait rien. Il appelle simplement la fonctionnalité qui se trouve derrière.

Maintenant, en fonction de l'utilisateur, il existe différents types d'interfaces.

Interface de ligne de commande (CLI) Les commandes sont les entités existantes, le consommateur est l'utilisateur et la fonctionnalité est derrière.

functionality: ma fonctionnalité logicielle qui résout un problème pour lequel nous décrivons cette interface.

existing entities: commandes

consumer: utilisateur

Interface graphique (GUI) fenêtre, boutons, etc. sont les entités existantes. Là encore, le consommateur est l'utilisateur et la fonctionnalité est derrière.

functionality: ma fonctionnalité logicielle qui résout un problème pour lequel nous décrivons cette interface.

existing entities: fenêtre, boutons, etc.

consumer: utilisateur

Interface de programmation d'application (API) Les fonctions (ou pour être plus correctes) les interfaces (dans la programmation basée sur l'interface) sont les entités existantes, le consommateur est ici un autre programme non un utilisateur, et encore une fonctionnalité se cache derrière cette couche.

functionality: ma fonctionnalité logicielle qui résout un problème pour lequel nous décrivons cette interface.

existing entities: fonctions, Interfaces (tableau de fonctions).

consumer: un autre programme/application.

Interface binaire d'application (ABI) Voici où mon problème commence.

functionality: ???

existing entities: ???

consumer: ???

  • J'ai écrit des logiciels dans différentes langues et fourni différents types d'interfaces (CLI, GUI et API), mais je ne suis pas sûr d'avoir déjà fourni une ABI.

Wikipedia dit:

Les ABI couvrent des détails tels que

  • type de données, taille et alignement;
  • la convention d'appel, qui contrôle la manière dont les arguments des fonctions sont transmis et les valeurs renvoyées sont extraites;
  • les numéros d'appel système et la manière dont une application doit effectuer des appels système vers le système d'exploitation;

D'autres ABI standardisent des détails tels que

  • le nom C++ se déformant,
  • propagation d'exception, et
  • convention d'appel entre les compilateurs sur la même plate-forme, mais n'exigent pas de compatibilité multiplate-forme.
  • Qui a besoin de ces détails? S'il vous plaît ne dites pas l'OS. Je connais la programmation de l'Assemblée. Je sais comment lier et charger fonctionne. Je sais exactement ce qui se passe à l'intérieur.

  • Pourquoi le nom C++ a-t-il été modifié? Je pensais que nous parlions au niveau binaire. Pourquoi les langues entrent-elles?

Quoi qu'il en soit, j'ai téléchargé le [PDF] interface binaire d'application System V Edition 4.1 (1997-03-18) pour voir ce qu'il contient exactement. Eh bien, la plupart de cela n'avait aucun sens.

  • Pourquoi contient-il deux chapitres (4ème et 5ème) décrivant le format de fichier ELF ? En fait, ce sont les deux seuls chapitres significatifs de cette spécification. Les autres chapitres sont "spécifiques au processeur". Quoi qu'il en soit, je pensais que c'était un sujet complètement différent. Ne dites pas que les spécifications de format de fichier ELF sont l’ABI. Il n'est pas qualifié pour être une interface selon la définition.

  • Je sais, puisque nous parlons à un niveau aussi bas, il doit être très spécifique. Mais je ne suis pas sûr de savoir comment est-il spécifique à "architecture de jeu d'instructions (ISA)"?

  • Où puis-je trouver l'ABI de Microsoft Windows?

Donc, ce sont les principales requêtes qui me dérangent.

418
claws

Un moyen simple de comprendre "ABI" consiste à le comparer à "API".

Vous connaissez déjà le concept d'une API. Si vous souhaitez utiliser les fonctionnalités d'une bibliothèque ou de votre système d'exploitation, par exemple, vous utiliserez une API. L'API se compose de types de données/structures, constantes, fonctions, etc. que vous pouvez utiliser dans votre code pour accéder aux fonctionnalités de ce composant externe.

Un ABI est très similaire. Considérez-le comme la version compilée d'une API (ou comme une API au niveau du langage machine). Lorsque vous écrivez du code source, vous accédez à la bibliothèque via une API. Une fois le code compilé, votre application accède aux données binaires de la bibliothèque via l’ABI. L’ABI définit les structures et les méthodes que votre application compilée utilisera pour accéder à la bibliothèque externe (tout comme l’API), mais à un niveau inférieur.

Les ABI sont importantes pour les applications utilisant des bibliothèques externes. Si un programme est conçu pour utiliser une bibliothèque particulière et que cette bibliothèque est mise à jour ultérieurement, vous ne souhaitez pas avoir à recompiler cette application (et du point de vue de l'utilisateur final, il se peut que vous n'ayez pas la source). Si la bibliothèque mise à jour utilise le même ABI, votre programme n'aura pas besoin de changer. L’interface avec la bibliothèque (qui intéresse tout votre programme) est la même, même si le fonctionnement interne a peut-être changé. Deux versions d'une bibliothèque qui ont le même ABI sont parfois appelées "compatibles binaires" car elles ont la même interface de bas niveau (vous devriez pouvoir remplacer l'ancienne version par la nouvelle sans avoir de problèmes majeurs).

Parfois, les changements ABI sont inévitables. Lorsque cela se produit, tous les programmes utilisant cette bibliothèque ne fonctionneront que s'ils sont recompilés pour utiliser la nouvelle version de la bibliothèque. Si l'ABI change mais que l'API ne change pas, les anciennes et les nouvelles versions de la bibliothèque sont parfois appelées "source compatible". Cela implique que, même si un programme compilé pour une version de bibliothèque ne fonctionnera pas avec l'autre, le code source écrit pour l'une fonctionnera pour l'autre s'il est recompilé.

Pour cette raison, les rédacteurs de bibliothèques ont tendance à essayer de maintenir leur ABI stable (pour minimiser les perturbations). Garder une ABI stable signifie ne pas changer les interfaces des fonctions (type et nombre de retour, types et ordre des arguments), les définitions des types de données ou des structures de données, les constantes définies, etc. De nouvelles fonctions et types de données peuvent être ajoutés, mais les existants doivent rester le même. Si vous développez, par exemple, un champ de structure de données de 16 bits en un champ de 32 bits, le code déjà compilé qui utilise cette structure de données n'accédera pas correctement à ce champ (ni à aucun de ses suivants). L'accès aux membres de la structure de données est converti en adresses de mémoire et en décalages lors de la compilation et si la structure de données change, ces décalages ne pointeront pas sur ce que le code attend d'eux et les résultats sont au mieux imprévisibles.

Une ABI n’est pas nécessairement quelque chose que vous fournirez explicitement, sauf si vous attendez des personnes qu’elles se connectent à votre code avec Assembly. Il n'est pas non plus spécifique à la langue, car (par exemple) une application C et une application Pascal utiliseront la même ABI après leur compilation.

Edit: En ce qui concerne votre question sur les chapitres concernant le format de fichier ELF dans la documentation ABI SysV: la raison pour laquelle ces informations sont incluses est le fait que le format ELF définit la interface entre le système d'exploitation et l'application. Lorsque vous indiquez au système d'exploitation d'exécuter un programme, il s'attend à ce que le programme soit formaté d'une certaine manière et, par exemple, à la première section du fichier binaire comme un en-tête ELF contenant certaines informations à des décalages de mémoire spécifiques. C’est ainsi que l’application communique au système d’exploitation des informations importantes la concernant. Si vous créez un programme dans un format binaire non-ELF (tel que a.out ou PE), un système d'exploitation qui attend des applications au format ELF ne sera pas en mesure d'interpréter le fichier binaire ou de l'exécuter. C'est l'une des principales raisons pour lesquelles les applications Windows ne peuvent pas être exécutées directement sur une machine Linux (ou inversement) sans être recompilées ou exécutées à l'intérieur d'un type de couche d'émulation pouvant être converti d'un format binaire à un autre.

IIRC, Windows utilise actuellement le format Portable Executable (ou PE). Il y a des liens dans la section "liens externes" de cette page Wikipedia avec plus d'informations sur le format PE.

En outre, en ce qui concerne votre remarque sur la modification de nom C++: L’ABI peut définir un moyen "normalisé" permettant à un compilateur C++ de gérer la modification de nom à des fins de compatibilité. C'est-à-dire que si je crée une bibliothèque et que vous développez un programme utilisant cette bibliothèque, vous devriez pouvoir utiliser un compilateur différent de celui que j'ai utilisé et ne pas avoir à vous soucier de l'incompatibilité des fichiers binaires résultants en raison de différents modèles de gestion des noms. Ceci n’est vraiment utile que si vous définissez un nouveau format de fichier binaire ou écrivez un compilateur ou un éditeur de liens.

448
bta

Si vous connaissez Assembly et comment les choses fonctionnent au niveau du système d'exploitation, vous vous conformez à un certain ABI. L’ABI régit des choses telles que la manière dont les paramètres sont passés, où les valeurs de retour sont placées. Pour de nombreuses plates-formes, il n'y a qu'un seul ABI parmi lequel choisir, et dans ce cas l'ABI est simplement "comment ça marche".

Cependant, les ABI régissent également des choses telles que la manière dont les classes/objets sont disposés en C++. Cela est nécessaire si vous voulez pouvoir passer des références d'objet à travers les limites du module ou si vous voulez mélanger du code compilé avec différents compilateurs.

De plus, si vous avez un système d'exploitation 64 bits capable d'exécuter des fichiers binaires 32 bits, vous aurez différents ABI pour le code 32 et 64 bits.

En général, tout code que vous associez au même exécutable doit être conforme au même ABI. Si vous souhaitez communiquer entre codes à l'aide de différentes ABI, vous devez utiliser une forme de protocole RPC ou de protocole de sérialisation.

Je pense que vous essayez trop de combiner différents types d’interfaces en un ensemble de caractéristiques fixe. Par exemple, une interface ne doit pas nécessairement être divisée en consommateurs et producteurs. Une interface est simplement une convention par laquelle deux entités interagissent.

Les ABI peuvent être (partiellement) ISA-agnostiques. Certains aspects (tels que les conventions d'appel) dépendent de l'ISA, d'autres non (tels que la disposition des classes C++) ne le sont pas.

Un ABI bien défini est très important pour ceux qui écrivent des compilateurs. Sans une ABI bien définie, il serait impossible de générer du code interopérable.

EDIT: Quelques notes pour clarifier:

  • "Binary" dans ABI n'exclut pas l'utilisation de chaînes ou de texte. Si vous souhaitez lier une DLL exportant une classe C++, les méthodes et les signatures de type doivent être codées quelque part. C'est là qu'intervient la gestion de noms C++.
  • La raison pour laquelle vous n’avez jamais fourni d’ABI est que la grande majorité des programmeurs ne le feront jamais. Les ABI sont fournis par les mêmes personnes qui conçoivent la plate-forme (c’est-à-dire le système d’exploitation), et très peu de programmeurs auront le privilège de concevoir une ABI largement utilisée.
129
JesperE

En fait n'avez pas besoin d'un ABI du tout si--

  • Votre programme n'a pas de fonctions, et--
  • Votre programme est un seul exécutable qui s'exécute seul (c'est-à-dire un système intégré) où il est littéralement la seule chose qui tourne et qui n'a pas besoin de parler à autre chose.

Un résumé trop simplifié:

API: "Voici toutes les fonctions que vous pouvez appeler."

ABI: "Ceci est comment pour appeler une fonction. "

L'ABI est un ensemble de règles que les compilateurs et les éditeurs de liens respectent afin de compiler votre programme afin que cela fonctionne correctement. Les ABI couvrent plusieurs sujets:

  • On peut dire que la partie la plus importante et la plus importante d'une ABI est la norme d'appel de procédure parfois appelée "convention d'appel". Les conventions d'appel normalisent la manière dont les "fonctions" sont traduites en code d'assemblage.
  • Les ABI dictent également la manière dont les noms des fonctions exposées dans les bibliothèques doivent être représentés afin que les autres codes puissent appeler ces bibliothèques et savoir quels arguments doivent être transmis. Cela s'appelle "nom mangling".
  • Les ABI dictent également le type de types de données pouvant être utilisés, la manière dont ils doivent être alignés et d'autres détails de bas niveau.

En examinant plus en profondeur la convention d’appel, que je considère comme le cœur d’une ABI:

La machine elle-même n'a pas de concept de "fonctions". Lorsque vous écrivez une fonction dans un langage de haut niveau tel que c, le compilateur génère une ligne de code Assembly telle que _MyFunction1:. C'est une étiquette , qui finira par être résolue en adresse par l'assembleur. Cette étiquette marque le "début" de votre "fonction" dans le code d'assemblage. Dans le code de haut niveau, lorsque vous "appelez" cette fonction, ce que vous êtes en train de faire est de forcer le processeur à sauter à l'adresse de cette étiquette et à continuer à y exécuter.

En préparation pour le saut, le compilateur doit faire un tas de choses importantes. La convention d'appel est comme une liste de contrôle suivie par le compilateur pour faire tout ce travail:

  • Tout d'abord, le compilateur insère un peu de code d'assemblage pour enregistrer l'adresse actuelle. Ainsi, lorsque votre "fonction" est terminée, la CPU peut revenir au bon endroit et continuer à exécuter.
  • Ensuite, le compilateur génère le code Assembly pour transmettre les arguments.
    • Certaines conventions d'appel dictent que les arguments doivent être placés sur la pile ( dans un ordre particulier bien sûr).
    • D'autres conventions dictent que les arguments doivent être placés dans des registres particuliers ( en fonction de leurs types de données bien sûr).
    • D'autres conventions encore dictent qu'une combinaison spécifique de pile et de registres doit être utilisée.
  • Bien sûr, s’il existait auparavant des éléments importants dans ces registres, ces valeurs sont maintenant écrasées et perdues à jamais. Par conséquent, certaines conventions d’appel peuvent exiger que le compilateur enregistre certains de ces registres avant d’y mettre les arguments.
  • Maintenant, le compilateur insère une instruction de saut indiquant à la CPU de se rendre à l’étiquette qu’elle avait faite précédemment (_MyFunction1:). À ce stade, vous pouvez considérer que la CPU est "dans" votre "fonction".
  • À la fin de la fonction, le compilateur place du code d'assemblage qui obligera la CPU à écrire la valeur de retour au bon endroit. La convention d'appel déterminera si la valeur de retour doit être placée dans un registre particulier (selon son type) ou sur la pile.
  • Il est maintenant temps de nettoyer. La convention d’appel dictera l’endroit où le compilateur place le code de l’assemblée de nettoyage.
    • Certaines conventions indiquent que l'appelant doit nettoyer la pile. Cela signifie qu'après que la "fonction" est terminée et que la CPU revienne à son état initial, le prochain code à exécuter devrait être un code de nettoyage très spécifique.
    • D’autres conventions indiquent que certaines parties du code de nettoyage doivent figurer à la fin de la "fonction" avant le retour en arrière.

Il existe de nombreuses ABI/conventions d'appel différentes. Certains principaux sont:

  • Pour les CPU x86 ou x86-64 (environnement 32 bits):
    • CDECL
    • STDCALL
    • FASTCALL
    • VECTORCALL
    • CET APPEL
  • Pour le processeur x86-64 (environnement 64 bits):
    • SYSTEMV
    • MSNATIVE
    • VECTORCALL
  • Pour le ARM CPU (32 bits)
    • AAPCS
  • Pour le ARM CPU (64 bits)
    • AAPCS64

Here est une excellente page qui montre les différences entre les assemblys générés lors de la compilation pour différentes ABI.

Une autre chose à mentionner est qu'un ABI n'est pas seulement pertinent dans le module exécutable de votre programme. est également utilisé par l'éditeur de liens pour s'assurer que la bibliothèque d'appels de programmes fonctionne correctement. Vous avez plusieurs bibliothèques partagées en cours d'exécution sur votre ordinateur et tant que votre compilateur sait quel ABI ils utilisent, il peut appeler des fonctions à partir de celles-ci correctement sans faire exploser la pile.

Votre compilateur qui comprend comment appeler des fonctions de bibliothèque est extrêmement extrêmement important. Sur une plate-forme hébergée (c'est-à-dire sur laquelle un système d'exploitation charge des programmes), votre programme ne peut même pas clignoter sans passer un appel du noyau.

28
Lakey

Une interface binaire d'application (ABI) est similaire à une API, mais la fonction n'est pas accessible à l'appelant au niveau du code source. Seule une représentation binaire est accessible/disponible.

Les ABI peuvent être définies au niveau de l'architecture du processeur ou du système d'exploitation. Les ABI sont des normes à suivre par la phase de génération de code du compilateur. La norme est fixée soit par le système d'exploitation, soit par le processeur.

Fonctionnalité: Définissez le mécanisme/standard pour effectuer des appels de fonction indépendamment du langage d'implémentation ou d'un compilateur/éditeur de liens/chaîne d'outils spécifique. Fournissez le mécanisme qui permet JNI, ou une interface Python-C, etc.

Entités existantes: Fonctions sous forme de code machine.

Consommateur: une autre fonction (dont une dans un autre langage, compilée par un autre compilateur ou liée par un autre éditeur de liens).

17
alvin

Fonctionnalité: ensemble de contrats qui concernent le compilateur, les rédacteurs d'Assembly, l'éditeur de liens et le système d'exploitation. Les contrats spécifient comment les fonctions sont disposées, où les paramètres sont passés, comment les paramètres sont passés, comment les retours de fonction fonctionnent. Celles-ci sont généralement spécifiques à un tuple (architecture de processeur, système d'exploitation).

Entités existantes: disposition des paramètres, sémantique des fonctions, allocation des registres. Par exemple, les architectures ARM ont de nombreuses ABI (APCS, EABI, GNU-EABI, sans parler de plusieurs cas historiques). .

Consommateur: Le compilateur, les rédacteurs d'assemblage, le système d'exploitation, l'architecture spécifique du processeur.

Qui a besoin de ces détails? Le compilateur, les rédacteurs d'Assembly, les éditeurs de liens chargés de la génération de code (ou des exigences d'alignement), du système d'exploitation (traitement des interruptions, interface syscall). Si vous faisiez de la programmation d'assemblage, vous vous conformiez à un ABI!

La gestion de noms C++ est un cas particulier - c’est un problème lié à l’éditeur de liens et à l’éditeur de liens dynamiques - si la gestion de noms n’est pas normalisée, la liaison dynamique ne fonctionnera pas. Désormais, l'ABI C++ s'appelle justement cela, l'ABI C++. Ce n'est pas un problème de niveau de l'éditeur de liens, mais un problème de génération de code. Une fois que vous avez un binaire C++, il n’est pas possible de le rendre compatible avec un autre ABI C++ (changement de nom, gestion des exceptions) sans recompiler à partir du source.

ELF est un format de fichier pour l'utilisation d'un chargeur et d'un éditeur de liens dynamique. ELF est un format de conteneur pour le code et les données binaires et, en tant que tel, spécifie l’ABI d’un morceau de code. Je ne considérerais pas ELF comme une ABI au sens strict, car les exécutables PE ne sont pas une ABI.

Toutes les ABI sont spécifiques à un jeu d'instructions. Un ABI ARM n'aura aucun sens sur un processeur MSP430 ou x86_64.

Windows possède plusieurs ABI - par exemple, fastcall et stdcall sont deux ABI à usage courant. Le syscall ABI est encore différent.

10
Yann Ramin

Laissez-moi au moins répondre à une partie de votre question. Avec un exemple de la manière dont l’ABI Linux affecte les appels système, et pourquoi c’est utile.

Un appel système est un moyen pour un programme utilisateur de demander quelque chose au kernelspace. Cela fonctionne en mettant le code numérique pour l'appel et l'argument dans un certain registre et en déclenchant une interruption. Puis un commutateur se produit dans l'espace noyau et le noyau recherche le code numérique et l'argument, gère la demande, place le résultat dans un registre et déclenche un changement dans l'espace utilisateur. Ceci est nécessaire par exemple lorsque l'application souhaite allouer de la mémoire ou ouvrir un fichier (appels système "brk" et "open").

Maintenant, les appels système ont des noms abrégés "brk", etc. et les opcodes correspondants, ceux-ci sont définis dans un fichier d'en-tête spécifique au système. Tant que ces codes d'opération restent les mêmes, vous pouvez exécuter les mêmes programmes utilisateur compilés avec différents noyaux mis à jour sans devoir recompiler. Vous avez donc une interface utilisée par les binaires précompilés, d’où ABI.

7
snies

La meilleure façon de différencier ABI et API est de savoir pourquoi et à quoi sert-il:

Pour x86-64, il y a généralement un ABI (et pour x86 32 bits, il y a un autre ensemble):

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 suivent avec quelques légères variations. Et Windows x64 a son propre ABI:

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

Connaissant l'ABI et supposant qu'un autre compilateur le suit également, les binaires savent théoriquement comment s'appeler (API des bibliothèques en particulier) et passer des paramètres sur la pile ou par des registres, etc. Ou quels registres seront modifiés lors de l'appel des fonctions, etc. Ces connaissances aideront essentiellement les logiciels à s’intégrer les uns aux autres. Connaissant l'ordre des registres/la structure de la pile, je peux facilement rassembler différents logiciels écrits dans des assemblages sans trop de problèmes.

Mais les API sont différentes:

Il s'agit d'un nom de fonction de haut niveau, avec un argument défini, tel que, si différents logiciels construisent à l'aide de cette API, PEUT être en mesure de s'appeler les uns les autres. Mais une exigence supplémentaire de SAME ABI doit être respectée.

Par exemple, Windows était auparavant conforme à l'API POSIX:

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

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

Et Linux est également compatible POSIX. Mais les fichiers binaires ne peuvent pas être simplement déplacés et exécutés immédiatement. Mais comme ils utilisaient les mêmes NOMS dans l'API compatible POSIX, vous pouvez utiliser le même logiciel en C, le recompiler dans un système d'exploitation différent et le faire fonctionner immédiatement.

Les API ont pour but de faciliter l'intégration du logiciel - phase de pré-compilation. Donc, après la compilation, le logiciel peut avoir un aspect totalement différent - si les ABI sont différentes.

Les ABI sont destinés à définir une intégration exacte du logiciel au niveau binaire/Assemblée.

4
Peter Teoh

Pour appeler du code dans des bibliothèques partagées ou un code entre des unités de compilation, le fichier objet doit contenir des étiquettes pour les appels. C++ modifie les noms des étiquettes de méthodes afin d’imposer le masquage des données et de permettre les méthodes surchargées. C'est pourquoi vous ne pouvez pas mélanger des fichiers provenant de différents compilateurs C++ à moins qu'ils ne prennent explicitement en charge le même ABI.

3
Justin Smith

Sommaire

Il existe différentes interprétations et opinions fortes de la couche exacte qui définissent une ABI (interface binaire d'application).

À mon avis, une ABI est une convention subjective de ce qui est considéré comme une plateforme/donnée pour une API spécifique. L'ABI est le "reste" de conventions qui "ne changeront pas" pour une API spécifique ou qui seront traitées par l'environnement d'exécution: exécuteurs, outils, lieurs, compilateurs, jvm et système d'exploitation.

Définition d'une interface : ABI, API

Si vous souhaitez utiliser une bibliothèque telle que joda-time, vous devez déclarer une dépendance sur joda-time-<major>.<minor>.<patch>.jar. La bibliothèque suit les meilleures pratiques et utilise Versioning Semantic . Ceci définit la compatibilité de l'API à trois niveaux:

  1. Patch - Vous n'avez pas besoin de changer votre code. La bibliothèque ne corrige que quelques bugs.
  2. Mineure - Vous n'avez pas besoin de changer votre code depuis les ajouts
  3. Major - L'interface (API) est modifiée et vous devrez peut-être modifier votre code.

Pour que vous puissiez utiliser une nouvelle version majeure de la même bibliothèque, beaucoup d'autres conventions doivent encore être respectées:

  • Le langage binaire utilisé pour les bibliothèques (dans Java, la version cible de la machine virtuelle Java qui définit le Java bytecode)
  • Conventions d'appel
  • Conventions JVM
  • Conventions de liaison
  • Conventions d'exécution Toutes celles-ci sont définies et gérées par les outils que nous utilisons.

Exemples

Etude de cas Java

Par exemple, Java a normalisé toutes ces conventions, non pas dans un outil, mais dans une spécification JVM formelle. La spécification permettait à d'autres fournisseurs de fournir un ensemble différent d'outils pouvant générer des bibliothèques compatibles.

Java fournit deux autres études de cas intéressantes pour ABI: Scala versions et Dalvik machine virtuelle.

Dalvik machine virtuelle a brisé le ABI

Dalvik VM nécessite un type de bytecode différent de celui de Java. Les bibliothèques Dalvik sont obtenues en convertissant le bytecode Java (avec la même API) pour Dalvik. De cette manière, vous pouvez obtenir deux versions de la même API: définies par le joda-time-1.7.2.jar original. Nous pourrions m'appeler joda-time-1.7.2.jar et joda-time-1.7.2-dalvik.jar. Ils utilisent un ABI différent pour le standard orienté pile Java vms: celui d’Oracle, celui d’IBM, open Java ou tout autre; et le deuxième ABI est celui autour de Dalvik.

Les sorties successives de Scala sont incompatibles

Scala n’a pas de compatibilité binaire entre les versions mineures Scala: 2.X. Pour cette raison, la même API "io.reactivex" %% "rxscala"% "0.26.5" a trois versions (plus dans le futur): pour Scala 2.10, 2.11 et 2.12. Qu'est-ce qui a changé? Je ne sais pas pour l'instant , mais les fichiers binaires ne sont pas compatibles. Les dernières versions ajoutent probablement des éléments qui rendent les bibliothèques inutilisables sur les anciennes machines virtuelles, probablement des éléments liés aux conventions de liaison/nommage/paramètres.

Les versions successives de Java sont incompatibles

Java a également des problèmes avec les versions majeures de la machine virtuelle Java: 4,5,6,7,8,9. Ils offrent uniquement une compatibilité ascendante. Jvm9 sait comment exécuter le code compilé/ciblé (option -target de javac) pour toutes les autres versions, alors que JVM 4 ne sait pas comment exécuter du code ciblé pour JVM 5. Tout cela avec une seule bibliothèque Joda. Cette incompatibilité passe sous le radar grâce à différentes solutions:

  1. Versionnage sémantique: lorsque les bibliothèques ciblent une machine virtuelle plus élevée, elles modifient généralement la version principale.
  2. Utilisez JVM 4 comme ABI et vous êtes en sécurité.
  3. Java 9 ajoute une spécification sur la manière dont vous pouvez inclure du code intermédiaire pour une machine virtuelle Java ciblée spécifique dans la même bibliothèque.

Pourquoi ai-je commencé avec la définition de l'API?

API et ABI ne sont que des conventions sur la définition de la compatibilité. Les couches inférieures sont génériques en ce qui concerne une pléthore de sémantiques de haut niveau. C'est pourquoi il est facile de faire des conventions. Le premier type de conventions concerne l’alignement de la mémoire, le codage d’octets, les conventions d’appel, les codages grand et petit endian, etc. En outre, vous obtenez les conventions exécutables comme celles décrites, les conventions de liaison, code d'octet intermédiaire comme celui utilisé par Java ou LLVM IR utilisé par GCC. Troisièmement, vous obtenez des conventions sur la recherche de bibliothèques, leur chargement (voir Java chargeurs de classes). En allant de plus en plus haut dans les concepts, vous avez de nouvelles conventions que vous considérez comme une donnée. C'est pourquoi ils ne sont pas parvenus au versioning sémantique . Ils sont implicites ou réduits dans la version majeure . Nous pourrions modifier le versioning sémantique avec <major>-<minor>-<patch>-<platform/ABI>. C'est déjà ce qui se passe déjà: la plateforme est déjà une rpm, dll, jar (bytecode JVM), war (jvm + serveur Web), apk, 2.11 (version spécifique de Scala) et ainsi de suite. Lorsque vous dites APK, vous parlez déjà d'une partie ABI spécifique de votre API.

L'API peut être porté sur différents ABI

Le niveau supérieur d'une abstraction (les sources écrites avec l'API la plus élevée peuvent être recompilées/portées vers toute autre abstraction de niveau inférieur.

Disons que j'ai quelques sources pour rxscala. Si les outils Scala sont modifiés, je peux les recompiler en conséquence. Si la machine virtuelle Java change, je pourrais avoir des conversions automatiques de l'ancienne machine vers la nouvelle sans me soucier des concepts de haut niveau. Bien que le portage puisse être difficile, cela aidera tout autre client. Si un nouveau système d'exploitation est créé à l'aide d'un code assembleur totalement différent, un traducteur peut être créé.

API portées à travers les langues

Il existe des API portées dans plusieurs langues telles que flux réactifs . En général, ils définissent des correspondances avec des langages/plates-formes spécifiques. Je dirais que l'API est la spécification principale définie formellement en langage humain ou même un langage de programmation spécifique. Tous les autres "mappages" sont en fait ABI, sinon plus d'API que les ABI habituels. La même chose se passe avec les interfaces REST.

3
raisercostin

exemple ABI exécutable minimal de la bibliothèque partagée Linux

Dans le contexte des bibliothèques partagées, l'implication la plus importante de "disposer d'une ABI stable" est qu'il n'est pas nécessaire de recompiler vos programmes après les modifications de la bibliothèque.

Donc par exemple:

  • si vous vendez une bibliothèque partagée, vous éviterez à vos utilisateurs de recompiler tout ce qui dépend de votre bibliothèque pour chaque nouvelle version.

  • si vous vendez un programme source fermé qui dépend d'une bibliothèque partagée présente dans la distribution de l'utilisateur, vous pouvez publier et tester moins de pré-logiciels si vous êtes certain que ABI est stable dans certaines versions du système d'exploitation cible.

    Ceci est particulièrement important dans le cas de la bibliothèque standard C, à laquelle de nombreux programmes de votre système sont liés.

Maintenant, je veux donner un exemple concret minimal exécutable de cela.

principal 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

Compile et fonctionne bien avec:

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

Supposons maintenant que pour la v2 de la bibliothèque, nous voulions ajouter un nouveau champ à mylib_mystruct appelé new_field.

Si nous avons ajouté le champ avant old_field comme dans:

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

et reconstruit la bibliothèque mais pas main.out, l'assertion échoue!

C'est parce que la ligne:

myobject->old_field == 1

avait généré Assembly qui essayait d'accéder à la toute première int de la structure, qui est maintenant new_field au lieu du old_field attendu.

Par conséquent, ce changement a brisé le ABI.

Toutefois, si nous ajoutons new_field après old_field:

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

alors l'ancien assemblage généré accède toujours à la première int de la structure, et le programme fonctionne toujours, car nous avons maintenu la stabilité de l'ABI.

Voici un version entièrement automatisée de cet exemple sur GitHub .

Une autre façon de garder cette ABI stable aurait été de traiter mylib_mystruct comme une structure opaque , et d'accéder uniquement à ses champs via des aides à la méthode. Cela facilite la stabilité de l’ABI, mais entraîne un surcoût en termes de performances car nous appelons davantage de fonctions.

API vs ABI

Dans l'exemple précédent, il est intéressant de noter que l'ajout de new_field avant old_field ne fait que briser l'ABI, mais pas l'API.

Cela signifie que si nous avions recompilé notre programme main.c contre la bibliothèque, il aurait fonctionné de manière indépendante.

Cependant, nous aurions également cassé l'API si nous avions changé par exemple la signature de la fonction:

mylib_mystruct* mylib_init(int old_field, int new_field);

puisque dans ce cas, main.c cesserait de compiler.

API sémantique vs API de programmation

Nous pouvons également classer les modifications de l'API dans un troisième type: les modifications sémantiques.

L'API sémantique est généralement une description en langage naturel de ce que l'API est censée faire, généralement incluse dans la documentation de l'API.

Il est donc possible de casser l'API sémantique sans casser le programme lui-même.

Par exemple, si nous avions modifié

myobject->old_field = old_field;

à:

myobject->old_field = old_field + 1;

alors cela n'aurait cassé ni l'API de programmation, ni l'ABI, mais main.c l'API sémantique se briserait.

Il existe deux manières de vérifier par programme l'API du contrat:

  • tester un tas de cas de coin. Facile à faire, mais vous risquez toujours d'en manquer un.
  • vérification formelle . Plus difficile à faire, mais produit une preuve mathématique de correction, unifiant essentiellement la documentation et les tests de manière "humaine"/machine vérifiable! Tant qu'il n'y a pas de bogue dans votre description formelle, bien sûr ;-)

    Ce concept est étroitement lié à la formalisation de la mathématique elle-même: https://math.stackexchange.com/questions/53969/what-does-formal-mean/3297537#3297537

Liste de tout ce qui casse les ABI de bibliothèques partagées C/C++

TODO: trouver/créer la liste ultime:

Exemple minimal exécutable en Java

Qu'est-ce que la compatibilité binaire en Java?

Testé sous Ubuntu 18.10, GCC 8.2.0.

J'essayais également de comprendre ABI et la réponse de JesperE était très utile.

D'un point de vue très simple, nous pouvons essayer de comprendre ABI en considérant la compatibilité binaire.

Le wiki KDE définit une bibliothèque comme compatible binaire "si un programme lié dynamiquement à une ancienne version de la bibliothèque continue de fonctionner avec les nouvelles versions de la bibliothèque sans avoir besoin de recompiler." Pour plus d'informations sur les liens dynamiques, reportez-vous à Lien statique vs. liens dynamiques

Essayons maintenant d’examiner les aspects les plus fondamentaux nécessaires à la compatibilité binaire d’une bibliothèque (en supposant qu’il n’y ait aucune modification de code source dans la bibliothèque):

  1. Architecture de jeu d'instructions compatible/identique (instructions du processeur, structure du fichier de registre, organisation de la pile, types d'accès à la mémoire, tailles, présentation et alignement des types de données de base auxquels le processeur peut accéder directement)
  2. Conventions d'appel identiques
  3. Même convention de nom (cela peut être nécessaire si un programme Fortran a besoin d'appeler une fonction de bibliothèque C++).

Bien sûr, il y a beaucoup d'autres détails, mais c'est surtout ce que couvre l'ABI.

Plus précisément, pour répondre à votre question, on peut déduire de ce qui précède:

Fonctionnalité ABI: compatibilité binaire

entités existantes: programme/bibliothèques/systèmes d'exploitation existants

consommateur: bibliothèques, OS

J'espère que cela t'aides!

1
blue_whale

Le terme ABI est utilisé pour désigner deux concepts distincts mais liés.

Quand on parle de compilateurs, il fait référence aux règles utilisées pour convertir des constructions de niveau source en constructions binaires. Quelle est la taille des types de données? comment fonctionne la pile? Comment puis-je passer des paramètres aux fonctions? quels registres doivent être sauvegardés par l'appelant par rapport à l'appelé?

Quand on parle de bibliothèques, cela fait référence à l'interface binaire présentée par une bibliothèque compilée. Cette interface résulte d'un certain nombre de facteurs, notamment le code source de la bibliothèque, les règles utilisées par le compilateur et, dans certains cas, les définitions extraites d'autres bibliothèques.

Les modifications apportées à une bibliothèque peuvent casser l'ABI sans casser l'API. Considérons par exemple une bibliothèque avec une interface comme.

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

et le programmeur d'application écrit du code comme

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

Le programmeur d'application ne se soucie pas de la taille ou de la disposition de FOO, mais le binaire de l'application se termine par une taille codée en dur de foo. Si le programmeur de la bibliothèque ajoute un champ supplémentaire à foo et que quelqu'un utilise le nouveau fichier binaire de la bibliothèque avec l'ancien fichier binaire de l'application, la bibliothèque peut effectuer des accès mémoire en dehors des limites.

OTOH si l'auteur de la bibliothèque avait conçu leur API comme.

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

et le programmeur d'application écrit du code comme

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

Ensuite, le fichier binaire de l’application n’a pas besoin de connaître la structure de FOO, tout cela pouvant être caché à l’intérieur de la bibliothèque. Le prix que vous payez pour cela est cependant que les opérations de tas sont impliquées.

1
plugwash

L'ABI doit être cohérent entre l'appelant et l'appelé pour être certain du succès de l'appel. Utilisation de pile, utilisation de registre, pop de pile de fin de routine. Ce sont toutes les parties les plus importantes de l’ABI.

1

Interface binaire d'application (ABI)

Fonctionnalité:

  • Traduction du modèle du programmeur vers le type de données de domaine du système sous-jacent, la taille, l'alignement, la convention d'appel, qui contrôle la manière dont les arguments des fonctions sont transmis et les valeurs renvoyées; les numéros d'appel système et la manière dont une application doit effectuer des appels système vers le système d'exploitation; le schéma de manglage, la propagation des exceptions et la convention d'appel entre les compilateurs sur la même plate-forme, mais ne nécessitent pas de compatibilité multiplate-forme ...

entités existantes:

  • Blocs logiques qui participent directement à l'exécution du programme: ALU, registres à usage général, registres pour mappage mémoire/E/S des E/S, etc.

consommateur:

  • Processeurs de langage linker, assembler ...

Celles-ci sont nécessaires à quiconque doit s'assurer que les chaînes d'outils de construction fonctionnent dans leur ensemble. Si vous écrivez un module en langage Assembly, un autre en Python et si, au lieu de votre propre chargeur de démarrage, vous souhaitez utiliser un système d'exploitation, vos modules "application" fonctionnent au-delà des limites "binaires" et nécessitent l'accord de cette "interface".

Nom C++ modifié, car il peut être nécessaire de lier des fichiers objets de différents langages de haut niveau à votre application. Pensez à utiliser la bibliothèque standard GCC pour passer des appels système à Windows construits avec Visual C++.

ELF est l'une des attentes possibles de l'éditeur de liens à partir d'un fichier objet pour interprétation, bien que JVM puisse avoir une autre idée.

Pour une application Windows RT Store, essayez de rechercher ARM ABI si vous souhaitez vraiment que certaines chaînes d'outils de construction fonctionnent ensemble.

1
Chawathe Vipul

En bref et en philosophie, seules les choses d'un type peuvent bien s'entendre, et l'ABI pourrait être considéré comme le type du logiciel qui fonctionne ensemble.

1
smwikipedia