Двоичный интерфейс приложения - родственник API с нижнего уровня

Есть ли смысл в этом разбираться?

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

Что же такое ABI?

Чтобы понять ABI, нужно для начала вспомнить, что такое API.

API  —  это контракт, который позволяет отправлять и получать данные между приложениями. У контракта, как это и положено, есть определенные условия, следуя которым, мы получаем желаемое.

Чтобы ваша программа могла обратиться к данным моей программы, используйте этот формат и URL для вызова  —  GET https://www.somesite.com?someQueryParm=42. Я, в свою очередь, отправлю вам ответ в следующем формате { “result”: “Some imaginary computer says this is the meaning of life”}.

Для использования API вам не нужно задействовать тот же код, что и его создатель. API может быть написан на Go при том, что я работаю в JS. И это не является проблемой  —  при условии соблюдения контракта.

ABI в этом плане довольно похожи  —  но работают на двоичном уровне. Контракт заключается между двумя элементами двоичного кода. Для того, чтобы работать вместе, им нужно следовать одному набору правил. Эти правила должны соблюдаться каждым инструментом, взаимодействующим с двоичными представлениями. Здесь подразумеваются компиляторы, компоновщики и ассемблеры.

Целевой ABI может влиять даже на реализацию кода библиотеки.

Соглашения ABI зависят от двух основных компонентов:

  • архитектуры набора команд (ISA)  —  в основном для соглашений вызовов;
  • используемой операционной системы  —  системные вызовы, библиотеки среды выполнения и так далее.

Эта двоякая зависимость и является причиной, по которой код, скомпилированный для Windows, не будет работать на машине с OS X, даже если эти системы будут использовать одинаковые ЦПУ и ISA.

Что включают в себя ABI?

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

Кроме того, они определяют следующее.

  • Наборы инструкций процессора: как организуются регистры, стек, доступ к памяти и так далее.
  • Допустимые для использования типы данных: беззнаковый int 32 и так далее.
  • Как представлять имена библиотечных функций. В API это довольно просто, даже при использовании перегрузки функций. А вот в двоичном интерфейсе необходимо уникальное представление. Поэтому ABI должен определять, как разрешать идентичные названия функций, например с помощью декорирования имен.
  • Как приложение может вызывать ОС, через прямые/косвенные системные вызовы, как растут стеки, порядок следования байтов и так далее.

Реальные примеры

Для начала приведу вымышленный забавный случай использования программы для заказа пиццы.

  • Уровень API. У API есть функция PreparePizza, принимающая один аргумент: pizzaSize. Предположим, что этот аргумент представлен целым числом в диапазоне от 1 до 3.
  • Уровень ABI. ABI идет глубже и определяет, будет ли аргумент передан в стек вызовов или же через регистры.

Итак, мы вызываем API  —  preparePizza(3) → max size, заказывая самую большую пиццу, так как очень голодны. Этот код преобразуется в двоичную форму, машина его потребляет, и Pizza уже в пути.

Но разве мыслима пицца без топпинга (toppings)?

Значит в API v2 есть еще один аргумент  —  toppings. Чтобы не усложнять пример, этот аргумент мы выразим тоже целым числом. На этот раз его значение будет от 1 до 6. Каждое число представляет разный вид топпинга  —  дополнительный сыр, оливки, ананас, грибы.

Далее мы делаем еще один вызов API  —  preparePizza(3,1) → max size. Мы очень голодны и хотим больше сыра.

Только есть одна проблема. Мы использовали другой компилятор. Он смог скомпилировать код, но работает этот компилятор с другим ABI, в связи с чем передает аргументы в стек вызова в обратном порядке по сравнению с предыдущим компилятором.

Значит за кадром генерируется другой двоичный код. Заказ привозят к нашей двери, и мы видим маленькую пиццу, приправленную ананасами.

Дело в том, что (3,1) превратилось в (1,3) → большая пицца с дополнительным сыром стала маленькой с ананасом.

А теперь реальные примеры.

  • Работа с разными языками. Например, Pascal передает аргументы в стек в порядке, обратном тому, в котором передает их C. Поэтому по умолчанию они не соответствуют одному ABI.
  • Работа с разными компиляторами в одном языке. Разные компиляторы С++ по-разному декорируют имена (это лишь одно из отличий), поэтому их двоичные файлы нельзя слинковать в один исполняемый.
  • Ядро Linux известно тем, что не сохраняет стабильный ABI. Его необходимо каждый раз перекомпилировать (хотя есть и обходные пути  —  DKMS).
  • Внесение изменений в библиотеку без изменения связанного с ней исполняемого файла. Пример.

Выводы

Вы можете никогда не использовать ABI, но будет нелишним ознакомиться с этим понятием и разобраться, как все работает за кадром. 

ABI  —  это контракт между двумя компонентами двоичного кода. Этот контракт должен соблюдаться каждым инструментом, который работает с двоичными представлениями. К таким инструментам относятся компиляторы, компоновщики, ассемблеры и даже реализация кода библиотек. 

Это делает двоичный код портативным, позволяя различным платформам, имеющим совместимые ABI, его выполнять или динамически линковать в исполняемый файл.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Guy Erez: Application Binary Interface — API’s Low-Level Relative

Предыдущая статьяРешение крупномасштабных задач машинного обучения на Python
Следующая статьяСоздание анимированной пузырьковой диаграммы Ханса Рослинга на языке R