Linuxバイナリが動かない原因はglibcとmuslの違いです
「GLIBC_2.34 not found」は、ビルド環境が必要なglibcの最低バージョンを決めたサインです。バイナリの要求を確認し、移植性を高める4つの配布方法を解説します。
古いサーバーで Linux バイナリが実行できない理由
移植性の高い Linux バイナリを作るのは困難です。glibc(GNU C library)は、互換性を一方向にしか保証しないためです。古いバイナリは新しい glibc 上で実行できます。新しいバイナリは古い glibc 上では実行できません。コンパイルに使用したマシンが、デプロイ先のすべてのマシンに必要な最低バージョンを決めます。
エラーには、必要だった正確なバージョンが示されます。
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)ファイルが破損しているわけではなく、通常は設定ミスでもありません。例外は、ローダーを古い libc に向ける LD_LIBRARY_PATH 設定です。メッセージ内のパスから、実際に読み込まれた libc を確認できます。動的ローダーは、バイナリ内部に記録されたバージョンタグを読み取り、そのタグをその libc 内で探します。タグが見つからないため、プログラムの起動を拒否します。ファイルを再ダウンロードして chmod +x を実行しても、何も変わりません。必要なバージョンはファイル自体に記録されているためです。バイナリのビルド方法を変更するか、配布するファイルを変更する必要があります。
glibc のシンボルバージョニングが実際に行うこと
glibc がエクスポートするすべての関数には、バージョンタグが付いています。printf 内の libc.so.6 は、実際には printf@@GLIBC_2.2.5 です。glibc が関数の動作や ABI(アプリケーションバイナリインターフェイス)を変更した場合、古い関数を置き換えることはありません。古いコードを古いタグのまま保持し、新しいコードを新しいタグで追加します。そのため、1 つの libc.so.6 に同じシンボルの複数のバージョンが同時に存在します。2009 年のバイナリは、2009 年のタグが残っていることを確認できます。これが、フォワード互換性が非常に高い理由です。
リンク処理では、使用した内容が記録されます。バイナリには .gnu.version_r セクションが含まれ、実質的に「libc.so.6 から GLIBC_2.38 が必要」と示します。古い libc にはそのタグが存在しないため、ローダーは main の実行前に停止します。フォールバックはありません。フォールバックを許可すると、コンパイル時に使用した関数とは異なる関数が、気付かないうちにプログラムへ渡されるためです。
2021 年以降に最も多いきっかけは glibc 2.34 です。このリリースでは libpthread と libdl が libc に統合され、すべての C プログラムを開始する関数である __libc_start_main が GLIBC_2.34 タグへ移動しました。そのため、glibc 2.34 以降を搭載したディストリビューションでコンパイルした hello-world は、ソースコードが最新の機能を何も呼び出していなくても GLIBC_2.34 を要求します。これが、独自のコードを何年も変更していない人々の環境で、この問題が突然発生した理由です。
ディストリビューションが提供する glibc のバージョンはどれですか?
このグラフのデータ
[
{
"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 年 8 月時点の内容です。CentOS 7 はサポート終了から長期間経過していますが、既存のサーバーで引き続き稼働しているため一覧に残しています。使用している環境で ldd --version を実行すると、1 行目に glibc のリリースが表示されます。getconf GNU_LIBC_VERSION でも確認できます。
この 9 行は、段階表として読み取ってください。glibc 2.41 の Debian 13 上でビルドすると、その結果は Debian 13 より古い環境では実行できません。glibc 2.31 の Ubuntu 20.04 上でビルドすると、同じソースはその行より上にあるすべての環境で実行でき、Debian 13 の環境も含まれます。サポート対象にする最も古いサーバーが、考慮すべき唯一のビルドホストです。そのため、標準化するディストリビューションによって、そこでコンパイルするすべてのソフトウェアの ABI の下限が決まります。これは、フリートを構築する前に、VPS で実行する OS の選択に伴う他のトレードオフと合わせて決めておく価値があります。
バイナリが必要とする 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 を使うと、アーキテクチャとリンク方式を 1 行で確認できます。
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, ...重要なのはインタープリターの項目です。これはプログラムの実行前にカーネルが起動する動的ローダーであり、その正確なパスが対象マシン上に存在している必要があります。Debian または Ubuntu のサーバーでは、musl をインストールしていない限り /lib/ld-musl-x86_64.so.1 は存在しません。
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 を前提にコンパイルされています。対象環境に何かをインストールしても、安全には解決できません。以下の4つの選択肢から1つを選んでください。
明らかに存在するファイルに対して、シェルが No such file or directory と表示する。 不足しているのはバイナリではなくインタープリターです。ELF ヘッダーにローダーのパスがない場合、execve は ENOENT を返し、シェルは取得できる唯一のメッセージを表示します。file を実行し、インタープリターのパスを確認してください。musl にリンクされたバイナリを glibc 専用のサーバーで実行すると、まったく同じ症状になります。
cannot execute binary file: Exec format error は通常、アーキテクチャが一致していないことを示します。たとえば、arm64 サーバー上で x86-64 バイナリを実行している場合や、その逆の場合です。また、ファイルが有効な実行ファイルではない場合もあります。ダウンロードが途中で切れた場合や、破損している場合などです。file の出力の1行目を見ると、手元のファイルのアーキテクチャ、または使用可能な ELF 実行ファイルではないことを確認できます。
error while loading shared libraries: libssl.so.3: cannot open shared object file は、NEEDED ライブラリがないか、別の soname で存在することを示します。一致するディストリビューションパッケージをインストールしてください。パッケージ名はディストリビューションの系統によって異なります。そのため、README から apt の行をコピーする前に読み替えてください。dnf と apt のコマンド対応で対応関係を確認できます。
glibc では正常に動作する musl ビルドから、単独で Segmentation fault が表示される。 通常はスレッドのスタックサイズが原因です。これは選択肢2で説明しています。
移植可能な Linux バイナリをリリースする4つの方法
どの方法にも、実際のコストが伴います。見た目の簡潔さではなく、プログラムが実行時に何を行うかを基準に選択してください。
対応対象とする最も古いディストリビューション上でビルドする
最も地味ですが、通常はこれが正解です。サポート対象として保証する最も古いディストリビューションのコンテナイメージ内でコンパイルします。リンカーが記録できるのは、実際に使用した古い 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 のワンライナーを結果に対して実行し、最も高いタグが想定したものになっていることを確認します。
コストはツールチェーンです。古いベースイメージには古いコンパイラも含まれるため、コードで新しい C++ 標準が必要になると問題になります。Go と Rust では、ツールチェーンがディストリビューションのパッケージとは独立してコンテナ内にインストールされるため、この問題をほぼ回避できます。C と C++ では、ディストリビューション独自のツールチェーンチャネルから新しいコンパイラを追加できます。または、古い glibc と最新の GCC を組み合わせるために存在する manylinux イメージを使用できます。Zig に付属する C コンパイラも、zig cc -target x86_64-linux-gnu.2.28 のように指定した glibc を直接ターゲットにできます。これにより、古いイメージを維持せずに同じ下限を実現できます。
オプション 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-muslGo では、これらの作業は必要ありません。CGO_ENABLED=0 go build を指定すると、結果はすでに静的で、libc には一切リンクしません。
NSS の検索動作は変わります。 glibc は NSS(name service switch)を通じてユーザーとホスト名を解決します。実行中に dlopen を使って libnss_* モジュールを読み込みます。静的リンクした 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 のものより新しいため、UDP 応答に収まらない大きな DNS 応答に対する TCP フォールバックは、2023 年の musl 1.2.4 で追加されました。古い musl に対してビルドしたプログラムでは、大きな DNS レコードを検索できません。また、切り詰めた UDP 応答で部分的な結果を返さない再帰 DNS サーバーに対しては、検索に失敗することがあります。
dlopen は動作しません。 静的な musl バイナリでは、dlopen は常に失敗するスタブです。実行時にコードを読み込むものは利用できません。対象には、プラグインシステム、PAM モジュール、GPU ドライバー、glibc 独自の iconv 文字セットモジュールなどがあります。プログラムが dlopen を必要とする場合、静的リンクは選択できません。残り 3 つの方法のいずれかを使う必要があります。
セキュリティ更新は自分で管理する必要があります。 動的リンクしたバイナリは、サーバーでパッケージ更新を実行すると、libc の修正を直ちに取り込めます。静的バイナリは取り込みません。libc または組み込んだ静的 OpenSSL に対して CVE(common vulnerabilities and exposures)の登録が行われた場合は、自分で再ビルドして再配布する必要があります。すでに配置した各コピーは、誰かがファイルを置き換えるまで脆弱なままです。何をリンクしたかを記録してください。対象システム上の情報だけでは、管理者はその単一ファイルのバイナリに 2 年前のライブラリが含まれていることを確認できません。
ライセンスも変わります。 glibc は LGPL であり、静的リンクすると LGPL の再リンク義務が発生します。受け取った人が、別の glibc に対してプログラムを再リンクするために必要なものを提供しなければなりません。musl は MIT で、このような条件はありません。単一ファイルのバイナリを配布するプロジェクトが、静的な glibc ではなく musl を選ぶ主な理由はこれです。
2 つのランタイム差異により、原因の分かりにくいクラッシュが発生します。 musl のデフォルトのスレッドスタックは 128 KiB です。glibc は、プログラムの起動時に設定されていたスタック制限からデフォルト値を決めます。通常は 2~10 MB になり、その制限が無制限の場合は、ほとんどのアーキテクチャで 2 MB にフォールバックします。そのため、スレッドスタック上に大きなバッファーを確保するコードは、musl ではメッセージやログ行を出さずに segfault することがあります。pthread_attr_setstacksize でサイズを明示するか、バッファーをヒープへ移してください。musl のアロケーターは、多数のスレッドが同時に割り当てる状況よりも、小さいサイズと予測可能な動作を重視して設計されています。そのため、割り当て処理の多いマルチスレッドプログラムでは、実行速度が明確に低下することがあります。いずれかのライブラリの評判をそのまま信じず、自分のワークロードでベンチマークしてください。
オプション3: loader と libraries を同梱する
libraries を binary の隣に、対応する loader とともに配置し、その loader 経由で program を起動します。
./ld-linux-x86-64.so.2 --library-path ./lib ./mytool設定を永続化するには、patchelf を使用して paths をファイルに記述します。
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolこれが機能するかどうかを決めるルールは1つです。loader と libc.so.6 は、同じ glibc build から取得する必要があります。これらは対応する組み合わせであり、host の loader と同梱した libc を混在させると、読み取り可能なエラーではなく起動時に crash が発生します。両方を同梱するか、どちらも同梱しないでください。
AppImage はこのパターンをパッケージ化したもので、payload を squashfs image に格納し、小さな runtime で mount します。見落とされやすい特性があります。多くの AppImage は glibc を同梱せず、host の glibc を使用します。そのため、現行の distribution 上でその方式で build した AppImage は、古い server では同じ version error で失敗します。AppImage のベストプラクティスでは、サポート対象の中で最も古い system と同じか、それより新しくない base 上で build することを推奨しています。このため、そのような AppImage はオプション1の代替ではなく、オプション1の上に重ねる delivery format です。glibc の同梱も可能です。appimage-builder と go-appimage は、どちらも AppImage 内に glibc を含めて ship できます。この方式で build した AppImage は、build system より古い distribution と新しい distribution の両方で実行できます。ただし、file size が大きくなり、libssl などの libraries が bundle 内で固定されるというコストがあります。
headless server では、AppImage が payload を mount するために FUSE (filesystem in userspace) が必要です。minimal VPS image には、FUSE が含まれていないことがよくあります。古い runtime で build した AppImage は、host に FUSE 2 library を必要とします。Debian と Ubuntu では libfuse2 としてパッケージ化され、Ubuntu 24.04 では libfuse2t64 に名前が変更されています。FUSE がない場合、AppImages require FUSE to run. で停止します。新しい AppImage には FUSE 3 support が組み込まれているため、host に libfuse を install する必要はありません。./App.AppImage --appimage-extract-and-run を使用すれば mount 自体を省略できます。この場合、payload は temporary directory に展開され、そこから実行されます。
オプション 4: コンテナイメージをリリースする
プログラムとともにユーザーランド全体を移行します。イメージには独自の libc が含まれるため、ホストの glibc に依存しなくなり、カーネルとアーキテクチャだけを一致させれば済みます。これは最も単純な選択肢であり、この問題群全体をなくせるため、多くのサーバーソフトウェアがこの形で配布されています。VPS で Docker を実行することが、通常の利用方法です。
ホストのカーネルには、引き続き制約があります。新しい glibc は新しいシステムコールを使用するため、古いコンテナホストではそれらがブロックされることがあります。glibc 2.34 以降は clone3 を使用しますが、古いデフォルトの seccomp プロファイルでは拒否されます。症状は、Operation not permitted が表示されて即座に失敗し、glbic への言及がどこにもないことです。ホスト上のコンテナランタイムを更新すれば解決します。ブロックはカーネルではなく、ランタイムのシステムコールフィルターに存在するためです。
コストは一般的なものです。対象にはコンテナランタイムと、その使用権限が必要です。ダウンロードサイズは 1 ファイルから数十または数百メガバイトに増えます。ベースイメージとそのパッチ適用スケジュールも管理する必要があるため、オプション 2 で回避したセキュリティ作業が、イメージの再ビルドという形で戻ってきます。長時間稼働するサービスであれば、妥当なトレードオフです。誰かが 1 回だけ実行するコマンドラインツールには適していません。
どの選択肢を選ぶべきか
- 自分が管理するサーバーで、すべて同じディストリビューションを実行する内部ツールの場合は、そのディストリビューション上で動的にビルドして完了とします。
- 見知らぬ利用者がダウンロードして実行する単一ファイルの場合は、プログラムが
dlopenを必要とせず、NSS に依存する名前解決も行わないなら、static musl を使用します。 - プラグインまたは GPU にアクセスするプログラムの場合は、動的リンクのまま、古いベース上でビルドし、option 3 を使って配布します。
- ランタイムがすでに存在するマシンで長時間実行するサービスの場合は、image を配布します。
どの選択肢を選んだ場合でも、記録して確認してください。ビルドホストの glibc は、リリースプロセスの一部になります。ビルドマシンをある LTS リリースから次の LTS リリースへアップグレードすると、必要な最低バージョンが気付かないうちに引き上げられ、先月までは問題なく使えていた利用者の環境で動かなくなることがあります。ビルド用 image は tag で固定し、出力に含まれる最も高い GLIBC_ tag をビルドステップで検証してください。これにより、誰かの端末で失敗する前に、パイプラインで検出できます。
FAQ
「version GLIBC_2.38 not found」とサーバーで表示されるのはなぜですか?
バイナリが、サーバーにインストールされているものより新しい glibc を持つマシンでコンパイルされています。glibc のシンボルバージョニングによる前方互換性は、古いバイナリを新しい glibc で実行できる場合に限られます。新しいバイナリを古い glibc では実行できません。ローダーはファイルに記録された正確なバージョンタグを必要としますが、古い libc にはそのタグが存在しないためです。サーバー上で何かをインストールして安全に解決することはできません。古いベースイメージで再ビルドするか、static musl ビルドを提供します。または、対応するローダーとライブラリを同梱するか、コンテナイメージを提供します。
バイナリが必要とする glibc のバージョンを確認するにはどうすればよいですか?
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 でファイルからバージョンタグを読み取ります。最後の行に、そのバイナリをロードできる最も古い glibc が示されます。readelf -V ./mytool では、.gnu.version_r セクションに同じ要件が、必要なライブラリごとにまとめて表示されます。objdump -T が何も出力しない場合、バイナリは static で、glibc をまったく必要としません。結果を、サーバー自身の ldd --version から取得したバージョンと比較します。
musl バイナリは glibc バイナリより遅いですか?
ワークロードによって異なるため、正確な答えは実測することです。static musl バイナリは、起動するローダーがなく、exec 時の再配置処理もないため、より速く起動します。一方、musl のアロケーターは、多数のスレッドが同時に割り当てを行う状況よりも、小さなサイズと予測可能な動作を重視して設計されています。また、musl に含まれる文字列・メモリ処理の手動最適化ルーチンは glibc より少ないため、割り当て処理や文字列処理が多いスレッドプログラムでは、明らかに遅くなることがあります。どちらの主張を受け入れる場合も、まず自分のプログラムを自分のサーバーでベンチマークしてください。
ファイルが存在するのに、bash が「No such file or directory」と表示するのはなぜですか?
見つからないのはバイナリではなく、dynamic loader です。カーネルは ELF ヘッダーからインタープリターのパスを読み取り、そのパスが存在しない場合に ENOENT を返します。シェルは利用できる唯一のメッセージとして、それを表示します。file ./mytool を実行し、interpreter フィールドを確認してください。Debian または Ubuntu サーバーで /lib/ld-musl-x86_64.so.1 と表示される場合、glibc システム上で musl にリンクされたバイナリを実行しようとしています。ディストリビューションの musl runtime(Debian と Ubuntu では musl パッケージ)をインストールするか、プログラムの glibc ビルドまたは static ビルドを入手してください。
新しいサーバーから libc.so.6 をコピーして、この問題を解決できますか?
いいえ。libc.so.6 と ld-linux-x86-64.so.2 は、1 つの glibc ビルドから生成された対応関係のある組み合わせです。マシン上のすべての動的リンクされた glibc プログラムがこれらを使用します。そのため、システムのコピーを上書きすると、元に戻すために必要なツールを含め、ほぼすべてのプログラムをサーバーで実行できなくなる可能性があります。古いホストで新しいバイナリを 1 つだけ実行する必要がある場合は、新しい glibc を専用ディレクトリに展開し、--library-path を使って、そのプログラムを専用ローダー経由で起動してください。この方法は、そのプロセスだけに影響します。6 か月後にも予期しない問題を起こさない方法は、古い glibc に対して再ビルドすることです。