Che cos'è un'applicazione binaria (ABI)?


493

Non ho mai capito chiaramente cosa sia un ABI. Per favore, non indicarmi un articolo di Wikipedia. Se potessi capirlo, non sarei qui a pubblicare un post così lungo.

Questa è la mia mentalità su diverse interfacce:

Un telecomando TV è un'interfaccia tra l'utente e la TV. È un'entità esistente, ma inutile (non fornisce alcuna funzionalità) da sola. Tutte le funzionalità per ciascuno di quei pulsanti sul telecomando sono implementate nel televisore.

Interfaccia: è un livello di "entità esistente" tra la functionalitye consumerquella funzionalità. Un'interfaccia da sola non fa nulla. Invoca semplicemente la funzionalità che sta dietro.

Ora, a seconda di chi è l'utente, esistono diversi tipi di interfacce.

I comandi CLI (Command Line Interface) sono le entità esistenti, l'utente è l'utente e la funzionalità sta dietro.

functionality: la mia funzionalità software che risolve alcuni scopi ai quali stiamo descrivendo questa interfaccia.

existing entities: comandi

consumer: utente

La finestra, i pulsanti, ecc. Della Graphical User Interface (GUI) sono le entità esistenti, e di nuovo il consumatore è l'utente e la funzionalità sta dietro.

functionality: la mia funzionalità software che risolve alcuni problemi ai quali stiamo descrivendo questa interfaccia.

existing entities: finestra, pulsanti ecc.

consumer: utente

Le funzioni API (Application Programming Interface) (o per essere più corrette) (nella programmazione basata su interfacce) sono le entità esistenti, il consumatore qui è un altro programma non un utente, e di nuovo la funzionalità sta dietro questo livello.

functionality: la mia funzionalità software che risolve alcuni problemi ai quali stiamo descrivendo questa interfaccia.

existing entities: funzioni, interfacce (matrice di funzioni).

consumer: un altro programma / applicazione.

Application Binary Interface (ABI) Qui è dove inizia il mio problema.

functionality: ???

existing entities: ???

consumer: ???

  • Ho scritto software in diverse lingue e fornito diversi tipi di interfacce (CLI, GUI e API), ma non sono sicuro di aver mai fornito alcuna ABI.

Wikipedia dice:

ABI coprono dettagli come

  • tipo di dati, dimensioni e allineamento;
  • la convenzione di chiamata, che controlla come vengono passati gli argomenti delle funzioni e restituiti i valori;
  • i numeri di chiamata del sistema e come un'applicazione deve effettuare chiamate di sistema al sistema operativo;

Altri ABI standardizzano dettagli come

  • la menzione del nome C ++,
  • propagazione delle eccezioni e
  • convenzione di chiamata tra compilatori sulla stessa piattaforma, ma non richiede compatibilità multipiattaforma.
  • Chi ha bisogno di questi dettagli? Per favore, non dire il sistema operativo. Conosco la programmazione degli assiemi. So come funzionano i collegamenti e il caricamento. So esattamente cosa succede dentro.

  • Perché è arrivata la modifica del nome C ++? Pensavo che stessimo parlando a livello binario. Perché arrivano le lingue?

Ad ogni modo, ho scaricato [PDF] System V Application Binary Interface Edition 4.1 (1997-03-18) per vedere cosa contiene esattamente. Bene, la maggior parte non aveva alcun senso.

  • Perché contiene due capitoli (4 ° e 5 °) per descrivere il formato di file ELF ? In effetti, questi sono gli unici due capitoli significativi di tale specifica. Il resto dei capitoli sono "specifici del processore". Comunque, ho pensato che fosse un argomento completamente diverso. Si prega di non dire che le specifiche del formato di file ELF sono l'ABI. Non si qualifica come interfaccia secondo la definizione.

  • Lo so, poiché stiamo parlando a un livello così basso, deve essere molto specifico. Ma non sono sicuro di come sia specifico "istruzione set architecture (ISA)"?

  • Dove posso trovare l'ABI di Microsoft Windows?

Quindi, queste sono le principali domande che mi infastidiscono.


7
"Per favore non dire, OS" I compilatori devono conoscere l'ABI. I linker devono conoscere l'ABI. Il kernel deve conoscere l'ABI per impostare il programma nella RAM affinché funzioni correttamente. Per quanto riguarda il C ++, vedi sotto, trasforma intenzionalmente le etichette in incomprensibili a causa del sovraccarico e dei metodi privati, e il linker e qualsiasi altro compilatore devono avere un nome compatibile per lavorare con esso, in altre parole lo stesso ABI.
Justin Smith,

8
Penso che la domanda sia così chiara; descrivere esattamente qual è il formato di risposta previsto e tuttavia non una singola risposta soddisfacente a ciò che può essere accettato.
legends2k,

3
@ legends2k La mia opinione sul problema è che OP sa davvero cos'è un ABI, ma non se ne rende conto. La stragrande maggioranza dei programmatori non progetterà o fornirà mai un ABI, perché questo è il compito dei progettisti di sistemi operativi / piattaforme.
JesperE,

4
@JesperE: sono d'accordo con il tuo punto. Ma probabilmente l'OP vuole conoscerlo chiaramente, nel formato che ritiene opportuno, anche se potrebbe non essere necessario fornire un ABI.
legends2k,

2
Ero ignorante. Di recente, lavorando con tutte queste cose. Ho capito che cos'è realmente l'ABI. Sì, sono d'accordo sul fatto che il mio modello sia difettoso. Non è appropriato inserire l'ABI nel mio modello. Grazie @ JasperE. È bastata l'esperienza lavorativa per realizzare la tua risposta.
artigli

Risposte:


536

Un modo semplice per capire "ABI" è confrontarlo con "API".

