Почему Linux-бинарный файл не запускается на сервере
Ошибка GLIBC_2.34 not found возникает из-за несовместимости версий библиотек. Узнайте, как проверить требования бинарного файла и выбрать один из четырех способов его переноса.
Почему Linux-бинарный файл не запускается на старом сервере
Создать переносимый Linux-бинарный файл сложно, так как glibc (библиотека GNU C) гарантирует совместимость только в одном направлении. Старый бинарный файл продолжает работать с новой версией glibc. Новый бинарный файл не работает со старой версией glibc. Машина, на которой вы выполняете компиляцию, определяет минимальные требования для всех машин, на которые вы будете развертывать ПО.
Ошибка указывает на точную версию, которая потребовалась системе:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)Файл не поврежден, и конфигурация не нарушена. Динамический загрузчик считал версию, записанную внутри бинарного файла, выполнил поиск этой версии в системной библиотеке libc, не нашел ее и отказался запускать программу. Повторная загрузка файла и выполнение chmod +x ничего не изменят, так как требование прописано в самом файле. Вам необходимо либо изменить способ сборки бинарного файла, либо изменить то, что вы поставляете.
Как на самом деле работает версионирование символов в glibc
Каждая функция, экспортируемая glibc, имеет тег версии. printf внутри libc.so.6 — это на самом деле printf@@GLIBC_2.2.5. Когда glibc меняет поведение или ABI (интерфейс двоичного уровня) функции, она не заменяет старую версию. Она сохраняет старый код под старым тегом и добавляет новый код под новым тегом, поэтому один libc.so.6 содержит сразу несколько версий одного и того же символа. Бинарный файл 2009 года находит тег 2009 года на месте, именно поэтому прямая совместимость работает так хорошо.
Этап компоновки фиксирует использованные версии. Ваш бинарный файл получает секцию .gnu.version_r, которая по сути говорит: «Мне нужна GLIBC_2.38 из libc.so.6». В старой версии libc этого тега никогда не было, поэтому загрузчик останавливается до того, как main начнет выполнение. Механизма отката не существует, так как откат означал бы скрытую подмену функции на ту, для которой программа не была скомпилирована.
Самая частая причина проблем с 2021 года — это glibc 2.34. В этом релизе libpthread и libdl были объединены в libc, а __libc_start_main, функция, запускающая любую программу на C, была перенесена в тег GLIBC_2.34. Программа «hello-world», скомпилированная в любом дистрибутиве с glibc 2.34 или новее, требует GLIBC_2.34, даже если исходный код не использует ничего современного. Именно поэтому проблема внезапно возникла у тех, чей код не менялся годами.
Какая версия glibc используется в моем дистрибутиве?
The data behind this chart
[
{
"distro": "CentOS 7",
"glibc": 2.17
},
{
"distro": "RHEL 8",
"glibc": 2.28
},
{
"distro": "Ubuntu 20.04",
"glibc": 2.31
},
{
"distro": "Debian 11",
"glibc": 2.31
},
{
"distro": "RHEL 9",
"glibc": 2.34
},
{
"distro": "Ubuntu 22.04",
"glibc": 2.35
},
{
"distro": "Debian 12",
"glibc": 2.36
},
{
"distro": "Ubuntu 24.04",
"glibc": 2.39
},
{
"distro": "Debian 13",
"glibc": 2.41
}
]Это версии, которые каждый дистрибутив включает в свои репозитории согласно данным на август 2026 года. Срок поддержки CentOS 7 давно истек, но он остается в списке, так как на унаследованных серверах он все еще используется. Проверьте любой сервер с помощью ldd --version, которая выводит версию glibc в первой строке, или с помощью getconf GNU_LIBC_VERSION.
Рассматривайте эти 9 строк как лестницу. Если вы собираете проект на Debian 13 с glibc 2.41, результат не будет работать ни на чем старше Debian 13. Если вы собираете проект на Ubuntu 20.04 с glibc 2.31, тот же исходный код будет работать на всех строках выше этой, включая Debian 13. Самый старый сервер, который вы должны поддерживать, — это единственный важный хост для сборки, поэтому дистрибутив, на котором вы стандартизируетесь, определяет нижний порог ABI для всего, что вы для него компилируете. Это стоит решить до того, как парк серверов будет развернут, наряду с другими компромиссами при выборе ОС для вашего VPS.
Как узнать минимальную версию glibc, необходимую для бинарного файла?
Прочитайте это значение из самого файла. Этот способ работает с любыми файлами, включая бинарные файлы от поставщиков, к которым не прилагаются заметки о сборке.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5Последняя строка указывает на минимально допустимую версию. Если objdump отсутствует, установите пакет binutils. Если на сервере нельзя ничего устанавливать, strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 даст приблизительный результат, так как теги хранятся внутри файла в виде обычных строк.
readelf -V ./mytool показывает те же требования в структурированном виде. Ищите блок с заголовком Version needs section '.gnu.version_r'. В нем перечислена одна строка Name: GLIBC_x.y для каждого тега, сгруппированная по библиотеке, которая должна его предоставлять.
Чтобы выяснить, какая именно функция задает минимальную версию, выполните поиск тега через grep: objdump -T ./mytool | grep GLIBC_2.38. Часто это один конкретный символ, и иногда его можно избежать. Если objdump -T не выводит ничего, значит, бинарный файл скомпилирован статически. У него нет таблицы динамических символов и требований к версиям библиотек.
Что file и readelf -d сообщают о бинарном файле, который вам передали
file выводит архитектуру и тип линковки в одну строку.
file ./mytoolОбычная сборка glibc выглядит так:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, not strippedСтатическая сборка содержит statically linked и не указывает интерпретатор. Сборка на musl указывает другой:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...Интерпретатор — это поле, которое имеет значение. Это динамический загрузчик, который ядро запускает перед вашей программой, и этот конкретный путь должен существовать на целевой машине. /lib/ld-musl-x86_64.so.1 отсутствует на сервере Debian или Ubuntu, если только кто-то не установил там musl.
readelf -d ./mytool перечисляет общие библиотеки, которые требует бинарный файл, по их именам:
Dynamic section at offset 0x2d58 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libssl.so.3]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]Каждая запись NEEDED является жестким требованием, которое сопоставляется по soname. libssl.so.3 — это OpenSSL 3, поэтому данный бинарный файл не загрузится на сервере, где есть только libssl.so.1.1, и причина именно в soname: разный soname означает намеренно несовместимый ABI. RUNPATH указывает загрузчику, где искать в первую очередь, а $ORIGIN раскрывается в директорию, содержащую бинарный файл; именно так самодостаточный пакет находит свои библиотеки.
Не запускайте ldd для бинарного файла, которому вы не доверяете. В glibc ldd может разрешать зависимости, запуская программу под управлением загрузчика, поэтому вредоносный файл получает возможность выполнить код. readelf и objdump только читают байты. Прежде чем делать что-либо из этого, убедитесь, что загруженный файл — это именно тот файл, который опубликовал проект, поскольку проверка загрузки по опубликованной контрольной сумме — это единственный этап, который позволяет понять, чьи именно байты находятся у вас в руках.
Ошибки, с которыми вы столкнётесь, и их значение
Загрузчик указывает на версию GLIBC, которую не удаётся найти. Бинарный файл был скомпилирован с более новой версией glibc, чем установлена в целевой системе. Безопасных способов исправить это установкой пакетов в целевой системе нет. Выберите один из четырёх вариантов ниже.
Оболочка сообщает No such file or directory для файла, который вы видите. Отсутствует не сам бинарный файл, а интерпретатор. execve возвращает ENOENT, если путь к загрузчику в заголовке ELF отсутствует, и оболочка выводит единственное доступное ей сообщение. Выполните file и прочитайте путь к интерпретатору. Бинарный файл, скомпилированный с musl, на сервере только с glibc даёт именно такой симптом.
cannot execute binary file: Exec format error означает, что архитектура не совпадает: бинарный файл x86-64 на сервере arm64 или наоборот. Первая строка вывода file покажет, какой именно файл у вас в руках.
error while loading shared libraries: libssl.so.3: cannot open shared object file означает, что библиотека NEEDED отсутствует или присутствует под другим soname. Установите соответствующий пакет из дистрибутива. Имена пакетов различаются в разных семействах ОС, поэтому адаптируйте их перед копированием строки apt из README, а эквиваленты команд dnf и apt помогут с этим соответствием.
Простая ошибка Segmentation fault при сборке с musl, которая нормально работает в glibc. Обычно это связано с размером стека потока, что описано в варианте 2.
Четыре способа распространения переносимого бинарного файла Linux
Каждое решение имеет свою цену. Выбирайте способ, исходя из того, что именно программа делает во время выполнения, а не из того, какой вариант кажется наиболее элегантным.
Вариант 1: сборка на самом старом дистрибутиве, который вы поддерживаете
Это самый простой и обычно наиболее правильный подход. Выполняйте компиляцию внутри образа контейнера с самым старым дистрибутивом, который вы обещаете поддерживать. Компоновщик может записывать только те теги, которые реально существуют в старой версии glibc, поэтому минимально требуемая версия опускается до этого релиза, а бинарный файл остается обычным динамическим исполняемым файлом со всеми возможностями glibc.
docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'Полученный результат будет работать на glibc 2.31 и всех последующих версиях. Проверяйте, а не предполагайте: выполните однострочник objdump -T, приведенный ранее, для полученного файла и убедитесь, что самый старший тег соответствует ожидаемому.
Цена этого подхода — инструментарий сборки (toolchain). Старый базовый образ также содержит старый компилятор, что создает проблемы, если вашему коду требуется современный стандарт C++. Go и Rust в основном избегают этой проблемы, так как их инструментарий устанавливается в контейнер независимо от пакетов дистрибутива. Для C и C++ вы можете добавить более новый компилятор из репозиториев дистрибутива или использовать образы manylinux, которые существуют именно для того, чтобы объединить древнюю glibc с актуальным GCC. Встроенный C-компилятор Zig также может напрямую нацеливаться на выбранную версию glibc, как показано в zig cc -target x86_64-linux-gnu.2.28, что позволяет достичь того же минимального порога без необходимости хранить старый образ.
Вариант 2: статическая линковка с musl
musl — это компактная библиотека C, разработанная с расчетом на статическую линковку. Статический бинарный файл musl содержит собственную libc, не указывает интерпретатор и запускается на любом ядре Linux подходящей архитектуры. Именно так собирается большинство инструментов, распространяемых в виде одного файла.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile теперь должен выводить statically linked без поля интерпретатора. Для Rust добавьте целевую платформу и выполните сборку для неё:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslДля Go это не требуется. При использовании CGO_ENABLED=0 go build результат уже является статическим и не линкуется с libc вовсе.
Изменения в поиске NSS. glibc разрешает имена пользователей и хостов через NSS (name service switch), который загружает модули libnss_* с помощью dlopen во время выполнения программы. Статически слинкованная glibc так не умеет, о чем линковщик предупреждает следующими словами:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingmusl обходит это предупреждение, не реализуя NSS вовсе. У библиотеки собственный резолвер, который напрямую читает /etc/resolv.conf и /etc/hosts. На обычном VPS это приемлемо и даже проще. На хосте, где учетные записи или имена приходят из LDAP или SSSD, ваш бинарный файл не увидит их, в то время как все остальные программы на сервере будут работать корректно. Резолвер musl также моложе, чем у glibc: TCP-резервирование для DNS-ответов размером более 512 байт появилось в musl 1.2.4 в 2023 году, поэтому сборки с более старыми версиями musl обрезают длинные ответы.
dlopen не работает. В статическом бинарном файле musl функция dlopen является заглушкой, которая всегда возвращает ошибку. Любая загрузка кода во время выполнения невозможна: системы плагинов, модули PAM, драйверы GPU, собственные модули кодировок iconv из glibc. Если вашей программе требуется dlopen, статическая линковка не подходит, и вам нужно выбрать один из трех других вариантов.
Обновления безопасности становятся вашей задачей. Динамически слинкованный бинарный файл получает исправления libc в момент обновления пакетов на сервере. Статический бинарный файл — никогда. Когда появляется запись CVE (common vulnerabilities and exposures) для вашей libc или статически слинкованной OpenSSL, которую вы включили в состав, вам нужно пересобрать и перераспределить файл. Все уже развернутые копии остаются уязвимыми, пока кто-то не заменит файл вручную. Ведите учет того, что именно вы слинковали, так как на целевой системе нет инструментов, которые могли бы сообщить администратору, что ваш бинарный файл содержит библиотеку двухлетней давности.
Изменения в лицензировании. glibc распространяется под лицензией LGPL, и статическая линковка накладывает обязательство по перелинковке: вы должны предоставить получателям всё необходимое для перелинковки вашей программы с другой версией glibc. musl распространяется под лицензией MIT и не содержит подобных условий. Это главная причина, по которой проекты, поставляющие бинарные файлы в виде одного файла, выбирают musl, а не статическую glibc.
Два различия в среде выполнения приводят к запутанным сбоям. Размер стека потока по умолчанию в musl составляет 128 KiB против 8 MiB в glibc, поэтому код, размещающий большой буфер в стеке потока, вызывает segfault без сообщений и записей в логах. Установите размер явно с помощью pthread_attr_setstacksize или перенесите буфер в кучу (heap). Аллокатор musl также написан с расчетом на малый размер и предсказуемое поведение, а не на большое количество потоков, выполняющих аллокацию одновременно, поэтому многопоточные программы с интенсивным выделением памяти могут работать заметно медленнее. Протестируйте собственную нагрузку, вместо того чтобы полагаться на репутацию той или иной библиотеки.
Вариант 3: упаковка загрузчика и библиотек
Разместите библиотеки рядом с бинарным файлом вместе с соответствующим загрузчиком, а затем запускайте программу через этот загрузчик.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolЧтобы сделать это постоянным, запишите пути в файл с помощью patchelf:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolУспех этого метода зависит от одного правила: загрузчик и libc.so.6 должны быть из одной сборки glibc. Это согласованная пара, и смешивание системного загрузчика с упакованной библиотекой libc приведет к аварийному завершению при запуске, а не к понятному сообщению об ошибке. Упаковывайте либо оба компонента, либо ни одного.
AppImage — это реализация данного шаблона, где полезная нагрузка находится в образе squashfs, а небольшая среда выполнения монтирует его. У него есть одна особенность, которую часто упускают из виду: AppImage не включает в себя glibc, поэтому AppImage, собранный в актуальном дистрибутиве, все равно выдаст ту же ошибку версии на старом сервере. Рекомендация разработчиков AppImage — выполнять сборку на самой старой базовой системе, которую вы поддерживаете. Таким образом, это формат доставки, надстроенный над вариантом 1, а не его замена.
На сервере без графической оболочки для монтирования полезной нагрузки AppImage требуется FUSE (filesystem in userspace), который часто отсутствует в минимальных образах VPS. Ошибка проявляется как libfuse.so.2. Вы можете полностью пропустить этап монтирования с помощью ./App.AppImage --appimage-extract-and-run: в этом случае содержимое распаковывается во временный каталог и выполняется оттуда.
Вариант 4: поставка образа контейнера
Переместите всё пользовательское окружение вместе с программой. Образ содержит собственную библиотеку libc, поэтому версия glibc на хосте перестаёт иметь значение — важны только ядро и архитектура процессора. Это наименее изящный вариант, который полностью устраняет целый класс проблем, поэтому именно так распространяется большая часть серверного ПО. Запуск Docker на VPS — стандартный способ использования такого подхода.
Ядро хоста по-прежнему накладывает ограничения. Более новые версии glibc используют новые системные вызовы, которые старый хост контейнеров может блокировать: glibc 2.34 и новее используют clone3, которые отвергаются старыми профилями seccomp по умолчанию. Симптомом является немедленный сбой с ошибкой Operation not permitted, при этом упоминания glibc в логах отсутствуют. Обновление среды выполнения контейнеров (container runtime) на хосте решает проблему, так как блокировка находится в фильтре системных вызовов самой среды, а не в ядре.
Издержки стандартны. Целевой системе требуется среда выполнения контейнеров и права на её использование. Размер загружаемого файла увеличивается с одного исполняемого файла до десятков или сотен мегабайт. Теперь вы отвечаете за базовый образ и график его обновлений, поэтому работа по обеспечению безопасности, которой вы избежали в варианте 2, возвращается в виде пересборки образов. Для долгоживущего сервиса это оправданный компромисс. Для консольной утилиты, которую запускают один раз, — нет.
Какой вариант выбрать?
- Внутренний инструмент для серверов под вашим управлением, работающих на одном дистрибутиве: собирайте динамически в этом дистрибутиве и на этом остановитесь.
- Единый файл, который скачивают и запускают сторонние пользователи: используйте статическую сборку musl, если программе не требуется
dlopenи не нужны NSS-запросы. - Программа с плагинами или доступом к GPU: используйте динамическую сборку, собирайте на старой базовой системе и используйте вариант 3 для доставки.
- Долгоживущий сервис на машине, где уже есть среда выполнения: поставляйте образ.
Что бы вы ни выбрали, задокументируйте это и проверяйте. Версия glibc на сборочном узле теперь является частью вашего процесса выпуска, и обновление сборочной машины с одного LTS-релиза на другой незаметно повышает требования к системе, что приводит к сбоям у пользователей, у которых всё работало в прошлом месяце. Фиксируйте образ сборки по тегу и добавьте проверку максимального тега GLIBC_ в выходном файле как этап сборки. Тогда проверка завершится ошибкой в вашем конвейере, а не в терминале у другого пользователя.
FAQ
Почему на сервере возникает ошибка "version GLIBC_2.38 not found"?
Бинарный файл был скомпилирован на машине с более новой версией glibc, чем установлена на вашем сервере. Версионирование символов в glibc обеспечивает только прямую совместимость: старые бинарные файлы работают на новой glibc, а новые — нет, так как загрузчик требует точный тег версии, записанный в файле, а в старой версии libc этого тега не существует. Никакие действия по установке пакетов на сервере не решат эту проблему безопасно. Пересоберите программу на более старой базовой системе, поставляйте статическую сборку на базе musl, упакуйте библиотеки вместе с соответствующим загрузчиком или используйте контейнерный образ.
Как узнать, какая версия glibc требуется бинарному файлу?
Прочитайте теги версий из файла с помощью objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. Последняя строка указывает минимальную версию glibc, необходимую для загрузки файла. readelf -V ./mytool показывает те же требования в секции .gnu.version_r, сгруппированные по библиотекам, которые должны их предоставлять. Если objdump -T ничего не выводит, значит, бинарный файл является статическим и не имеет зависимостей от glibc. Сравните результат с версией glibc на вашем сервере, которую можно узнать через ldd --version.
Работает ли бинарный файл на musl медленнее, чем на glibc?
Это зависит от нагрузки, поэтому честный ответ — нужно проводить измерения. Статический бинарный файл на musl запускается быстрее, так как не требуется работа загрузчика и перемещение адресов во время выполнения exec. С другой стороны, аллокатор musl оптимизирован для компактности и предсказуемого поведения, а не для работы с множеством потоков, одновременно выполняющих выделение памяти. Кроме того, в musl меньше оптимизированных вручную процедур для работы со строками и памятью, чем в glibc, поэтому многопоточные программы с интенсивным выделением памяти или обработкой строк могут работать заметно медленнее. Протестируйте свою программу на своем сервере, прежде чем делать выводы.
Почему bash выдает "No such file or directory" для существующего файла?
Отсутствующий файл — это динамический загрузчик, а не ваш бинарный файл. Ядро считывает путь к интерпретатору из заголовка ELF и возвращает ENOENT, если этот путь отсутствует, а оболочка сообщает об этом единственным доступным ей сообщением. Запустите file ./mytool и прочитайте поле interpreter. Если на сервере с Debian или Ubuntu там указано /lib/ld-musl-x86_64.so.1, значит, вы пытаетесь запустить бинарный файл, скомпилированный для musl, в системе с glibc. Вам потребуется версия программы, совместимая с musl, или статическая сборка.
Можно ли скопировать libc.so.6 с более нового сервера, чтобы исправить это?
Нет. libc.so.6 и ld-linux-x86-64.so.2 представляют собой согласованную пару из одной сборки glibc, и каждый процесс в системе использует их. Перезапись системной копии может привести к тому, что на сервере перестанет работать всё, включая инструменты, необходимые для отмены изменений. Если вам необходимо запустить один новый бинарный файл на старом хосте, распакуйте более новую версию glibc в отдельный каталог и запускайте программу через её собственный загрузчик с помощью --library-path — это затронет только данный процесс. Пересборка программы с использованием старой версии glibc остается единственным решением, которое не создаст проблем в будущем.