api - Что такое двоичный интерфейс приложения (ABI)?




7 Answers

Один простой способ понять «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. Пожалуйста, не указывайте мне статью в Википедии. Если бы я мог это понять, я бы не стал размещать такой длинный пост.

Это мой взгляд на разные интерфейсы:

Пульт телевизора - это интерфейс между пользователем и телевизором. Это уже существующая сущность, но бесполезная (не предоставляет никакой функциональности) сама по себе. Все функции для каждой из этих кнопок на пульте дистанционного управления реализованы в телевизоре.

Интерфейс: это уровень существующей сущности между functionality и consumer этой функциональности. Интерфейс сам по себе не делает ничего. Он просто вызывает функциональность, лежащую позади.

Теперь в зависимости от того, кто у пользователя есть разные типы интерфейсов.

Команды интерфейса командной строки (CLI) - это существующие сущности, потребитель - пользователь, а функции - отстающие.

functionality: моя программная функциональность, которая решает какую-то цель, с которой мы описываем этот интерфейс.

existing entities: команды

consumer: пользователь

Окно графических пользовательских интерфейсов (GUI) , кнопки и т. Д. - это существующие сущности, и снова потребитель - это пользователь, и функции лежат позади.

functionality: моя программная функциональность, которая решает какую-то цель, с которой мы описываем этот интерфейс.

existing entities: окно, кнопки и т. д.

consumer: пользователь

Функции API прикладного программирования (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)»?

  • Где я могу найти ABI Microsoft Windows?

Итак, это основные запросы, которые меня прослушивают.




Вы вообще не нуждаетесь в ABI, если ...

  • Ваша программа не имеет функций, и -
  • Ваша программа - это единственный исполняемый файл, который работает один (т.е. встроенная система), где он буквально работает только, и ему не нужно разговаривать ни с чем другим.

Упрощенное резюме:

API: «Вот все функции, которые вы можете назвать».

ABI: «Вот как вызвать функцию».

ABI - это набор правил, которым придерживаются компиляторы и компоновщики, чтобы скомпилировать вашу программу, чтобы она работала правильно. ABI охватывают несколько тем:

  • Вероятно, самая большая и самая важная часть ABI - это стандарт вызова процедур, иногда называемый «конвенцией о вызове». Вызывающие условные обозначения стандартизируют, как «функции» переводятся в код сборки.
  • ABI также определяют, как должны отображаться имена открытых функций в библиотеках, чтобы другой код мог вызывать эти библиотеки и знать, какие аргументы должны быть переданы. Это называется «манипулирование именем».
  • ABI также определяют, какие типы типов данных могут использоваться, как они должны быть выровнены, и другие детали низкого уровня.

Взяв более глубокий взгляд на вызов конвенции, которую я считаю основным ядром ABI:

Сама машина не имеет понятия «функции». Когда вы пишете функцию на высокоуровневом языке, таком как c, компилятор генерирует строку кода сборки, например _MyFunction1: Это ярлык , который в конечном итоге будет разрешен в адрес ассемблером. Этот ярлык обозначает «начало» вашей «функции» в коде сборки. В высокоуровневом коде, когда вы «вызываете» эту функцию, то, что вы действительно делаете, приводит к тому, что CPU переходит к адресу этой метки и продолжает выполняться там.

