Найти:

- Взлом
- Приватность
- Трюки
- Кодинг
- Админ
- Geek
- Обучение
- Магазин

- Windows
- Linux
- Android
- Железо
- Мессенджеры
- Утечки
- Коронавирус
- Все выпуски «Хакера»

### Содержание статьи

- Двоичная совместимость в системах на основе Linux
- Совместимость библиотек
- Совместимость версий ядра
- А что в других системах?
- Выводы
- Практика
- Собираем статические файлы с GNU libc
- Статическая GNU libc и NSS
- Альтернативные библиотеки
- Заключение

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

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

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

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

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

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

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

Границы ABI при статической и динамической компоновке
Другие статьи в выпуске:

### Xakep #257. Pivoting

- Содержание выпуска
- Подписка на «Хакер»

При динамической компоновке стабильным должен оставаться 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 с исполняемым файлом и динамическими библиотеками внутри. Увы, инструменты автоматического поиска и упаковки всех нужных библиотек капризны, и детальное рассмотрение этого подхода не поместится в рамки нашей статьи. Мы сосредоточимся на статической компоновке.

## Практика

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

### Собираем статические файлы с GNU libc

Для работы с регулярными выражениями мы используем библиотеку PCRE. Сохраним следующий код в файл `match.c`:

    #include <stdio.h>
    #include <string.h>
    #include <pcre.h>

    int main(int argc, char** argv) {
        if(argc < 2) {
            fprintf(stderr, "Usage: %s <regex> <string>\n", argv[0]);
            exit(1);
        }

        char* regex = argv[1];
        char* str = argv[2];

        int offsets[1];
        pcre *re;
        int res;
        const char *error;
        int erroffset;

        re = pcre_compile(regex, 0, &error, &erroffset, NULL);
        res = pcre_exec(re, NULL, str, strlen(str), 0, 0, offsets, 1);

        return res;
    }

Программа принимает регулярное выражение и произвольную строку. Если строка соответствует этому выражению, программа завершается с кодом 0.

`$ ./match '^\d+[a-z]?$' '212850a' && echo yes` yes

Для начала соберем программу самым «обычным» образом.

`$ gcc -o match -lpcre ./match.c` `$ ldd ./match` linux-vdso.so.1 (0x00007ffc0ec5d000) libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fe700e24000) libc.so.6 => /lib64/libc.so.6 (0x00007fe700c5a000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fe700c38000) /lib64/ld-linux-x86-64.so.2 (0x00007fe700ed5000)

По умолчанию GCC использует динамическую компоновку, поэтому в выводе команды `ldd` мы видим зависимость от `libpcre.so`.

Если ты хочешь просто проверить, динамический файл или статический, достаточно и утилиты `file`.

`$ file ./match` ./match: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9e2c84368501419dc6da681aa0f0520950ad896a, for GNU/Linux 3.2.0, not stripped

Теперь перейдем к статической сборке. Для этого нужно убедиться, что в твоей системе есть статическая версия PCRE. Традиционное расширение файлов статических библиотек — `.a`. В системах на основе Debian большинство пакетов с библиотеками содержат и статическую, и динамическую версии. В системах семейства Red Hat статические версии библиотек часто живут в отдельных пакетах. На моей Fedora перед сборкой этого примера нужно выполнить `sudo dnf install glibc-static pcre-static`.

Тонкий момент — при статической сборке порядок следования аргументов становится важным и файл с программой должен идти до всех библиотек. Наивное добавление опции `-static` к прошлой команде вызовет ошибку.

`$ gcc -o match -static -lpcre ./match.c` /usr/bin/ld: /tmp/ccdiyqgc.o: in function 'main': match.c:(.text+0x72): undefined reference to 'pcre_compile' /usr/bin/ld: match.c:(.text+0xae): undefined reference to 'pcre_exec' collect2: error: ld returned 1 exit status