Hai già familiarità con il concetto di API. Se vuoi usare le funzionalità, per esempio, di alcune librerie o del tuo sistema operativo, programmerai contro un'API. L'API è composta da tipi / strutture di dati, costanti, funzioni, ecc. Che è possibile utilizzare nel codice per accedere alla funzionalità di quel componente esterno.

Un ABI è molto simile. Pensala come la versione compilata di un'API (o come un'API a livello di linguaggio macchina). Quando scrivi il codice sorgente, accedi alla libreria tramite un'API. Una volta compilato il codice, l'applicazione accede ai dati binari nella libreria tramite l'ABI. L'ABI definisce le strutture e i metodi che l'applicazione compilata utilizzerà per accedere alla libreria esterna (proprio come l'API), solo a un livello inferiore. L'API definisce l'ordine in cui si passano gli argomenti a una funzione. La tua ABI definisce la meccanica di comequesti argomenti vengono passati (registri, stack, ecc.). La tua API definisce quali funzioni fanno parte della tua libreria. L'ABI definisce la modalità di memorizzazione del codice all'interno del file della libreria, in modo che qualsiasi programma che utilizza la libreria possa individuare la funzione desiderata ed eseguirla.

Le ABI sono importanti quando si tratta di applicazioni che usano librerie esterne. Le librerie sono piene di codice e altre risorse, ma il tuo programma deve sapere come individuare ciò di cui ha bisogno all'interno del file della libreria. L'ABI definisce il modo in cui i contenuti di una libreria sono archiviati all'interno del file e il programma utilizza l'ABI per cercare nel file e trovare ciò di cui ha bisogno. Se tutto nel tuo sistema è conforme alla stessa ABI, qualsiasi programma è in grado di funzionare con qualsiasi file di libreria, indipendentemente da chi li ha creati. Linux e Windows usano ABI diversi, quindi un programma Windows non saprà come accedere a una libreria compilata per Linux.

A volte, le modifiche ABI sono inevitabili. In questo caso, tutti i programmi che utilizzano quella libreria non funzioneranno a meno che non vengano ricompilati per utilizzare la nuova versione della libreria. Se l'ABI cambia, ma l'API no, le versioni vecchie e nuove della libreria vengono talvolta chiamate "compatibili con l'origine". Ciò implica che mentre un programma compilato per una versione di libreria non funzionerà con l'altro, il codice sorgente scritto per uno funzionerà per l'altro se ricompilato.

Per questo motivo, gli sviluppatori tendono a cercare di mantenere stabile la loro ABI (per ridurre al minimo le interruzioni). Mantenere stabile un ABI significa non modificare le interfacce di funzione (tipo e numero di ritorno, tipi e ordine degli argomenti), definizioni di tipi di dati o strutture di dati, costanti definite, ecc. È possibile aggiungere nuove funzioni e tipi di dati, ma quelli esistenti devono rimanere lo stesso. Se, ad esempio, la tua libreria utilizza numeri interi a 32 bit per indicare l'offset di una funzione e passi a numeri interi a 64 bit, allora il codice già compilato che utilizza quella libreria non accederà correttamente a quel campo (o qualsiasi altro che lo segua) . L'accesso ai membri della struttura dati viene convertito in indirizzi di memoria e offset durante la compilazione e se la struttura dei dati cambia,

Un'ABI non è necessariamente qualcosa che fornirai esplicitamente a meno che tu non stia facendo un lavoro di progettazione di sistemi di livello molto basso. Non è nemmeno specifico della lingua, poiché (ad esempio) un'applicazione C e un'applicazione Pascal possono utilizzare la stessa ABI dopo essere state compilate.

Modificare:Per quanto riguarda la tua domanda sui capitoli relativi al formato di file ELF nei documenti SysV ABI: il motivo per cui queste informazioni sono incluse è perché il formato ELF definisce l'interfaccia tra il sistema operativo e l'applicazione. Quando si dice al sistema operativo di eseguire un programma, si aspetta che il programma venga formattato in un certo modo e (per esempio) si aspetta che la prima sezione del binario sia un'intestazione ELF contenente determinate informazioni in specifici offset di memoria. Ecco come l'applicazione comunica informazioni importanti su se stessa al sistema operativo. Se si crea un programma in un formato binario non ELF (come a.out o PE), un sistema operativo che prevede applicazioni in formato ELF non sarà in grado di interpretare il file binario o eseguire l'applicazione.

IIRC, Windows attualmente utilizza il formato Portable Executable (o, PE). Ci sono collegamenti nella sezione "collegamenti esterni" di quella pagina di Wikipedia con maggiori informazioni sul formato PE.

Inoltre, per quanto riguarda la nota sulla modifica del nome C ++: quando si individua una funzione in un file di libreria, la funzione viene in genere cercata per nome. C ++ consente di sovraccaricare i nomi delle funzioni, quindi il nome da solo non è sufficiente per identificare una funzione. I compilatori C ++ hanno i loro modi di gestirli internamente, chiamati nome mangling . Un'ABI può definire un modo standard per codificare il nome di una funzione in modo che i programmi creati con una lingua o un compilatore diversi possano individuare ciò di cui hanno bisogno. Quando si utilizza extern "c"in un programma C ++, si sta istruendo il compilatore a utilizzare un modo standardizzato di registrare nomi comprensibili da altri software.


2
@bta, grazie per l'ottima risposta. La convenzione di chiamata è una specie di ABI? Grazie
camino

37
Bella risposta. Tranne questo non è ciò che è un ABI. Un ABI è un insieme di regole che determina la convenzione di chiamata e le regole per la struttura delle strutture. Pascal passa gli argomenti sullo stack in ordine inverso dalle applicazioni C, quindi i compilatori Pascal e C NON vengono compilati nella stessa ABI. I rispettivi standard per i compilatori C e Pascal assicurano implicitamente che ciò accada. I compilatori C ++ non possono definire un modo "standard" per manipolare i nomi, poiché non esiste un modo standard. Le convenzioni di modifica del nome C ++ non erano compatibili tra i compilatori C ++ quando c'erano compilatori C ++ concorrenti su Windows.
Robin Davies,


