Полная совместимость. Как работают статические исполняемые файлы в Linux

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

В последние годы независимые от дистрибутива решения для установки пакетов набирают популярность: к 0install с его давней историей прибавились более новые Flatpak и Snap, да и в Steam игр для Linux становится больше.

Удобнее всего может быть обойтись без установки вообще — просто скачать файл, сделать его исполняемым с помощью chmod +x и запустить. В этой статье мы рассмотрим, почему это возможно и как это реализовать.

Двоичная совместимость в системах на основе Linux

Часто можно услышать, что в Linux плохо с двоичной совместимостью. При этом люди имеют в виду, что пакет из одной версии дистрибутива Linux порой невозможно поставить на другую, — и в этом они правы. Однако само утверждение о плохой двоичной совместимости версий Linux неверно.

Нужно вспомнить, что Linux — это только ядро, а вся операционная система состоит из ядра, разделяемых библиотек и программ. Библиотеки можно разделить на стандартные библиотеки языков (например, libc, libstd++) и сторонние библиотеки (например, GTK, Qt). Так вот, причиной сломанной двоичной совместимости, как правило, оказываются именно сторонние библиотеки.

Границы ABI, то есть места, где что-то может сломаться, зависят от метода компоновки (linking): статического или динамического.

Границы ABI при статической и динамической компоновке

При динамической компоновке стабильным должен оставаться ABI всех задействованных библиотек. При этом ABI ядра может меняться, если библиотеки это компенсируют. При статической компоновке важна только стабильность ABI ядра. Рассмотрим, как с этим в Linux.

Совместимость библиотек

ABI библиотеки — это набор символов, которые она экспортирует. Может ли он быть стабильным? Да, реализация формата ELF в Linux поддерживает версионирование символов. С помощью этого механизма библиотеки могут предоставлять несколько версий одной и той же функции с разными сигнатурами и поведением.

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

Не у каждого разработчика есть время и возможность поддерживать совместимость таким образом. Кроме того, радикальные изменения во внутренностях библиотеки могут сделать этот подход совершенно непрактичным — придется, по сути, собирать один двоичный файл из нескольких версий исходного кода.

Именно из-за идеи сделать как можно больше библиотек общими для всех пакетов мы и не можем поставить пакет из Debian 9 на 10 или из CentOS 6 на 8 — сторонние библиотеки не входят в пакет, а их ABI не остается неизменным между версиями.

Однако разработчики GNU libc серьезно относятся к совместимости и версионируют все символы. Благодаря этому, если собрать программу на машине со старой версией glibc, она будет работать с более новыми (но не наоборот).

Совместимость версий ядра

ABI ядра представляет собой соглашение о системных вызовах, а также номера и порядок аргументов отдельных вызовов. К примеру, чтобы попросить ядро выполнить write — системный вызов для записи данных в файл, нужно поместить в один регистр процессора номер этого вызова, а в три других регистра — номер файлового дескриптора, указатель на буфер с данными и размер данных в байтах. Любая функция для записи в файл или вывода текста на экран (то есть записи в файлы stdout и stderr с дескрипторами 1 и 2 соответственно) — обертка к этому вызову.

Самый низкий уровень стандартной библиотеки любого языка — набор оберток к системным вызовам. Если изменится ABI ядра, перестанут работать все библиотеки. К счастью, в Linux такого не происходит — интерфейс системных вызовов исключительно стабилен. Линус Торвальдс строго следит за соблюдением совместимости и ругается на всех, кто ее пытается случайно или намеренно сломать.

Благодаря такой политике любая программа из времен ядра 2.6 будет нормально работать и на 5.x, если она не использует никакие разделяемые библиотеки.

Более того, новые программы могут работать на старых ядрах, если используют только старые системные вызовы.

А что в других системах?

Из распространенных свободных Unix-подобных систем Linux — единственная с такими гарантиями совместимости ABI ядра.

Именно поэтому для Linux существует множество альтернативных стандартных библиотек языка C (musl, dietlibc, uclibc, newlib...). По этой же причине FreeBSD реализует двоичную совместимость с Linux, но не наоборот — FreeBSD не дает гарантий стабильности ABI между релизами.

Поддержка версионирования символов в GNU libc тоже нетипична для реализаций стандартной библиотеки.

Таким образом, возможностей для двоичной совместимости в Linux больше, чем во многих других системах, — нужно только ими пользоваться.

Выводы

Из всего сказанного видно, что статическая сборка — залог совместимости с любой системой на основе Linux, причем и с более новыми ядрами, и с более старыми.

Альтернативный подход — AppImage тоже позволяет собрать все в один файл, но этот файл на деле представляет собой сжатый образ SquashFS с исполняемым файлом и динамическими библиотеками внутри. Увы, инструменты автоматического поиска и упаковки всех нужных библиотек капризны, и детальное рассмотрение этого подхода не поместится в рамки нашей статьи. Мы сосредоточимся на статической компоновке.

Практика

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

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Даниил Батурин: Координатор проекта VyOS (https://vyos.io), «языковед», функциональщик, иногда сетевой администратор

Комментарии (4)

  • Как-то внезапно статья подошла к концу...

    • Согласен, вроде только началась интересная часть и ... Комментарии.
      Хотя стартовый задел дан.

  • А что если в либе есть две функции с одинаковой сигнатурой, но разным поведением и они имеют разные версии, то как мне из своего кода вызвать функцию нужной мне версии?

  • На основе чего file определяет dynamically link? Это какой-то бит или более сложный анализ?

    В чем отличие динамической и статической либы? Одну и ту же либу, полагаю, можно собрать разными путями или статическая от динамической будет отличаться более чем методом сборки (например, кодом либы)?

    Верно ли, что в musl libnss трушно статичен в отличии от libc'шного?

Похожие материалы