В рамках подготовки к прыжку компилятор должен сделать кучу важных вещей. Вызывающее соглашение похоже на контрольный список, который следует компилятору для выполнения всего этого:

  • Во-первых, компилятор вставляет немного кода сборки, чтобы сохранить текущий адрес, так что, когда ваша «функция» будет выполнена, процессор может вернуться в нужное место и продолжить выполнение.
  • Затем компилятор генерирует код сборки для передачи аргументов.
    • Некоторые соглашения о вызовах диктуют, что аргументы должны быть помещены в стек ( в определенном порядке, конечно).
    • Другие соглашения диктуют, что аргументы должны быть помещены в конкретные регистры (в зависимости от их типов данных, конечно).
    • В других конвенциях диктуется необходимость использования конкретной комбинации стека и регистров.
  • Конечно, если раньше в этих регистрах было что-то важное, эти значения теперь перезаписываются и теряются навсегда, поэтому некоторые соглашения о вызове могут диктовать, что компилятор должен сохранять некоторые из этих регистров до размещения в них аргументов.
  • Теперь компилятор вставляет инструкцию перехода, сообщающую CPU, чтобы перейти к той метке, которую она сделала ранее ( _MyFunction1: . На этом этапе вы можете считать CPU «включенным» в вашу «функцию».
  • В конце функции компилятор помещает некоторый код сборки, который заставит CPU записать возвращаемое значение в нужном месте. Вызывающая конвенция будет определять, следует ли вводить возвращаемое значение в конкретный регистр (в зависимости от его типа) или в стеке.
  • Теперь пришло время для очистки. Вызывающее соглашение будет определять, где компилятор размещает код сборки очистки.
    • Некоторые соглашения говорят, что вызывающий должен очистить стек. Это означает, что после того, как «функция» будет выполнена, и процессор вернется туда, где он был раньше, самый следующий код, который должен быть выполнен, должен быть некоторым очень конкретным кодом очистки.
    • В других соглашениях утверждается, что некоторые части кода очистки должны быть в конце «функции» перед скачком назад.

Существует много различных соглашений ABI / call. Некоторые из них:

  • Для процессора x86 или x86-64 (32-разрядная среда):
    • Cdecl
    • STDCALL
    • азЬсаИ
    • VECTORCALL
    • THISCALL
  • Для процессора x86-64 (64-разрядная среда):
    • SystemV
    • MSNATIVE
    • VECTORCALL
  • Для процессора ARM (32-разрядный)
    • AAPCS
  • Для процессора ARM (64-разрядная версия)
    • AAPCS64

Here отличная страница, которая на самом деле показывает различия в сборке, сгенерированной при компиляции для разных ABI.

Другое дело, что ABI не только релевантен внутри исполняемого модуля вашей программы. Он также используется компоновщиком, чтобы убедиться, что ваша программа правильно выполняет функции библиотеки. У вас есть несколько разделяемых библиотек, работающих на вашем компьютере, и пока ваш компилятор знает, что каждый из них использует ABI, он может корректно вызывать функции из них, не взорвав стек.

Ваш компилятор, понимающий, как вызвать библиотечные функции, чрезвычайно важен. На размещенной платформе (то есть, где ОС загружает программы), ваша программа даже не может мигать без вызова ядра.




Функциональность: набор контрактов, которые влияют на компилятор, сборщики, компоновщик и операционную систему. Контракты определяют, как распределяются функции, где передаются параметры, как передаются параметры, как функция возвращается. Они обычно специфичны для кортежей (процессорной архитектуры, операционной системы).

Существующие объекты: макет параметров, семантика функций, распределение регистров. Например, архитектуры ARM имеют множество ABI (APCS, EABI, GNU-EABI, неважно, куча исторических случаев) - использование смешанного ABI приведет к тому, что ваш код просто не работает при вызове через границы.

Потребитель: компилятор, сборщики, операционная система, архитектура процессора.

Кому нужны эти детали? Компилятор, сборщики, компоновщики, которые генерируют код (или требования к выравниванию), операционную систему (обработка прерываний, интерфейс syscall). Если вы программировали сборку, вы соответствовали ABI!

C ++ name mangling - это особый случай - проблема с компоновщиком и динамическим компоновщиком - если управление именами не стандартизировано, динамическая компоновка не будет работать. В дальнейшем C ++ ABI называется именно этим, C ++ ABI. Это не проблема уровня компоновщика, а проблема генерации кода. После того, как у вас есть двоичный код C ++, невозможно сделать его совместимым с другим C ++ ABI (манипулирование именами, обработка исключений) без перекомпиляции из исходного кода.

ELF - это формат файла для использования загрузчика и динамического компоновщика. ELF - это формат контейнера для двоичного кода и данных и, как таковой, указывает ABI части кода. Я бы не рассматривал ELF как ABI в строгом смысле слова, поскольку исполняемые файлы PE не являются ABI.

Все ABI являются определенными наборами инструкций. ARM ABI не будет иметь смысла на процессоре MSP430 или x86_64.

Windows имеет несколько ABI - например, fastcall и stdcall - это два распространенных ABI. Сценарий ABI снова отличается.




Лучший способ разграничения между ABI и API - узнать, почему и для чего он используется:

Для x86-64 обычно есть один ABI (а для 32-разрядного x86 - другой набор):

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 следуют за ним с небольшими вариациями. И у Windows x64 есть свой ABI:

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

Зная ABI и предполагая, что другой компилятор следует за ним, тогда бинарные файлы теоретически знают, как звонить друг другу (в частности, API библиотек) и передавать параметры по стеку или регистрам и т. Д. Или какие регистры будут изменены при вызове функций и т. Д. По сути, эти знания помогут программному обеспечению интегрироваться друг с другом. Зная порядок реестров / компоновку стека, я могу легко собрать вместе разные программы, написанные на сборках, без особых проблем.

Но API разные:

Это имена функций высокого уровня с определенным аргументом, так что, если различные части программного обеспечения, созданные с использованием этих API, МОГУТ иметь возможность звонить друг другу. Но необходимо соблюдать дополнительное требование SAME ABI.

Например, Windows была совместима с POSIX API:

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

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

И Linux также совместим с POSIX. Но двоичные файлы нельзя просто переместить и запустить сразу. Но поскольку они использовали те же NAMES в API, совместимом с POSIX, вы можете взять то же программное обеспечение на C, перекомпилировать его в другой ОС и немедленно запустить.

API предназначены для облегчения интеграции программного обеспечения - стадии предварительной компиляции. Поэтому после компиляции программное обеспечение может выглядеть совершенно иначе - если ABI отличается.

ABI предназначены для определения точной интеграции программного обеспечения на уровне двоичного / сборочного уровня.




Резюме

Существуют различные интерпретации и сильные мнения о точном слое, которые определяют ABI (двоичный интерфейс приложения).

На мой взгляд, ABI является субъективным соглашением того, что считается конкретной / платформой для конкретного API. ABI - это «отдых» соглашений, которые «не изменятся» для конкретного API или которые будут рассмотрены средой выполнения: исполнители, инструменты, компоновщики, компиляторы, jvm и ОС.

Определение интерфейса : ABI, API

Если вы хотите использовать библиотеку типа joda-time, вы должны объявить зависимость от joda-time-<major>.<minor>.<patch>.jar . Библиотека следует лучшим практикам и использует семантическую версию . Это определяет совместимость API на трех уровнях:

  1. Патч - вам не нужно менять код вообще. Библиотека исправляет некоторые ошибки.
  2. Незначительный - вам не нужно менять код с момента добавления
  3. Major - интерфейс (API) изменен, и вам может потребоваться изменить код.

Для того, чтобы вы могли использовать новый основной выпуск одной и той же библиотеки, необходимо соблюдать множество других конвенций:

  • Бинарный язык, используемый для библиотек (в Java рассматривается целевая версия JVM, которая определяет байт-код Java)
  • Вызовы
  • Соглашения JVM
  • Связывание конвенций
  • Соглашения о времени выполнения Все это определяется и управляется инструментами, которые мы используем.

Примеры

Изучение Java

Например, Java стандартизовал все эти соглашения, а не в инструменте, но в формальной спецификации JVM. Спецификация позволила другим поставщикам предоставить другой набор инструментов, которые могут выводить совместимые библиотеки.

Java предоставляет два других интересных примера для ABI: версии Scala и виртуальной машины Dalvik .

Виртуальная машина Dalvik нарушила ABI

Для Dalvik VM нужен другой тип байт-кода, чем байт-код 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, открытая 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 знает, как запускать скомпилированный / целевой код ( -targetвариант javac ) для всех других версий, в то время как JVM 4 не знает, как запускать код, предназначенный для JVM 5. Все это, пока у вас есть одна библиотека joda. Эта несовместимость пролетает над радаром благодаря различным решениям:

  1. Семантическое управление версиями: когда библиотеки нацелены на более высокую JVM, они обычно меняют основную версию.
  2. Используйте JVM 4 как ABI, и вы в безопасности.
  3. В 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 можно рассматривать как вид из которых программного обеспечения вещи работают вместе.




Бинарный интерфейс приложения (ABI)

Функциональные возможности:

  • Перевод с модели программиста на тип данных домена, размер, выравнивание, соглашение о вызове, которое контролирует передачу аргументов функций и возвращает полученные значения; номера системных вызовов и как приложение должно выполнять системные вызовы в операционной системе; схему кодирования имени компилятора языка высокого уровня, распространение исключений и соглашение о вызовах между компиляторами на одной платформе, но не требуют совместимости между платформами ...

Существующие объекты:

  • Логические блоки, которые непосредственно участвуют в выполнении программы: ALU, регистры общего назначения, регистры для отображения памяти / ввода-вывода ввода-вывода и т. Д.

потребитель:

  • Компилятор языка, сборщик ...

Они необходимы тем, кто должен обеспечить, чтобы сборка цепей инструментов работала в целом. Если вы пишете один модуль на ассемблере, другой в Python, а вместо своего собственного загрузчика хотите использовать операционную систему, то ваши «прикладные» модули работают через «двоичные» границы и требуют согласования такого «интерфейса».

C ++, поскольку объектные файлы с разных языков высокого уровня могут быть связаны с вашим приложением. Подумайте о том, как использовать стандартную библиотеку GCC для системных вызовов Windows, созданных с помощью Visual C ++.

ELF - одно из возможных ожиданий компоновщика из объектного файла для интерпретации, хотя у JVM может быть и другая идея.

Для приложения Windows RT Store попробуйте выполнить поиск ARM ABI, если вы действительно хотите объединить несколько цепочек инструментов сборки.




Related