1
@RobinDavies: sulle piattaforme in cui i compilatori Pascal avrebbero chiamato funzioni pop argomenti forniti dai loro chiamanti, i compilatori C in genere definiscono i mezzi con cui un programmatore potrebbe indicare che particolari funzioni dovrebbero usare, o ci si dovrebbe aspettare che usino, le stesse convenzioni di chiamata del Compilatori Pascal anche se generalmente i compilatori C userebbero di default una convenzione in cui le funzioni chiamate lasciano nello stack tutto ciò che viene posto dai loro chiamanti.
supercat

Posso dire che i file obj generati dal compilatore C contengono ABI?
Mitu Raj,

144

Se conosci l'assemblaggio e come funzionano le cose a livello di sistema operativo, ti stai conformando a un determinato ABI. L'ABI governa cose come il modo in cui vengono passati i parametri, dove vengono inseriti i valori di ritorno. Per molte piattaforme c'è solo un ABI tra cui scegliere, e in quei casi l'ABI è semplicemente "come funzionano le cose".

Tuttavia, l'ABI governa anche cose come il modo in cui le classi / oggetti sono disposti in C ++. Ciò è necessario se si desidera essere in grado di passare i riferimenti agli oggetti attraverso i confini del modulo o se si desidera mescolare il codice compilato con compilatori diversi.

Inoltre, se si dispone di un sistema operativo a 64 bit in grado di eseguire binari a 32 bit, si avranno ABI diversi per il codice a 32 e 64 bit.

In generale, qualsiasi codice collegato nello stesso eseguibile deve essere conforme allo stesso ABI. Se si desidera comunicare tra codice utilizzando diversi ABI, è necessario utilizzare una qualche forma di RPC o protocolli di serializzazione.

Penso che tu stia provando troppo a comprimere diversi tipi di interfacce in un set fisso di caratteristiche. Ad esempio, un'interfaccia non deve necessariamente essere suddivisa in consumatori e produttori. Un'interfaccia è solo una convenzione in base alla quale due entità interagiscono.

Le ABI possono essere (parzialmente) indipendenti dall'ISA. Alcuni aspetti (come le convenzioni di chiamata) dipendono dall'ISA, mentre altri aspetti (come il layout di classe C ++) no.

Un ABI ben definito è molto importante per le persone che scrivono compilatori. Senza un ABI ben definito, sarebbe impossibile generare codice interoperabile.

EDIT: alcune note per chiarire:

  • "Binario" in ABI non esclude l'uso di stringhe o testo. Se si desidera collegare una DLL che esporta una classe C ++, da qualche parte in essa devono essere codificati i metodi e le firme dei tipi. È qui che entra in gioco il nome C ++.
  • Il motivo per cui non hai mai fornito un ABI è che la stragrande maggioranza dei programmatori non lo farà mai. Le ABI sono fornite dalle stesse persone che progettano la piattaforma (ovvero il sistema operativo) e pochissimi programmatori avranno mai il privilegio di progettare una ABI ampiamente utilizzata.

Non sono affatto convinto che il mio modello sia difettoso. Perché ogni dove questo modello per l'interfaccia è vero. Quindi, sì, voglio che mi aspetto che anche ABI si adatti a questo modello, ma non è così. La cosa IMPORTANTE è che ancora non capisco. Non so se sono così stupido o qualcos'altro ma non mi viene in mente. Non riesco a realizzare le risposte e l'articolo wiki.
artigli

2
@jesperE, "L'ABI governa cose come il modo in cui vengono passati i parametri, dove vengono posizionati i valori di ritorno." si riferisce a "cdecl, stdcall, fastcall, pascal" giusto?
camino

3
Sì. Il nome proprio è "convenzione di chiamata", che fa parte dell'ABI. en.wikipedia.org/wiki/X86_calling_conventions
JesperE

4
questa è la risposta corretta e precisa senza la verbosità (piuttosto il rumore )!
Nawaz,

Consiglio di scrivere un po 'di montaggio. Ciò aiuterà le persone a comprendere l'ABI in un modo più tangibile.
KunYu Tsai

40

In realtà non hai assolutamente bisogno di un ABI se--

  • Il tuo programma non ha funzioni e--
  • Il tuo programma è un singolo eseguibile che funziona da solo (cioè un sistema incorporato) in cui è letteralmente l'unica cosa in esecuzione e non ha bisogno di parlare con nient'altro.

Un riassunto semplificato:

API: "Ecco tutte le funzioni che puoi chiamare."

ABI: "Ecco come chiamare una funzione".

L'ABI è un insieme di regole che i compilatori e i linker aderiscono al fine di compilare il programma in modo che funzioni correttamente. ABI coprono più argomenti:

  • Probabilmente la parte più grande e più importante di un ABI è lo standard di chiamata di procedura a volte noto come "convenzione di chiamata". Le convenzioni di chiamata standardizzano il modo in cui le "funzioni" vengono tradotte in codice assembly.
  • Le ABI dettano anche come nomi delle funzioni esposte nelle librerie devono essere rappresentati in modo che altri codici possano chiamare quelle librerie e sapere quali argomenti devono essere passati. Questo si chiama "nome mangling".
  • Le ABI determinano anche quale tipo di tipi di dati possono essere utilizzati, come devono essere allineati e altri dettagli di basso livello.

Dando uno sguardo più approfondito alla convention di chiamate, che considero il nucleo di un ABI:

La macchina stessa non ha il concetto di "funzioni". Quando si scrive una funzione in un linguaggio di alto livello come c, il compilatore genera una riga di codice assembly come _MyFunction1:. Questa è un'etichetta , che alla fine verrà risolta in un indirizzo dall'assemblatore. Questa etichetta segna "inizio" della "funzione" nel codice assembly. Nel codice di alto livello, quando "chiami" quella funzione, ciò che stai realmente facendo è far saltare la CPU all'indirizzo dell'etichetta e continuare a eseguirla.

