Есть ли смысл в этом разбираться?
Честно сказать, у меня нет какого-то конкретного ответа, кроме как личное убеждение, что изучение всех нюансов профессии повышает уровень квалификации. Причем это также пробуждает в нас любопытство, что всегда только на пользу.
Что же такое 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, его выполнять или динамически линковать в исполняемый файл.
Читайте также:
- Как повысить производительность бэкенд-приложений
- Переиспользуем соединения OkHttp по-максимуму Журнал
- No-code и сферы его применения
Читайте нас в Telegram, VK и Дзен
Перевод статьи Guy Erez: Application Binary Interface — API’s Low-Level Relative