Что такое двоичный интерфейс приложения (ABI)?
Я никогда не понимал, что такое ABI. Пожалуйста, не указывайте мне статью в Википедии. Если бы я мог это понять, я бы не стал публиковать такие длинные сообщения.
Это мое мышление о различных интерфейсах:
Пульт дистанционного управления - это интерфейс между пользователем и телевизором. Это существующий объект, но сам по себе бесполезный (не предоставляет никакой функциональности). Все функции каждой из кнопок на пульте реализованы в телевизоре.
Интерфейс: это слой "существующий объект" между
functionality
а такжеconsumer
этой функциональности. Интерфейс сам по себе ничего не делает. Это просто вызывает функциональность, лежащую позади.Теперь в зависимости от того, кто пользователь, существуют различные типы интерфейсов.
Команды интерфейса командной строки (CLI) - это существующие объекты, а пользователь - это пользователь, а функциональность - позади.
functionality:
мой программный функционал, который решает какую-то цель, для которой мы описываем этот интерфейс.
existing entities:
команды
consumer:
пользовательОкнографического интерфейса пользователя (GUI), кнопки и т. Д. Являются существующими объектами, и опять-таки пользователь является пользователем, а функциональность остается позади.
functionality:
моя функциональность программного обеспечения, которая решает некоторую проблему, к которой мы описываем этот интерфейс.
existing entities:
окно, кнопки и т. д.
consumer:
пользовательФункцииинтерфейса прикладного программирования (API) (или, если быть более точным), интерфейсы (в интерфейсном программировании) являются существующими объектами, потребитель здесь - другая программа, а не пользователь, и опять-таки функциональность находится за этим уровнем.
functionality:
моя функциональность программного обеспечения, которая решает некоторую проблему, к которой мы описываем этот интерфейс.
existing entities:
функции, интерфейсы (массив функций).
consumer:
другая программа / приложение.Двоичный интерфейс приложения (ABI) Вот где начинается моя проблема.
functionality:
???
existing entities:
???
consumer:
???
- Я написал программное обеспечение на разных языках и предоставил различные виды интерфейсов (CLI, GUI и API), но я не уверен, что когда-либо предоставлял ABI.
ABI охватывает такие детали, как
- тип данных, размер и выравнивание;
- соглашение о вызовах, которое управляет передачей аргументов функций и получением возвращаемых значений;
- номера системных вызовов и как приложение должно выполнять системные вызовы операционной системы;
Другие ABI стандартизируют детали, такие как
- искажение имени в C++,
- распространение исключений и
- Соглашение о вызовах между компиляторами на одной платформе, но не требует кросс-платформенной совместимости.
Кому нужны эти детали? Пожалуйста, не говорите ОС. Я знаю ассемблерное программирование. Я знаю, как работают ссылки и загрузка. Я точно знаю, что происходит внутри.
Почему в C++ появилось искажение имен? Я думал, что мы говорим на двоичном уровне. Почему языки входят?
В любом случае, я скачал [PDF] System V Application Binary Interface Edition 4.1 (1997-03-18), чтобы увидеть, что именно в нем содержится. Ну, большая часть этого не имела никакого смысла.
Почему он содержит две главы (4-ю и 5-ю) для описания формата файла ELF? На самом деле, это только две важные главы этой спецификации. Остальные главы посвящены "процессору". Во всяком случае, я думаю, что это совершенно другая тема. Пожалуйста, не говорите, что спецификации формата файлов ELF являются ABI. Это не может быть интерфейсом в соответствии с определением.
Я знаю, поскольку мы говорим на таком низком уровне, он должен быть очень конкретным. Но я не уверен, как это специфично для "архитектуры набора команд (ISA)"?
Где я могу найти Microsoft Windows ABI?
Итак, вот основные запросы, которые меня беспокоят.
17 ответов
Один простой способ понять "ABI" - это сравнить его с "API".
Вы уже знакомы с концепцией API. Если вы хотите использовать функции, скажем, некоторой библиотеки или вашей ОС, вы будете использовать API. API состоит из типов / структур данных, констант, функций и т. Д., Которые вы можете использовать в своем коде для доступа к функциональности этого внешнего компонента.
ABI очень похож. Думайте об этом как о скомпилированной версии API (или как API на уровне машинного языка). Когда вы пишете исходный код, вы получаете доступ к библиотеке через API. Как только код скомпилирован, ваше приложение получает доступ к двоичным данным в библиотеке через ABI. ABI определяет структуры и методы, которые ваше скомпилированное приложение будет использовать для доступа к внешней библиотеке (как это делал API), только на более низком уровне.
ABI важны, когда речь идет о приложениях, которые используют внешние библиотеки. Если программа построена для использования конкретной библиотеки, и эта библиотека позднее обновляется, вам не нужно перекомпилировать это приложение (и с точки зрения конечного пользователя у вас может не быть исходного кода). Если в обновленной библиотеке используется тот же ABI, ваша программа не нуждается в изменении. Интерфейс библиотеки (который действительно заботится о вашей программе) остается тем же, хотя внутренняя работа может измениться. Две версии библиотеки, имеющие один и тот же ABI, иногда называют "двоично-совместимыми", поскольку они имеют один и тот же низкоуровневый интерфейс (вы должны иметь возможность заменить старую версию новой и не иметь никаких серьезных проблем).
Иногда изменения ABI неизбежны. Когда это происходит, любые программы, использующие эту библиотеку, не будут работать, если они не будут перекомпилированы для использования новой версии библиотеки. Если ABI изменяется, а API - нет, то старые и новые версии библиотеки иногда называют "совместимыми с исходным кодом". Это означает, что, хотя программа, скомпилированная для одной версии библиотеки, не будет работать с другой, исходный код, написанный для одной версии, будет работать для другой, если он будет перекомпилирован.
По этой причине авторы библиотек стремятся поддерживать стабильный ABI (чтобы минимизировать нарушение). Поддержание стабильности ABI означает не изменение интерфейсов функций (возвращаемый тип и число, типы и порядок аргументов), определения типов данных или структур данных, определенные константы и т. Д. Можно добавлять новые функции и типы данных, но существующие должны оставаться тот же самый. Если развернуть, скажем, 16-битное поле структуры данных в 32-битное поле, то уже скомпилированный код, использующий эту структуру данных, не будет правильно обращаться к этому полю (или к любому последующему за ним). Доступ к элементам структуры данных преобразуется в адреса и смещения памяти во время компиляции, и если структура данных изменяется, то эти смещения не будут указывать на то, на что ожидает их код, и результаты в лучшем случае непредсказуемы.
ABI - это не обязательно то, что вы будете явно предоставлять, если только вы не ожидаете, что люди будут взаимодействовать с вашим кодом с использованием ассемблера. Это также не зависит от языка, так как (например) приложение C и приложение Pascal будут использовать один и тот же ABI после компиляции.
Редактировать: Относительно вашего вопроса о главах, касающихся формата файлов ELF в документах SysV ABI: причина, по которой эта информация включена, заключается в том, что формат ELF определяет интерфейс между операционной системой и приложением. Когда вы указываете ОС запускать программу, она ожидает, что программа будет отформатирована определенным образом, и (например) ожидает, что первый раздел двоичного файла будет заголовком ELF, содержащим определенную информацию с определенными смещениями памяти. Так приложение передает важную информацию о себе в операционную систему. Если вы создаете программу в двоичном формате, отличном от ELF (например, a.out или PE), то ОС, которая ожидает приложения в формате ELF, не сможет интерпретировать двоичный файл или запустить приложение. Это одна из основных причин, по которой приложения Windows не могут быть запущены непосредственно на компьютере с Linux (или наоборот) без их повторной компиляции или запуска на каком-либо уровне эмуляции, который может переводиться из одного двоичного формата в другой.
IIRC, Windows в настоящее время использует формат Portable Executable (или PE). В разделе "Внешние ссылки" этой страницы Википедии есть ссылки с дополнительной информацией о формате PE.
Также, что касается вашей заметки об искажении имен в C++: ABI может определить "стандартизированный" способ компиляции имен в C++ для совместимости. То есть, если я создаю библиотеку, а вы разрабатываете программу, которая использует библиотеку, вы должны иметь возможность использовать другой компилятор, чем я, и не беспокоиться о несовместимости получаемых двоичных файлов из-за различных схем искажения имен. Это действительно полезно, только если вы определяете новый двоичный формат файла или пишете компилятор или компоновщик.
Если вы знаете сборку и то, как все работает на уровне операционной системы, вы соответствуете определенному ABI. ABI управляет такими вещами, как передача параметров и размещение возвращаемых значений. Для многих платформ есть только один ABI на выбор, и в этих случаях ABI просто "как все работает".
Тем не менее, ABI также управляет такими вещами, как классы / объекты в C++. Это необходимо, если вы хотите иметь возможность передавать ссылки на объекты через границы модуля или если вы хотите смешивать код, скомпилированный с разными компиляторами.
Кроме того, если у вас есть 64-разрядная ОС, которая может выполнять 32-разрядные двоичные файлы, у вас будут разные ABI для 32- и 64-разрядного кода.
В целом, любой код, который вы ссылаете на один и тот же исполняемый файл, должен соответствовать одному и тому же ABI. Если вы хотите обмениваться данными между кодом, используя разные ABI, вы должны использовать протокол RPC или сериализацию.
Я думаю, вы слишком стараетесь втиснуть различные типы интерфейсов в фиксированный набор характеристик. Например, интерфейс не обязательно должен быть разделен на потребителей и производителей. Интерфейс - это просто соглашение, по которому взаимодействуют два объекта.
ABI могут быть (частично) ISA-независимыми. Некоторые аспекты (такие как соглашения о вызовах) зависят от ISA, в то время как другие аспекты (такие как макет класса C++) не зависят.
Хорошо определенный ABI очень важен для людей, пишущих компиляторы. Без четко определенного ABI было бы невозможно генерировать совместимый код.
РЕДАКТИРОВАТЬ: Некоторые примечания для уточнения:
- "Двоичный" в ABI не исключает использование строк или текста. Если вы хотите связать DLL, экспортирующую класс C++, где-то в нем должны быть закодированы методы и сигнатуры типов. Вот тут-то и происходит сортировка имен в C++.
- Причина, по которой вы никогда не предоставляли ABI, заключается в том, что подавляющее большинство программистов никогда этого не сделают. ABI предоставляются теми же людьми, которые проектируют платформу (т.е. операционную систему), и очень немногие программисты когда-либо будут иметь привилегию разрабатывать широко используемый ABI.
Тебе вообще не нужен ABI, если...
- Ваша программа не имеет функций, и...
- Ваша программа - это один исполняемый файл, который выполняется один (то есть встроенная система), где он работает буквально единственно, и ему не нужно ни с чем разговаривать.
Упрощенное резюме:
API: "Вот все функции, которые вы можете вызывать".
ABI: "Это как вызвать функцию".
ABI - это набор правил, которых придерживаются компиляторы и компоновщики, чтобы скомпилировать вашу программу, чтобы она работала правильно. ABI охватывают несколько тем:
- Возможно, самой большой и самой важной частью ABI является стандарт вызова процедуры, иногда известный как "соглашение о вызовах". Соглашения о вызовах стандартизируют перевод "функций" в код сборки.
- ABI также определяют, как должны быть представлены имена предоставляемых функций в библиотеках, чтобы другой код мог вызывать эти библиотеки и знать, какие аргументы следует передавать. Это называется "искажение имени".
- ABI также определяют, какие типы данных могут использоваться, как они должны быть выровнены и другие низкоуровневые детали.
Более подробно рассмотрим соглашение о вызовах, которое я считаю основой ABI:
Сама машина не имеет понятия "функции". Когда вы пишете функцию на языке высокого уровня, таком как c, компилятор генерирует строку кода ассемблера, например _MyFunction1:
, Это метка, которая в конечном итоге будет преобразована ассемблером в адрес. Эта метка отмечает "начало" вашей "функции" в коде сборки. В высокоуровневом коде, когда вы "вызываете" эту функцию, то, что вы действительно делаете, заставляет ЦП перейти на адрес этой метки и продолжить выполнение там.
При подготовке к прыжку компилятор должен сделать кучу важных вещей. Соглашение о вызовах похоже на контрольный список, который следует компилятору для выполнения всего этого:
- Во-первых, компилятор вставляет немного кода сборки, чтобы сохранить текущий адрес, чтобы, когда ваша "функция" была выполнена, ЦП мог вернуться назад в нужное место и продолжить выполнение.
- Затем компилятор генерирует ассемблерный код для передачи аргументов.
- Некоторые соглашения о вызовах диктуют, что аргументы должны быть помещены в стек (в определенном порядке, конечно).
- Другие соглашения диктуют, что аргументы должны быть помещены в конкретные регистры (в зависимости от их типов данных, конечно).
- Тем не менее, другие соглашения предписывают использование определенной комбинации стека и регистров.
- Конечно, если раньше в этих регистрах было что-то важное, эти значения теперь перезаписываются и теряются навсегда, поэтому некоторые соглашения о вызовах могут предписывать компилятору сохранять некоторые из этих регистров до помещения в них аргументов.
- Теперь компилятор вставляет инструкцию перехода, указывающую процессору перейти к метке, которую он сделал ранее (
_MyFunction1:
). На этом этапе вы можете считать, что процессор находится "в" вашей "функции". - В конце функции компилятор помещает некоторый код сборки, который заставит CPU записать возвращаемое значение в правильное место. Соглашение о вызовах будет определять, должно ли возвращаемое значение быть помещено в определенный регистр (в зависимости от его типа) или в стек.
- Теперь пришло время для очистки. Соглашение о вызовах будет определять, где компилятор размещает код сборки очистки.
- В некоторых соглашениях говорится, что вызывающая сторона должна очистить стек. Это означает, что после того, как "функция" будет выполнена, и ЦП перейдет туда, где он был раньше, следующий код, который должен быть выполнен, должен быть очень специфическим кодом очистки.
- В других соглашениях говорится, что некоторые конкретные части кода очистки должны находиться в конце "функции" перед возвратом.
Существует много различных ABI / соглашений о вызовах. Некоторые основные из них:
- Для процессора x86 или x86-64 (32-разрядная среда):
- Cdecl
- STDCALL
- азЬсаИ
- VECTORCALL
- THISCALL
- Для процессора x86-64 (64-разрядная среда):
- SystemV
- MSNATIVE
- VECTORCALL
- Для процессора ARM (32-разрядный)
- AAPCS
- Для процессора ARM (64-разрядный)
- AAPCS64
Вот отличная страница, которая фактически показывает различия в сборке, генерируемой при компиляции для различных ABI.
Еще одна вещь, которую стоит упомянуть, это то, что ABI относится не только к исполняемому модулю вашей программы. Он также используется компоновщиком, чтобы убедиться, что ваша программа правильно вызывает библиотечные функции. У вас есть несколько общих библиотек, работающих на вашем компьютере, и, пока ваш компилятор знает, какой ABI они используют, он может правильно вызывать функции из них, не разрушая стек.
Ваш компилятор понимает, как вызывать библиотечные функции, чрезвычайно важно. На размещенной платформе (то есть той, на которой ОС загружает программы), ваша программа не может даже мигать без вызова ядра.
Пример ABI для минимальной исполняемой библиотеки Linux
В контексте разделяемых библиотек наиболее важным следствием "наличия стабильного ABI" является то, что вам не нужно перекомпилировать свои программы после изменений библиотеки.
Так, например:
если вы продаете разделяемую библиотеку, вы избавляете своих пользователей от необходимости перекомпилировать все, что зависит от вашей библиотеки, для каждого нового выпуска
Если вы продаете программу с закрытым исходным кодом, которая зависит от общей библиотеки, присутствующей в дистрибутиве пользователя, вы можете выпустить и протестировать меньше предварительных сборок, если вы уверены, что ABI стабилен в определенных версиях целевой ОС.
Это особенно важно в случае стандартной библиотеки C, на которую ссылаются многие программы в вашей системе.
Теперь я хочу предоставить минимальный конкретный работоспособный пример этого.
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
Компилируется и работает нормально с:
cc='gcc -pedantic-errors -std=c89 -Wall -Wextra'
$cc -fPIC -c -o mylib.o mylib.c
$cc -L . -shared -o libmylib.so mylib.o
$cc -L . -o main.out main.c -lmylib
LD_LIBRARY_PATH=. ./main.out
Теперь предположим, что для v2 библиотеки мы хотим добавить новое поле в mylib_mystruct
называется new_field
,
Если мы добавили поле раньше old_field
как в:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
и перестроить библиотеку, но не main.out
тогда утверждать не удастся!
Это потому что строка:
myobject->old_field == 1
сгенерировал сборку, которая пытается получить доступ к самому первому int
структуры, которая сейчас new_field
вместо ожидаемого old_field
,
Поэтому это изменение сломало ABI.
Если, однако, мы добавим new_field
после old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
тогда старая сгенерированная сборка все еще обращается к первой int
структуры, и программа все еще работает, потому что мы сохранили ABI стабильным.
Вот полностью автоматизированная версия этого примера на GitHub.
Другой способ сохранить стабильность ABI - лечить mylib_mystruct
как непрозрачная структура, и доступ к ее полям только через помощников метода. Это облегчает поддержание стабильности ABI, но может повлечь за собой снижение производительности, поскольку мы выполняем больше вызовов функций.
API против ABI
В предыдущем примере интересно отметить, что добавление new_field
до old_field
, только сломал ABI, но не API.
Это означает, что если бы мы перекомпилировали main.c
Программа против библиотеки, это работало бы независимо.
Однако мы бы также нарушили API, если бы изменили, например, сигнатуру функции:
mylib_mystruct* mylib_init(int old_field, int new_field);
так как в этом случае, main.c
перестанет компилироваться вообще.
Семантический API против API программирования
Мы также можем классифицировать изменения API по третьему типу: семантические изменения.
Семантический API, как правило, представляет собой естественное описание того, что должен делать API, обычно включается в документацию API.
Поэтому возможно нарушить семантический API, не нарушая саму сборку программы.
Например, если мы изменили
myobject->old_field = old_field;
чтобы:
myobject->old_field = old_field + 1;
тогда это не сломало бы ни API программирования, ни ABI, но main.c
семантический API сломался бы.
Существует два способа программной проверки API контракта:
- проверить кучу угловых случаев. Это легко сделать, но вы всегда можете пропустить один.
- формальная проверка. Сложнее сделать, но производит математическое доказательство правильности, по существу объединяя документацию и тесты в "человеческий" / машинно проверяемый способ! Пока, конечно, в вашем официальном описании нет ошибки;-)
Список всего, что нарушает C / C++ разделяемые библиотеки ABI
TODO: найти / создать окончательный список:
- https://github.com/lvc/abi-compliance-checker автоматизированный инструмент для проверки
- https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B Руководство по KDE C++ ABI
- https://plan99.net/~mike/writing-shared-libraries.html
Пример минимального запуска Java
Что такое двоичная совместимость в Java?
Протестировано в Ubuntu 18.10, GCC 8.2.0.
Двоичный интерфейс приложения (ABI) похож на API, но функция недоступна для вызывающей стороны на уровне исходного кода. Только двоичное представление доступно / доступно.
ABI могут быть определены на уровне архитектуры процессора или на уровне ОС. ABI - это стандарты, которым должна следовать фаза генерации кода компилятора. Стандарт фиксируется либо ОС, либо процессором.
Функциональность: Определите механизм / стандарт, чтобы сделать вызовы функций независимыми от языка реализации или конкретного компилятора / компоновщика / цепочки инструментов. Предоставьте механизм, который позволяет JNI или интерфейс Python-C и т. Д.
Существующие объекты: функции в форме машинного кода.
Потребитель: другая функция (в том числе на другом языке, скомпилированная другим компилятором или связанная с другим компоновщиком).
Функциональность: набор контрактов, которые влияют на компилятор, составители сборок, компоновщик и операционную систему. Контракты определяют, как распределяются функции, где передаются параметры, как передаются параметры, как работает функция. Они обычно относятся к кортежу (архитектура процессора, операционная система).
Существующие сущности: расположение параметров, семантика функций, распределение регистров. Например, архитектура ARM имеет множество ABI (APCS, EABI, GNU-EABI, не говоря уже о куче исторических случаев) - использование смешанного ABI приведет к тому, что ваш код просто не будет работать при вызове через границы.
Потребитель: Компилятор, сборщики, операционная система, специфичная для процессора архитектура.
Кому нужны эти детали? Компилятор, сборщики, компоновщики, которые выполняют генерацию кода (или требования выравнивания), операционную систему (обработка прерываний, интерфейс syscall). Если вы занимались программированием на ассемблере, вы соответствовали ABI!
Перенаправление имен в C++ является особым случаем - проблема, связанная с компоновщиком и динамическим компоновщиком, - если искажение имен не стандартизировано, динамическое связывание не будет работать. Отныне C++ ABI называется именно так, C++ ABI. Это не проблема уровня компоновщика, а проблема генерации кода. Если у вас есть бинарный файл C++, невозможно сделать его совместимым с другим ABI C++ (искажение имени, обработка исключений) без перекомпиляции из исходного кода.
ELF - это формат файла для использования загрузчика и динамического компоновщика. ELF является контейнерным форматом для двоичного кода и данных, и как таковой определяет ABI фрагмента кода. Я бы не стал считать ELF ABI в строгом смысле слова, так как исполняемые файлы PE не являются ABI.
Все ABI являются специфическими для набора команд. ARM ABI не будет иметь смысла на процессоре MSP430 или x86_64.
В Windows есть несколько ABI - например, fastcall и stdcall - это два ABI общего назначения. Системный вызов ABI снова отличается.
Позвольте мне хотя бы ответить на часть вашего вопроса. С примером того, как Linux ABI влияет на системные вызовы, и почему это полезно.
Системный вызов - это способ для программы пользовательского пространства запросить что-либо в пространстве ядра. Он работает, помещая числовой код для вызова и аргумента в определенный регистр и вызывая прерывание. Затем происходит переключение в пространство ядра, и ядро просматривает числовой код и аргумент, обрабатывает запрос, помещает результат обратно в регистр и запускает переключение обратно в пространство пользователя. Это необходимо, например, когда приложение хочет выделить память или открыть файл (системные вызовы "brk" и "open").
Теперь системные вызовы имеют короткие имена "brk" и т. Д. И соответствующие коды операций, которые определены в системном заголовочном файле. Пока эти коды операций остаются неизменными, вы можете запускать одни и те же скомпилированные пользовательские программы с разными обновленными ядрами без необходимости перекомпиляции. Итак, у вас есть интерфейс, используемый предварительно скомпилированными бинарными файлами, отсюда и ABI.
Резюме
Существуют различные толкования и точные мнения о точном слое, которые определяют ABI (двоичный интерфейс приложения).
На мой взгляд, ABI - это субъективное соглашение о том, что считать данной платформой для конкретного API. ABI - это "остальная часть" соглашений, которые "не изменятся" для конкретного API или будут решаться средой выполнения: исполнителями, инструментами, компоновщиками, компиляторами, jvm и ОС.
Определение интерфейса: ABI, API
Если вы хотите использовать библиотеку, такую как joda-time, вы должны объявить зависимость от joda-time-<major>.<minor>.<patch>.jar
, Библиотека следует передовым методам и использует семантическое управление версиями. Это определяет совместимость API на трех уровнях:
- Патч - вам вообще не нужно менять свой код. Библиотека просто исправляет некоторые ошибки.
- Незначительный - Вам не нужно менять код, так как дополнения
- Основные - Интерфейс (API) изменен, и вам может потребоваться изменить код.
Чтобы вы могли использовать новую основную версию той же библиотеки, нужно соблюдать множество других соглашений:
- Бинарный язык, используемый для библиотек (в случаях Java целевая версия JVM, определяющая байт-код Java)
- Соглашения о вызовах
- Соглашения JVM
- Связывание соглашений
- Соглашения времени выполнения Все это определяется и управляется используемыми нами инструментами.
Примеры
Пример использования Java
Например, Java стандартизировала все эти соглашения не в инструменте, а в формальной спецификации JVM. Спецификация позволила другим поставщикам предоставить другой набор инструментов, которые могут выводить совместимые библиотеки.
Java предоставляет два других интересных примера для ABI: версии Scala и виртуальная машина Dalvik.
Виртуальная машина Dalvik сломала ABI
Виртуальная машина Dalvik нуждается в байт-коде другого типа, чем байт-код Java. Библиотеки Dalvik получены путем преобразования байт-кода Java (с тем же API) для Dalvik. Таким образом, вы можете получить две версии одного и того же API: определены оригиналом joda-time-1.7.2.jar
, Мы могли бы позвонить мне joda-time-1.7.2.jar
а также joda-time-1.7.2-dalvik.jar
, Они используют другой ABI, один для стекового ориентированного стандартного Java vms: Oracle, IBM, open Java или любой другой; и второй ABI - тот, что вокруг Dalvik.
Последовательные выпуски Scala несовместимы
Scala не имеет двоичной совместимости между второстепенными версиями Scala: 2.X . По этой причине один и тот же API "io.reactivex" %% "rxscala" % "0.26.5" имеет три версии (в будущем больше): для Scala 2.10, 2.11 и 2.12. Что изменилось? Пока не знаю, но двоичные файлы не совместимы. Вероятно, в последних версиях добавлены вещи, которые делают библиотеки непригодными для использования на старых виртуальных машинах, возможно, вещи, связанные с соглашениями о связывании / именовании / параметрах.
Последовательные выпуски Java несовместимы
У Java также есть проблемы с основными выпусками JVM: 4,5,6,7,8,9. Они предлагают только обратную совместимость. Jvm9 знает, как запускать скомпилированный / целевой код (Javac's -target
опция) для всех других версий, в то время как JVM 4 не знает, как выполнить код, предназначенный для JVM 5. Все это, пока у вас есть одна библиотека joda. Эта несовместимость вылетает из радара благодаря различным решениям:
- Семантическое управление версиями: когда библиотеки нацелены на более высокую JVM, они обычно меняют основную версию.
- Используйте JVM 4 в качестве ABI, и вы в безопасности.
- Java 9 добавляет спецификацию о том, как вы можете включить байт-код для конкретной целевой JVM в той же библиотеке.
Почему я начал с определения API?
API и ABI - это просто соглашения о том, как вы определяете совместимость. Нижние уровни являются общими для множества семантики высокого уровня. Вот почему легко сделать некоторые соглашения. Первый тип соглашений касается выравнивания памяти, байтового кодирования, соглашений о вызовах, кодировок с прямым и прямым порядком байтов и т. Д. Помимо них вы получаете исполняемые соглашения, подобные описанным выше, соглашения о связывании, промежуточный байт-код, такой как тот, который используется Java или LLVM IR используется GCC. В-третьих, вы получаете соглашения о том, как искать библиотеки, как их загружать (см. Загрузчики классов Java). По мере того, как вы будете подниматься выше и выше в концепциях, у вас появляются новые соглашения, которые вы рассматриваете как данное. Вот почему они не добрались до семантической версии. Они скрыты или свернуты в основной версии. Мы могли бы изменить семантическое управление версиями с <major>-<minor>-<patch>-<platform/ABI>
, Это то, что на самом деле уже происходит: платформа уже rpm
, dll
, jar
(Байт-код JVM), war
(jvm + веб-сервер), apk
, 2.11
(конкретная версия Scala) и так далее. Когда вы говорите APK, вы уже говорите об определенной части ABI вашего API.
API можно портировать на разные ABI
Верхний уровень абстракции (источники, написанные на основе самого высокого API, могут быть перекомпилированы / перенесены на любую другую низкоуровневую абстракцию.
Допустим, у меня есть несколько источников для rxscala. Если инструменты Scala будут изменены, я могу перекомпилировать их. Если JVM изменится, у меня могут быть автоматические преобразования из старой машины в новую, не заботясь о концепциях высокого уровня. Хотя портировать может быть сложно, поможет любой другой клиент. Если новая операционная система создается с использованием совершенно другого ассемблерного кода, может быть создан переводчик.
API перенесены на разные языки
Существуют API, которые портированы на несколько языков, таких как реактивные потоки. В целом они определяют сопоставления с конкретными языками / платформами. Я бы сказал, что API - это основная спецификация, формально определенная на человеческом языке или даже на конкретном языке программирования. Все остальные "отображения" в некотором смысле являются ABI, иначе API, чем обычный ABI. То же самое происходит с интерфейсами REST.
Лучший способ провести различие между ABI и API - узнать, для чего и для чего он используется:
Для x86-64 обычно есть один ABI (а для x86 32-bit есть другой набор):
http://www.x86-64.org/documentation/abi.pdf
http://people.freebsd.org/~obrien/amd64-elf-abi.pdf
Linux + FreeBSD + MacOSX следуют за ним с некоторыми небольшими изменениями. И Windows x64 имеет свой собственный ABI:
http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/
Зная ABI и предполагая, что за ним также следует другой компилятор, тогда двоичные файлы теоретически знают, как вызывать друг друга (в частности, API библиотек) и передавать параметры через стек или через регистры и т. Д. Или какие регистры будут изменены при вызове функций и т. Д. По сути эти знания помогут программному обеспечению интегрироваться друг с другом. Зная порядок расположения регистров / стека, я могу без особых проблем собрать воедино различные программы, написанные на сборках.
Но API разные:
Это имена функций высокого уровня, с определенным аргументом, так что, если различные части программного обеспечения строятся с использованием этих API, МОГУТ иметь возможность вызывать друг друга. Но необходимо соблюдать дополнительное требование ЖЕ АБИ.
Например, Windows раньше была POSIX API-совместимой:
https://en.wikipedia.org/wiki/Windows_Services_for_UNIX
https://en.wikipedia.org/wiki/POSIX
И Linux также совместим с POSIX. Но двоичные файлы нельзя просто переместить и запустить сразу. Но поскольку они использовали одни и те же NAMES в POSIX-совместимом API, вы можете взять одно и то же программное обеспечение на C, перекомпилировать его в другой ОС и сразу запустить.
API предназначены для облегчения интеграции программного обеспечения - этап предварительной компиляции. Таким образом, после компиляции программное обеспечение может выглядеть совершенно иначе - если ABI разные.
ABI предназначены для определения точной интеграции программного обеспечения на уровне двоичного кода / сборки.
Чтобы вызвать код в общих библиотеках или код вызова между блоками компиляции, объектный файл должен содержать метки для вызовов. C++ изменяет имена меток методов для обеспечения скрытия данных и допускает перегруженные методы. Вот почему вы не можете смешивать файлы из разных компиляторов C++, если они явно не поддерживают один и тот же ABI.
Термин ABI используется для обозначения двух разных, но связанных понятий.
Когда речь идет о компиляторах, это относится к правилам, используемым для перевода из конструкций исходного уровня в двоичные конструкции. Насколько велики типы данных? как работает стек? как передать параметры в функции? какие регистры должны быть сохранены вызывающим абонентом против вызываемого абонента?
Когда речь идет о библиотеках, это относится к двоичному интерфейсу, представленному скомпилированной библиотекой. Этот интерфейс является результатом ряда факторов, включая исходный код библиотеки, правила, используемые компилятором, а в некоторых случаях определения, взятые из других библиотек.
Изменения в библиотеке могут нарушить ABI, не нарушая API. Рассмотрим, например, библиотеку с интерфейсом вроде.
void initfoo(FOO * foo)
int usefoo(FOO * foo, int bar)
void cleanupfoo(FOO * foo)
и прикладной программист пишет код как
int dostuffwithfoo(int bar) {
FOO foo;
initfoo(&foo);
int result = usefoo(&foo,bar)
cleanupfoo(&foo);
return result;
}
Программист приложения не заботится о размере или расположении FOO, но двоичный файл приложения заканчивается жестко заданным размером foo. Если программист библиотеки добавляет дополнительное поле в foo, и кто-то использует новый двоичный файл библиотеки со старым двоичным файлом приложения, тогда библиотека может получить доступ к памяти за пределами границ.
OTOH, если автор библиотеки разработал свой API как.
FOO * newfoo(void)
int usefoo(FOO * foo, int bar)
void deletefoo((FOO * foo, int bar))
и прикладной программист пишет код как
int dostuffwithfoo(int bar) {
FOO * foo;
foo = newfoo();
int result = usefoo(&foo,bar)
deletefoo(&foo);
return result;
}
Тогда бинарному приложению не нужно ничего знать о структуре FOO, и все это можно спрятать внутри библиотеки. Однако цена, которую вы платите за это, заключается в том, что в этом участвует куча операций.
ABI
- Application Binary Interface
о взаимодействии машинного кода во время выполнения между двумя двоичными частями, такими как приложение, библиотека, ОС...ABI
описывает, как объекты сохраняются в памяти, как вызываются функции (calling convention
), искажая...
Хорошим примером API и ABI является экосистема iOS с языком Swift.
Application layer
- При создании приложения на разных языках. Например, вы можете создать приложение, используяSwift
а такжеObjective-C
[Смешивание Swift и Objective-C]Application - OS layer
- время выполнения -Swift runtime
а такжеstandard libraries
являются частями ОС, и их не следует включать в каждый комплект (например, приложение, фреймворк). Это то же самое, что и в Objective-C.Library layer
-Module Stability
case - время компиляции - вы сможете импортировать фреймворк, созданный с помощью другой версии компилятора Swift. Это означает, что безопасно создать двоичный файл с закрытым исходным кодом (предварительную сборку), который будет использоваться другой версией компилятора (.swiftinterface
используется с.swiftmodule
) и вы не получитеModule compiled with _ cannot be imported by the _ compiler
Library layer
-Library Evolution
кейс
- Время компиляции - если зависимость была изменена, перекомпиляцию клиента не требуется.
- Время выполнения - системная библиотека или динамический фреймворк можно заменить на новую в горячем режиме.
ABI должен быть согласованным между вызывающим абонентом и вызываемым абонентом, чтобы быть уверенным, что вызов успешен. Использование стека, использование регистра, всплывающее окно в конце процедуры. Все это самые важные части ABI.
Двоичный интерфейс приложения (ABI)
Функциональные возможности :
- Перевод из модели программиста в тип данных, размер, выравнивание домена базовой системы, соглашение о вызовах, которое управляет передачей аргументов функций и получением возвращаемых значений; номера системных вызовов и как приложение должно выполнять системные вызовы операционной системы; схема искажения имени компиляторов языка высокого уровня, распространение исключений и соглашение о вызовах между компиляторами на одной платформе, но не требуют кросс-платформенной совместимости...
Существующие объекты:
- Логические блоки, которые непосредственно участвуют в выполнении программы: ALU, регистры общего назначения, регистры для отображения памяти / ввода-вывода ввода-вывода и т. Д.
потребитель:
- Языковые процессоры, компоновщик, ассемблер...
Они нужны всем, кто должен гарантировать, что цепочки инструментов сборки работают в целом. Если вы пишете один модуль на языке ассемблера, другой на Python и вместо собственного загрузчика хотите использовать операционную систему, то ваши "прикладные" модули работают через "двоичные" границы и требуют согласования такого "интерфейса".
Искаженное имя в C++, потому что в вашем приложении могут потребоваться ссылки на объектные файлы из разных языков высокого уровня. Рассмотрите возможность использования стандартной библиотеки GCC для выполнения системных вызовов Windows, созданных с использованием Visual C++.
ELF - одно из возможных ожиданий компоновщика от объектного файла для интерпретации, хотя у JVM может быть другая идея.
Для приложения Магазина Windows RT попробуйте поискать ARM ABI, если вы действительно хотите, чтобы некоторые инструменты сборки работали вместе.
Я также пытался понять ABI, и ответ JesperE был очень полезным.
С очень простой точки зрения, мы можем попытаться понять ABI, рассматривая двоичную совместимость.
Вики KDE определяет библиотеку как бинарно-совместимую "если программа, динамически связанная с предыдущей версией библиотеки, продолжает работать с более новыми версиями библиотеки без необходимости перекомпиляции". Для получения дополнительной информации о динамическом связывании см. Статическое связывание против динамического связывания
Теперь давайте попробуем взглянуть только на самые основные аспекты, необходимые для того, чтобы библиотека была двоичной совместимостью (при условии, что в библиотеке нет изменений исходного кода):
- Одинаковая / обратно совместимая архитектура набора команд (инструкции процессора, структура файла регистра, организация стека, типы доступа к памяти, наряду с размерами, компоновкой и выравниванием основных типов данных, к которым процессор может напрямую обращаться)
- Те же соглашения о вызовах
- Соглашение об одноименном названии (это может понадобиться, если, скажем, программе на Фортране нужно вызвать некоторую библиотечную функцию C++)
Конечно, есть много других деталей, но это в основном то, что ABI также охватывает.
Более конкретно, чтобы ответить на ваш вопрос, из вышесказанного можно сделать вывод:
Функциональность ABI: двоичная совместимость
существующие объекты: существующие программы / библиотеки / ОС
потребитель: библиотеки, ОС
Надеюсь это поможет!
A. Проще говоря, у ABI есть одна общая черта с API - это то, что это интерфейс. Программа многократного использования предоставляет стабильный интерфейс (API), который можно использовать для повторного использования программы в другой программе.
Б. Однако ABI - это интерфейс, выпущенный для определенной платформы процессора для определенного языка. Все поставщики компиляторов, желающие настроить эту платформу для того же языка, должны будут гарантировать, что не только скомпилированный код в форме перемещаемых объектных кодов соответствует интерфейсу, чтобы иметь возможность связываться и перекрестно связываться друг с другом, но и исполняемые файлы соответствуют ему. чтобы вообще иметь возможность работать на платформе. Итак, ABI - это гораздо более широкий набор спецификаций / стандартов, чем типичный функциональный API. Он может включать в себя некоторые объекты API, которые будут применяться компилятором к пользователям языка. Поставщик компилятора должен будет включить поддержку того же самого в свои дистрибутивы. Излишне говорить, что поставщик платформы имеет законное право выпускать ABI для своей платформы.И поставщики компиляторов, и ABI должны соответствовать соответствующему языковому стандарту (например, стандарту ISO для C++).
C. Определение ABI от поставщика платформы:
"1. Спецификации, которым должен соответствовать исполняемый файл для выполнения в определенной среде исполнения. Например, Linux ABI для архитектуры Arm.
- Особый аспект спецификаций, которым должны соответствовать независимо созданные перемещаемые файлы, чтобы быть статически связанными и исполняемыми. Например, C++ ABI для архитектуры Arm, ABI времени выполнения для архитектуры Arm, ABI библиотеки C для архитектуры Arm ».
Д. Например. Также был выпущен общий ABI для C++, основанный на архитектуре Itanium . Степень, в которой собственные ABI поставщиков платформы для C++ соответствуют ей, полностью зависит от поставщиков платформы.
E. Другой пример. C++ ABI для архитектуры Arm находится здесь.
F. Сказав, что под капотом, это ABI архитектуры процессора, которая гарантирует, что API между одной повторно используемой программой и другой программой, которая его повторно использует, работает для этой архитектуры процессора.
G. Это подводит нас к компонентам, ориентированным на службы (например, веб-службам на основе SOAP). Они также требуют наличия API между веб-службой на основе SOAP и клиентской программой (может быть приложение, интерфейс или другая веб-служба), чтобы клиентская программа могла повторно использовать веб-службу. API описывается в терминах стандартизованных протоколов. как WSDL (описание интерфейса) и SOAP (формат сообщения) и не зависит от платформы. Он не ориентирован на какую-либо конкретную платформу процессора и, следовательно, не является «двоичным», как ABI. Клиентская программа на любом одном типе платформы может удаленно повторно использовать веб-службу на совершенно другой платформе процессора. Это стало возможным благодаря тому факту, что и WSDL, и SOAP являются текстовыми (XML) протоколами. В случае веб-служб RESTful транспортный протокол http- также текстовый протокол - сам действует как API (методы CRUD).
Короче говоря, и в философии, только вещи вроде могут хорошо ладить, и ABI можно рассматривать как вид программного обеспечения, работающего вместе.