In preparazione al salto, il compilatore deve fare un sacco di cose importanti. La convenzione di chiamata è come una lista di controllo che il compilatore segue per fare tutto questo:

  • Innanzitutto, il compilatore inserisce un po 'di codice assembly per salvare l'indirizzo corrente, in modo che quando la "funzione" è terminata, la CPU può tornare al posto giusto e continuare l'esecuzione.
  • Successivamente, il compilatore genera il codice assembly per passare gli argomenti.
    • Alcune convenzioni di chiamata stabiliscono che gli argomenti dovrebbero essere messi in pila ( in un ordine particolare, ovviamente).
    • Altre convenzioni impongono che gli argomenti debbano essere inseriti in registri particolari (a seconda del tipo di dati ovviamente).
    • Ancora altre convenzioni impongono che si debba usare una combinazione specifica di stack e registri.
  • Naturalmente, se prima c'era qualcosa di importante in quei registri, quei valori ora vengono sovrascritti e persi per sempre, quindi alcune convenzioni di chiamata potrebbero imporre che il compilatore debba salvare alcuni di quei registri prima di inserire gli argomenti in essi.
  • Ora il compilatore inserisce un'istruzione di salto che dice alla CPU di andare a quell'etichetta che ha fatto in precedenza ( _MyFunction1:). A questo punto, puoi considerare la CPU "nella" tua "funzione".
  • Alla fine della funzione, il compilatore inserisce del codice assembly che farà scrivere alla CPU il valore restituito nella posizione corretta. La convenzione di chiamata stabilirà se il valore restituito deve essere inserito in un registro particolare (a seconda del tipo) o nello stack.
  • Ora è tempo di ripulire. La convenzione di chiamata determinerà dove il compilatore inserisce il codice assembly di cleanup.
    • Alcune convenzioni affermano che il chiamante deve ripulire lo stack. Ciò significa che dopo che la "funzione" è stata eseguita e la CPU torna al punto precedente, il codice immediatamente successivo da eseguire dovrebbe essere un codice di pulizia molto specifico.
    • Altre convenzioni affermano che alcune parti particolari del codice di pulizia dovrebbero trovarsi alla fine della "funzione" prima del salto indietro.

Esistono diverse ABI / convenzioni di chiamata. Alcuni dei principali sono:

  • Per la CPU x86 o x86-64 (ambiente a 32 bit):
    • CDECL
    • STDCALL
    • fastcall
    • VECTORCALL
    • thiscall
  • Per la CPU x86-64 (ambiente a 64 bit):
    • SystemV
    • MSNATIVE
    • VECTORCALL
  • Per la CPU ARM (32 bit)
    • AAPCS
  • Per la CPU ARM (64 bit)
    • AAPCS64

Qui una grande pagina che mostra effettivamente le differenze nell'assieme generato durante la compilazione per diversi ABI.

Un'altra cosa da menzionare è che un ABI non è rilevante solo all'interno del modulo eseguibile del programma. Viene anche utilizzato dal linker per assicurarsi che il programma chiami correttamente le funzioni della libreria. Hai più librerie condivise in esecuzione sul tuo computer e fintanto che il tuo compilatore sa quale ABI usano ciascuna, può richiamare correttamente le funzioni da esse senza far esplodere lo stack.

Il compilatore che comprende come chiamare le funzioni di libreria è estremamente importante. Su una piattaforma ospitata (ovvero quella in cui un sistema operativo carica programmi), il programma non può nemmeno lampeggiare senza effettuare una chiamata del kernel.


19

Un'interfaccia binaria dell'applicazione (ABI) è simile a un'API, ma la funzione non è accessibile al chiamante a livello di codice sorgente. Solo una rappresentazione binaria è accessibile / disponibile.

Le ABI possono essere definite a livello di architettura del processore o a livello di sistema operativo. Gli ABI sono standard che devono essere seguiti dalla fase di generazione del codice del compilatore. Lo standard è fissato dal sistema operativo o dal processore.

Funzionalità: definire il meccanismo / standard per rendere le chiamate di funzione indipendenti dal linguaggio di implementazione o da un compilatore / linker / toolchain specifico. Fornire il meccanismo che consente a JNI, o un'interfaccia Python-C, ecc.

Entità esistenti: funzioni in forma di codice macchina.

Consumatore: un'altra funzione (inclusa una in un'altra lingua, compilata da un altro compilatore o collegata da un altro linker).


Perché l'ABI sarebbe definito dall'architettura? Perché sistemi operativi diversi sulla stessa architettura non sarebbero in grado di definire ABI diversi?
Andreas Haferburg,

10

Funzionalità: un insieme di contratti che riguardano il compilatore, i writer di assembly, il linker e il sistema operativo. I contratti specificano come sono disposte le funzioni, dove vengono passati i parametri, come vengono passati i parametri, come funzionano i ritorni delle funzioni. Questi sono generalmente specifici di una tupla (architettura del processore, sistema operativo).

Entità esistenti: disposizione dei parametri, semantica delle funzioni, allocazione dei registri. Ad esempio, le architetture ARM hanno numerosi ABI (APCS, EABI, GNU-EABI, non importa un mucchio di casi storici) - l'uso di un ABI misto farà sì che il tuo codice semplicemente non funzioni quando si chiama oltre i confini.

Consumatore: compilatore, scrittori di assembly, sistema operativo, architettura specifica della CPU.

Chi ha bisogno di questi dettagli? Il compilatore, i writer di assembly, i linker che generano codice (o requisiti di allineamento), il sistema operativo (gestione degli interrupt, interfaccia syscall). Se hai programmato l'assemblaggio, ti stavi conformando a un ABI!

