SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Почему 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)

Файл не поврежден, и обычно проблема не в конфигурации. Единственное исключение — настройка LD_LIBRARY_PATH, которая направляет загрузчик к более старой версии libc, а путь в сообщении показывает, какую именно библиотеку он прочитал. Динамический загрузчик считал тег версии, записанный внутри бинарного файла, попытался найти этот тег в указанной библиотеке 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 используется в моем дистрибутиве?

Графикglibc version in each distribution's own repositories
Данные, на которых построен график
[
  {
    "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 добавляет директории, в которых загрузчик ищет прямые зависимости бинарного файла, после любых настроек LD_LIBRARY_PATH, но перед системным кэшем библиотек и путями по умолчанию. $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 покажет архитектуру файла или сообщит, что файл не является пригодным для использования ELF-исполняемым файлом.

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 ./hello

file теперь должен выводить 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 linking

musl обходит это предупреждение, так как вообще не реализует NSS. У библиотеки собственный резолвер, который напрямую читает /etc/resolv.conf и /etc/hosts. На обычном VPS это приемлемо и даже проще. Однако на хосте, где учетные записи или имена приходят из LDAP или SSSD, ваш бинарный файл не увидит их, в то время как остальные программы на сервере будут работать корректно. Резолвер musl также моложе, чем у glibc: поддержка TCP-резервирования для DNS-ответов, которые слишком велики для UDP, появилась в musl 1.2.4 в 2023 году. Сборки с более старыми версиями musl не могут запрашивать большие DNS-записи, и их поиск может завершаться ошибкой при работе с рекурсивными DNS-серверами, которые не возвращают частичные результаты в усеченных UDP-ответах.

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. glibc берет значение по умолчанию из лимита стека, с которым была запущена программа (обычно от 2 до 10 MB), и использует 2 MB на большинстве архитектур, если лимит не ограничен. Код, который размещает большой буфер в стеке потока, может вызвать segfault в musl без какого-либо сообщения или записи в логах. Устанавливайте размер явно с помощью 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 гласит: собирайте проект на базе, которая не новее самой старой поддерживаемой вами системы. Это делает такой AppImage форматом доставки, надстроенным над вариантом 1, а не его заменой. Упаковка glibc также возможна: appimage-builder и go-appimage позволяют включить glibc внутрь AppImage, и такой пакет может работать как на старых, так и на новых дистрибутивах. Платой за это является увеличенный размер файла и библиотеки вроде libssl, «замороженные» внутри пакета.

На сервере без графической оболочки для монтирования полезной нагрузки AppImage требуется FUSE (filesystem in userspace), а минимальные образы VPS часто его не содержат. AppImage, собранные со старой средой выполнения, требуют наличия библиотеки FUSE 2 в системе, которая в Debian и Ubuntu упакована как libfuse2, а в Ubuntu 24.04 переименована в libfuse2t64. При её отсутствии работа программы прерывается с ошибкой AppImages require FUSE to run.. Более новые AppImage имеют встроенную поддержку FUSE 3 и больше не требуют установки libfuse в системе. Вы можете полностью пропустить этап монтирования с помощью ./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 запускается быстрее, так как не требуется работа загрузчика и перемещение адресов во время выполнения. С другой стороны, аллокатор 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 из дистрибутива (пакет musl в Debian и Ubuntu) или используйте сборку программы для glibc либо статическую сборку.

Можно ли скопировать libc.so.6 с более нового сервера, чтобы исправить это?

Нет. libc.so.6 и ld-linux-x86-64.so.2 представляют собой согласованную пару из одной сборки glibc, и каждая динамически скомпилированная программа на сервере использует их. Перезапись системной копии может привести к тому, что на сервере перестанет работать практически всё, включая инструменты, необходимые для отмены изменений. Если вам необходимо запустить один новый бинарный файл на старом хосте, распакуйте новую версию glibc в отдельный каталог и запускайте программу через её собственный загрузчик с помощью --library-path — это затронет только данный процесс. Пересборка программы под старую версию glibc остается единственным надежным решением, которое не создаст проблем в будущем.

#glibc#musl#static-linking#portability#binaries