Однако и команда `gcc ./match.c -o match -static -lpcre` будет не совсем верной. При динамической компоновке GCC (вернее, GNU ld, которую он вызвал) сам разобрался, что libpcre зависит от libpthread (библиотеки POSIX threads). Теперь же нам придется указать ее вручную. Но с верным набором и порядком опций мы наконец получим статический исполняемый файл.

`$ gcc ./match.c -o match -static -lpcre -lpthread` `$ file ./match` ./match: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=3f816f2c426e417b27de3ac2cf2cfc28ad989889, for GNU/Linux 3.2.0, not stripped, too many notes (256) `$ ldd ./match` not a dynamic executable

### Статическая GNU libc и NSS

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

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

Однако во встраиваемых системах и образах initrd этот нюанс придется учитывать. Я сам узнал об этом, когда собрал образ initrd для загрузки по сети через PXE и обнаружил, что Wget из BusyBox работает только по IP и не способен разрешать имена хостов.

Если GNU libc в целевой системе нет, то нужно использовать альтернативную стандартную библиотеку языка C.

### Альтернативные библиотеки

Одна из самых популярных альтернативных библиотек — musl. Ее использует Alpine Linux. Кроме того, некоторые компиляторы других языков предоставляют опцию связывания с ней вместо GNU libc. Такая опция есть в Rust и OCaml.

И в Debian, и в Fedora с версии 32 присутствует пакет `musl-gcc`. Он предоставляет обертку к GCC, которая автоматически использует musl вместо GNU libc. Использование не отличается от обычного GCC. Простейший пример:

    musl-gcc -static -o hello ./hello.c

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

## Заключение

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

#### Оцени статью:

###### Что тебе понравилось больше всего?

Статья легкая и понятная Статья глубокая и подробная Интересуюсь этой темой Данные новые и актуальные Приятный стиль изложения

###### Что тебе не понравилось больше всего?

Статья поверхностная Статья слишком сложная Тема мне не интересна Данные устарели Неприятный стиль изложения
← Ранее Citrix ожидает атак на свежие проблемы в XenMobile
Далее → Обнаружена хак-группа RedCurl, промышляющая корпоративным шпионажем
4 комментария к записи Полная совместимость. Как работают статические исполняемые файлы в Linux

#### Последние взломы

### HTB Driver. Эксплуатируем PrintNightmare и делаем вредоносный SCF

### Routing nightmare. Как пентестить протоколы динамической маршрутизации OSPF и EIGRP

### HTB Bolt. Эксплуатируем шаблоны в Python и разбираемся с PGP

### Краденые миллиарды и плохой рэп. Как арестовали супругов, которые отмывали деньги Bitfinex и снимали тиктоки

#### Компьютерные трюки

### Поездка в Sonerezh. Поднимаем сервер для стриминга музыки

### Терминальное счастье. Колонка главреда

### Анатомия терминала. Осваиваем текстуальную графику

### Змей-стражник. Сигнализация для велосипеда из старого смартфона на Android

#### Свежие новости

- 14 часов назад

  ### Внутренние чаты хак-группы Conti слили в открытый доступ

- 16 часов назад

  ### Toyota закрыла производство в Японии после взлома одного из поставщиков

- 17 часов назад

  ### Microsoft: малварь FoxBlade атаковала Украину до начала военных действий

- 18 часов назад

  ### Хакеры, взломавшие Nvidia, выставили на продажу инструмент для обхода занижения хэшрейта

- 2 дня назад

  ### Вымогатели выбирают стороны: одни поддерживают Россию, другие Украину

Вопросы по материалам и подписке: support@glc.ru
Отдел рекламы и спецпроектов: yakovleva.a@glc.ru
Контент 18+
Сайт защищен Qrator —
самой забойной защитой от DDoS в мире
Подписка для физлиц
Подписка для юрлиц
Реклама на «Хакере»
Контакты
Posting....
wpDiscuz
FooBox
…