La manipolazione dei nomi C ++ è un caso speciale - è un problema centrato sul linker e sul linker dinamico - se la modifica del nome non è standardizzata, il collegamento dinamico non funzionerà. D'ora in poi, l'ABI C ++ si chiama proprio così, l'ABI C ++. Non è un problema a livello di linker, ma piuttosto un problema di generazione del codice. Una volta che hai un binario C ++, non è possibile renderlo compatibile con un'altra ABI C ++ (modifica del nome, gestione delle eccezioni) senza ricompilare dal sorgente.

ELF è un formato di file per l'uso di un caricatore e di un linker dinamico. ELF è un formato contenitore per codice binario e dati e come tale specifica l'ABI di un pezzo di codice. Non considererei ELF un ABI in senso stretto, poiché gli eseguibili PE non sono un ABI.

Tutte le ABI sono specifiche del set di istruzioni. Un ABI ARM non avrà senso su un processore MSP430 o x86_64.

Windows ha diversi ABI, ad esempio fastcall e stdcall sono due ABI di uso comune. Il syscall ABI è di nuovo diverso.


9

Lasciami almeno rispondere a una parte della tua domanda. Con un esempio di come l'ABI Linux influenza le chiamate di sistema e perché ciò è utile.

Una chiamata di sistema è un modo per un programma di spazio utente di chiedere qualcosa al kernelspace. Funziona inserendo il codice numerico per la chiamata e l'argomento in un determinato registro e attivando un interrupt. Di conseguenza si verifica un passaggio a kernelspace e il kernel cerca il codice numerico e l'argomento, gestisce la richiesta, rimette il risultato in un registro e innesca un passaggio allo spazio utente. Ciò è necessario ad esempio quando l'applicazione desidera allocare memoria o aprire un file (syscalls "brk" e "open").

Ora i syscalls hanno nomi brevi "brk", ecc. E codici operativi corrispondenti, questi sono definiti in un file di intestazione specifico del sistema. Finché questi codici operativi rimangono invariati, puoi eseguire gli stessi programmi compilati per l'utente con kernel aggiornati diversi senza dover ricompilare. Quindi hai un'interfaccia utilizzata dai binari precompilati, quindi ABI.


4

Per chiamare il codice nelle librerie condivise o il codice di chiamata tra le unità di compilazione, il file oggetto deve contenere etichette per le chiamate. Il C ++ modifica i nomi delle etichette dei metodi per imporre il nascondimento dei dati e consentire metodi sovraccarichi. Ecco perché non è possibile mescolare file da compilatori C ++ diversi a meno che non supportino esplicitamente la stessa ABI.


4

Il modo migliore per distinguere tra ABI e API è sapere perché e a cosa serve:

Per x86-64 esiste generalmente un ABI (e per x86 a 32 bit ne esiste un altro set):

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 lo seguono con alcune lievi variazioni. E Windows x64 ha il suo ABI:

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

Conoscendo l'ABI e presupponendo che anche un altro compilatore lo segua, i binari in teoria sanno come chiamarsi l'un l'altro (in particolare l'API delle librerie) e passare parametri sullo stack o tramite registri ecc. O quali registri verranno modificati quando si chiamano le funzioni ecc. Fondamentalmente queste conoscenze aiuteranno il software a integrarsi tra loro. Conoscendo l'ordine dei registri / layout dello stack posso facilmente mettere insieme diversi software scritti in assiemi senza troppi problemi.

Ma le API sono diverse:

È un nome di funzioni di alto livello, con argomento definito, in modo tale che se diversi pezzi di software si costruiscono usando queste API, POSSONO essere in grado di chiamarsi l'un l'altro. Ma è necessario rispettare un requisito aggiuntivo di SAME ABI.

Ad esempio, Windows era conforme alle API POSIX:

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

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

E Linux è anche conforme a POSIX. Ma i binari non possono essere semplicemente spostati ed eseguiti immediatamente. Ma poiché hanno usato gli stessi NOMI nell'API conforme a POSIX, puoi prendere lo stesso software in C, ricompilarlo nei diversi sistemi operativi e farlo subito funzionare.

Le API hanno lo scopo di facilitare l'integrazione del software - fase di pre-compilazione. Quindi, dopo la compilazione, il software può apparire completamente diverso, se l'ABI è diverso.

L'ABI intende definire l'esatta integrazione del software a livello binario / di assemblaggio.


La convenzione di chiamata x86-64 di Windows non utilizza la convenzione di chiamata SysV utilizzata da tutti gli altri sistemi operativi x86-64. Linux / OS X / FreeBSD condividono tutti la stessa convenzione di chiamata, ma non condividono l'ABI completo. L'ABI di un sistema operativo include numeri di chiamata di sistema. es. freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/… dice che SYS_execveè 11 su Linux a 32 bit, ma 59 su FreeBSD.
Peter Cordes,

grazie per il tuo commento, ho modificato il mio commento per rispondere meglio alla differenza tra ABI e API.
Peter Teoh,

Ti manca ancora la differenza tra una convenzione di chiamata e un ABI completo (chiamate di sistema e tutto il resto). Puoi eseguire alcuni binari di FreeBSD su Linux, perché Linux (il kernel) fornisce un livello di compatibilità di FreeBSD. Anche allora, questo è limitato ai binari che non tentano di usare alcuna parte dell'ABI di FreeBSD che Linux non fornisce. (es. qualsiasi chiamata di sistema solo su FreeBSD). Compatibile ABI significa che puoi eseguire lo stesso binario su entrambi i sistemi, non solo che si comporterebbero in modo simile.
Peter Cordes,

"Livello di compatibilità di FreeBSD", non ne ho mai sentito parlare. Puoi indicare il codice sorgente del kernel Linux pertinente? Ma esiste il contrario: freebsd.org/doc/en_US.ISO8859-1/books/handbook/linuxemu.html .
Peter Teoh,

