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



Книга Двоичный интерфейс приложения — родственник 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, его выполнять или динамически линковать в исполняемый файл.



328   0  

Comments

    Ничего не найдено.