Non è qualcosa che uso. Ho pensato una cosa del genere esiste, ma forse non è così più. tldp.org/HOWTO/Linux+FreeBSD-6.html afferma che non è stato mantenuto e che il howto è del 2000. xD. unix.stackexchange.com/questions/172038/… conferma che è stato abbandonato e mai rifatto (dal momento che nessuno lo desiderava abbastanza per farlo). personality(2)può impostare PER_BSD. Penso di ricordare di aver visto sempre personality(PER_LINUX)in straceoutput, ma i moderni binari Linux a 64 bit non lo fanno più.
Peter Cordes,

4

Esempio ABI minimo eseguibile della libreria condivisa Linux

Nel contesto delle librerie condivise, l'implicazione più importante di "avere un'ABI stabile" è che non è necessario ricompilare i programmi dopo che la libreria è cambiata.

Quindi per esempio:

  • se vendi una libreria condivisa, risparmi ai tuoi utenti il ​​fastidio di ricompilare tutto ciò che dipende dalla tua libreria per ogni nuova versione

  • se vendi un programma a codice chiuso che dipende da una libreria condivisa presente nella distribuzione dell'utente, potresti rilasciare e testare meno prebuilt se sei sicuro che l'ABI sia stabile su alcune versioni del sistema operativo di destinazione.

    Ciò è particolarmente importante nel caso della libreria standard C, a cui sono collegati molti molti programmi nel sistema.

Ora voglio fornire un esempio minimo di ciò che è possibile eseguire.

main.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

Compila e funziona bene con:

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

Supponiamo ora che per v2 della libreria, vogliamo aggiungere un nuovo campo a mylib_mystruct chiamato new_field.

Se abbiamo aggiunto il campo prima old_fieldcome in:

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

e ricostruito la biblioteca ma non main.out , quindi l'asserzione fallisce!

Questo perché la linea:

myobject->old_field == 1

aveva generato un assembly che sta provando ad accedere al primo int della struttura, che ora è new_fieldinvece previsto old_field.

Pertanto questo cambiamento ha rotto l'ABI.

Se, tuttavia, aggiungiamo new_fielddopo old_field:

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

quindi il vecchio assembly generato accede ancora al primo int della struttura e il programma funziona ancora, poiché abbiamo mantenuto l'ABI stabile.

Ecco un versione completamente automatizzata di questo esempio su GitHub .

Un altro modo per mantenere stabile questo ABI sarebbe stato quello di trattare mylib_mystructcome una struttura opaca , e accedere ai suoi campi solo attraverso i metodi di supporto. Ciò rende più semplice mantenere stabile l'ABI, ma comporterebbe un sovraccarico prestazionale poiché faremmo più chiamate di funzione.

API vs ABI

Nell'esempio precedente, è interessante notare che l'aggiunta di new_fieldprimaold_field solo rotto l'ABI, ma non l'API.

Ciò significa che se avessimo ricompilato il nostro main.c programma con la libreria, avrebbe funzionato a prescindere.

Avremmo anche rotto l'API se avessimo cambiato ad esempio la firma della funzione:

mylib_mystruct* mylib_init(int old_field, int new_field);

poiché in quel caso, main.csmetterebbe di compilare del tutto.

API semantica vs API di programmazione

Possiamo anche classificare le modifiche API in un terzo tipo: modifiche semantiche.

L'API semantica, di solito è una descrizione in linguaggio naturale di ciò che l'API dovrebbe fare, di solito inclusa nella documentazione dell'API.

È quindi possibile interrompere l'API semantica senza interrompere la creazione del programma stesso.

Ad esempio, se avessimo modificato

myobject->old_field = old_field;

per:

myobject->old_field = old_field + 1;

allora questo non avrebbe rotto né l'API di programmazione, né l'ABI, ma main.cl'API semantica si sarebbe rotta.

Esistono due modi per controllare a livello di programmazione l'API del contratto:

  • prova un mucchio di valigie angolari. Facile da fare, ma potresti sempre perderne uno.
  • verifica formale . Più difficile da fare, ma produce prove matematiche di correttezza, essenzialmente unificando la documentazione e i test in modo "umano" / verificabile dalla macchina! Fintanto che non c'è un bug nella tua descrizione formale ovviamente ;-)

    Questo concetto è strettamente correlato alla formalizzazione della matematica stessa: /math/53969/what-does-formal-mean/3297537#3297537

Elenco di tutto ciò che interrompe gli ABI della libreria condivisa C / C ++

TODO: trova / crea l'elenco definitivo:

Esempio eseguibile minimo Java

Che cos'è la compatibilità binaria in Java?

Testato in Ubuntu 18.10, GCC 8.2.0.


3

L'ABI deve essere coerente tra chiamante e chiamante per essere sicuro che la chiamata abbia esito positivo. Uso dello stack, uso del registro, pop dello stack di fine routine. Tutte queste sono le parti più importanti dell'ABI.


3

Sommario

Esistono varie interpretazioni e opinioni forti sul livello esatto che definiscono un ABI (interfaccia binaria dell'applicazione).

A mio avviso, un ABI è una convenzione soggettiva di ciò che è considerato una determinata piattaforma / per una specifica API. L'ABI è il "resto" delle convenzioni che "non cambieranno" per una specifica API o che saranno affrontate dall'ambiente di runtime: esecutori, strumenti, linker, compilatori, jvm e OS.

Definizione di un interfaccia : ABI, API

Se si desidera utilizzare una libreria come joda-time, è necessario dichiarare una dipendenza joda-time-<major>.<minor>.<patch>.jar. La libreria segue le migliori pratiche e utilizza il Semantic Versioning . Questo definisce la compatibilità API a tre livelli:

  1. Patch: non è necessario modificare affatto il codice. La libreria risolve solo alcuni bug.
  2. Minore: non è necessario modificare il codice dopo le aggiunte
  3. Maggiore: l'interfaccia (API) è stata modificata e potrebbe essere necessario modificare il codice.

Per poter utilizzare una nuova versione principale della stessa libreria, molte altre convenzioni devono ancora essere rispettate:

  • Il linguaggio binario utilizzato per le librerie (in casi Java la versione di destinazione JVM che definisce il bytecode Java)
  • Convenzioni di chiamata
  • Convenzioni JVM
  • Convenzioni di collegamento
  • Convenzioni di runtime Tutte queste sono definite e gestite dagli strumenti che utilizziamo.

Esempi

Caso di studio Java

Ad esempio, Java ha standardizzato tutte queste convenzioni, non in uno strumento, ma in una specifica JVM formale. La specifica ha permesso ad altri fornitori di fornire un diverso set di strumenti in grado di produrre librerie compatibili.

Java fornisce altri due casi studio interessanti per ABI: le versioni Scala e la macchina virtuale Dalvik .

La macchina virtuale Dalvik ha rotto l'ABI

La VM Dalvik necessita di un diverso tipo di bytecode rispetto al bytecode Java. Le librerie Dalvik sono ottenute convertendo il bytecode Java (con la stessa API) per Dalvik. In questo modo è possibile ottenere due versioni della stessa API: definita dall'originale joda-time-1.7.2.jar. Potremmo chiamarmi joda-time-1.7.2.jare joda-time-1.7.2-dalvik.jar. Usano un ABI diverso sia per lo standard vms Java orientato allo stack: quello Oracle, quello IBM, Java aperto o qualsiasi altro; e il secondo ABI è quello attorno a Dalvik.

Le versioni successive di Scala sono incompatibili

Scala non ha compatibilità binaria tra le versioni minori di Scala: 2.X. Per questo motivo la stessa API "io.reactivex" %% "rxscala"% "0.26.5" ha tre versioni (in futuro di più): per Scala 2.10, 2.11 e 2.12. Cosa è cambiato? Per ora non lo so , ma i binari non sono compatibili. Probabilmente le ultime versioni aggiungono cose che rendono inutilizzabili le librerie sulle vecchie macchine virtuali, probabilmente cose legate a convenzioni di collegamento / denominazione / parametro.

Le versioni successive di Java sono incompatibili

Java ha anche problemi con le principali versioni di JVM: 4,5,6,7,8,9. Offrono solo compatibilità con le versioni precedenti. Jvm9 sa come eseguire il codice compilato / mirato ( -targetopzione di javac ) per tutte le altre versioni, mentre JVM 4 non sa come eseguire il codice mirato per JVM 5. Tutti questi mentre si possiede una libreria joda. Questa incompatibilità vola sotto il radar grazie a diverse soluzioni:

  1. Versioning semantico: quando le librerie hanno come target JVM superiore, di solito cambiano la versione principale.
  2. Usa JVM 4 come ABI e sei al sicuro.
  3. Java 9 aggiunge una specifica su come includere il bytecode per specifiche JVM mirate nella stessa libreria.

Perché ho iniziato con la definizione dell'API?

API e ABI sono solo convenzioni su come definire la compatibilità. Gli strati inferiori sono generici rispetto a una pletora di semantica di alto livello. Ecco perché è facile prendere delle convenzioni. Il primo tipo di convenzioni riguarda l'allineamento della memoria, la codifica dei byte, le convenzioni di chiamata, le codifiche big e little endian, ecc. Inoltre, si ottengono le convenzioni eseguibili come altre descritte, le convenzioni di collegamento, il codice byte intermedio come quello usato da Java o LLVM IR utilizzato da GCC. Terzo ottieni convenzioni su come trovare le librerie, come caricarle (vedi Classloader Java). Mentre vai sempre più in alto nei concetti hai nuove convenzioni che consideri come un dato. Ecco perché non sono arrivati ​​al versioning semantico . Sono impliciti o collassati nelversione. Potremmo modificare il versioning semantico con <major>-<minor>-<patch>-<platform/ABI>. Questo è ciò che sta realmente accadendo già: la piattaforma è già una rpm, dll, jar(JVM bytecode), war(JVM + web server), apk, 2.11(versione specifica Scala) e così via. Quando dici APK, parli già di una parte ABI specifica della tua API.

L'API può essere trasferita su diversi ABI

Il livello superiore di un'astrazione (le fonti scritte sull'API più alta possono essere ricompilate / portate su qualsiasi altra astrazione di livello inferiore.

Diciamo che ho alcune fonti per rxscala. Se gli strumenti Scala vengono cambiati, posso ricompilarli in quello. Se la JVM cambia potrei avere conversioni automatiche dalla vecchia macchina a quella nuova senza preoccuparmi dei concetti di alto livello. Mentre il porting potrebbe essere difficile, aiuterà qualsiasi altro client. Se viene creato un nuovo sistema operativo utilizzando un codice assembler totalmente diverso, è possibile creare un traduttore.

API portate su più lingue

Esistono API portate in più lingue come flussi reattivi . In generale definiscono mappature su linguaggi / piattaforme specifici. Direi che l'API è la specifica principale definita formalmente in linguaggio umano o anche in un linguaggio di programmazione specifico. Tutte le altre "mappature" sono in un certo senso ABI, altrimenti più API rispetto al solito ABI. Lo stesso sta accadendo con le interfacce REST.


1

In breve e in filosofia, solo cose del genere possono andare d'accordo, e l'ABI potrebbe essere visto come il tipo di cose software che lavorano insieme.


1

Stavo anche cercando di capire ABI e la risposta di JesperE è stata molto utile.

Da una prospettiva molto semplice, possiamo provare a capire l'ABI considerando la compatibilità binaria.

Il wiki di KDE definisce una libreria compatibile binaria "se un programma collegato dinamicamente a una versione precedente della libreria continua a funzionare con le versioni più recenti della libreria senza la necessità di ricompilare". Per ulteriori informazioni sul collegamento dinamico, consultare Collegamento statico e collegamento dinamico

Ora, proviamo a esaminare solo gli aspetti più basilari necessari affinché una libreria sia compatibilità binaria (supponendo che non ci siano modifiche al codice sorgente nella libreria):

  1. Architettura dello stesso set di istruzioni compatibile con le versioni precedenti (istruzioni del processore, struttura del file di registro, organizzazione dello stack, tipi di accesso alla memoria, dimensioni, layout e allineamento dei tipi di dati di base a cui il processore può accedere direttamente)
  2. Stesse convenzioni di chiamata
  3. Convenzione con lo stesso nome sul mangling (questo potrebbe essere necessario se un programma Fortran deve chiamare una funzione di libreria C ++).

Certo, ci sono molti altri dettagli, ma questo è principalmente quello che copre anche l'ABI.

Più specificamente per rispondere alla tua domanda, da quanto sopra, possiamo dedurre:

Funzionalità ABI: compatibilità binaria

entità esistenti: programma / librerie / SO esistenti

consumatore: librerie, SO

Spero che sia di aiuto!


1

Interfaccia binaria dell'applicazione (ABI)

Funzionalità:

  • Traduzione dal modello del programmatore al tipo di dati di dominio del sistema sottostante, dimensione, allineamento, convenzione di chiamata, che controlla il modo in cui vengono passati gli argomenti delle funzioni e restituito i valori; i numeri di chiamata del sistema e come un'applicazione deve effettuare chiamate di sistema al sistema operativo; lo schema di modifica del nome dei compilatori di linguaggio di alto livello, la propagazione delle eccezioni e la convenzione di chiamata tra compilatori sulla stessa piattaforma, ma non richiedono compatibilità multipiattaforma ...

Entità esistenti:

  • Blocchi logici che partecipano direttamente all'esecuzione del programma: ALU, registri di uso generale, registri per mappatura memoria / I / O di I / O, ecc ...

consumatore:

  • Linker, assemblatore di processori di linguaggio ...

Questi sono necessari per chiunque debba garantire che le catene di strumenti di costruzione funzionino nel loro insieme. Se si scrive un modulo in linguaggio assembly, un altro in Python e invece del proprio bootloader si desidera utilizzare un sistema operativo, i moduli "applicazione" stanno lavorando oltre i limiti "binari" e richiedono l'accordo di tale "interfaccia".

Manomissione del nome C ++ perché potrebbe essere necessario collegare i file oggetto da diverse lingue di alto livello nell'applicazione. Prendi in considerazione l'utilizzo della libreria standard GCC per effettuare chiamate di sistema a Windows create con Visual C ++.

ELF è una possibile aspettativa del linker da un file oggetto per l'interpretazione, anche se JVM potrebbe avere qualche altra idea.

Per un'app di Windows RT Store, prova a cercare ARM ABI se desideri davvero far lavorare insieme una catena di strumenti di build.


1

Il termine ABI è usato per riferirsi a due concetti distinti ma correlati.

Quando si parla di compilatori, si fa riferimento alle regole utilizzate per tradurre da costrutti di livello sorgente a costrutti binari. Quanto sono grandi i tipi di dati? come funziona lo stack? come faccio a passare i parametri alle funzioni? quali registri devono essere salvati dal chiamante rispetto alla chiamata?

Quando si parla di librerie si riferisce all'interfaccia binaria presentata da una libreria compilata. Questa interfaccia è il risultato di una serie di fattori tra cui il codice sorgente della libreria, le regole utilizzate dal compilatore e in alcuni casi le definizioni raccolte da altre librerie.

Le modifiche a una libreria possono interrompere l'ABI senza interrompere l'API. Considera ad esempio una libreria con un'interfaccia simile.

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

e il programmatore dell'applicazione scrive codice come

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

Il programmatore dell'applicazione non si preoccupa delle dimensioni o del layout di FOO, ma il binario dell'applicazione finisce con una dimensione codificata di foo. Se il programmatore della libreria aggiunge un campo extra a foo e qualcuno usa il nuovo binario della libreria con il vecchio binario dell'applicazione, la libreria potrebbe fare fuori dagli accessi alla memoria dei limiti.

OTOH se l'autore della biblioteca avesse progettato la loro API come.

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

e il programmatore dell'applicazione scrive codice come

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

Quindi il binario dell'applicazione non ha bisogno di sapere nulla sulla struttura di FOO, che può essere tutto nascosto all'interno della libreria. Il prezzo da pagare per questo è che sono coinvolte le operazioni di heap.


0

ABI- Application Binary Interfaceriguarda una comunicazione del codice macchina in fase di esecuzione tra due parti di programma binario come - applicazione, libreria, sistema operativo ... ABIdescrive come gli oggetti vengono salvati in memoria e come vengono chiamate le funzioni ( calling convention)

Un buon esempio di API e ABI è l'ecosistema iOS con linguaggio Swift .

  • Application- Quando si crea un'applicazione utilizzando lingue diverse. Ad esempio, è possibile creare un'applicazione usando Swifte Objective-C[Mixing Swift e Objective-C]

  • Application - OS- runtime - Swift runtimee standard librariesfanno parte del sistema operativo e non devono essere inclusi in ciascun pacchetto (ad es. app, framework). È lo stesso che usa Objective-C

  • Library- Module Stabilitycase - tempo di compilazione - sarai in grado di importare un framework che è stato creato con un'altra versione del compilatore di Swift. Significa che è sicuro creare un binario (pre-build) a sorgente chiuso che verrà utilizzato da una diversa versione del compilatore ( .swiftinterfaceutilizzato con .swiftmodule) e non otterrai

    Module compiled with _ cannot be imported by the _ compiler
    
  • Library- Library Evolutioncaso

    1. Tempo di compilazione: se è stata modificata una dipendenza, non è necessario ricompilare un client.
    2. Runtime: una libreria di sistema o un framework dinamico possono essere sostituiti a caldo con uno nuovo.

[API vs ABI]

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.