SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-10-18

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)

通常不是檔案損毀,也不是設定錯誤。唯一的例外是 LD_LIBRARY_PATH 設定將載入器導向較舊的 libc,而訊息中的路徑會顯示實際載入的 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 區段,實際表示「我需要 libc.so.6 中的 GLIBC_2.38」。較舊的 libc 從未提供該標籤,因此載入器會在 main 執行前停止。這裡沒有 fallback,因為 fallback 代表系統會默默將與編譯時不同的函式交給程式。

自 2021 年起,最常見的觸發原因是 glibc 2.34。該版本將 libpthread 和 libdl 合併至 libc,並將 __libc_start_main(啟動每個 C 程式的函式)移至 GLIBC_2.34 標籤。只要在 glibc 2.34 或更新版本的發行版上編譯 hello-world,就會要求 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 年 8 月有效。CentOS 7 早已終止支援,但因為仍有繼承而來的伺服器在執行,因此保留在清單中。請在任何一台主機上執行 ldd --version,其第一行會顯示 glibc 版本;也可以執行 getconf GNU_LIBC_VERSION。

請將這 9 列視為一個階梯。若在 glibc 2.41 的 Debian 13 上建置,產出的程式只能在 Debian 13 或更新的環境執行。若在 glibc 2.31 的 Ubuntu 20.04 上建置,同一份原始碼產出的程式可在該列以上的所有環境執行,包括 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,並依必須提供該標記的函式庫分組。

若要找出是哪個函式集合設定了最低需求,請搜尋該標記:objdump -T ./mytool | grep GLIBC_2.38。通常只有一個符號,有時可以避開使用。如果 objdump -T 完全沒有輸出,表示二進位檔採用靜態連結,因此沒有動態符號表,也沒有可供輸出的版本需求。

file 與 readelf -d 可以告訴你別人交給你的 binary 哪些資訊

file 會在同一行顯示架構與連結類型。

file ./mytool

一般的 glibc build 會顯示如下:

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

Static build 會顯示 statically linked,且不會列出 interpreter。musl build 則會列出不同的 interpreter:

ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...

真正重要的是 interpreter 欄位。它是 kernel 在執行程式前啟動的 dynamic loader,而且該確切路徑必須存在於目標機器上。除非有人在 Debian 或 Ubuntu server 上安裝 musl,否則不會存在 /lib/ld-musl-x86_64.so.1。

readelf -d ./mytool 會依名稱列出 binary 所需的 shared libraries:

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,因此該 binary 無法在只有 libssl.so.1.1 的 server 上載入;原因就在 soname:不同的 soname 表示 ABI 刻意不相容。RUNPATH 會在任何 LD_LIBRARY_PATH 設定之後、system library cache 與預設路徑之前,加入 loader 搜尋 binary 直接相依項目的目錄。$ORIGIN 會展開為存放 binary 的目錄,讓 self-contained bundle 能找到自己的 libraries。

不要對不信任的 binary 執行 ldd。在 glibc 上,ldd 可能會透過在 loader 下執行程式來解析相依性,因此惡意檔案可能因此執行 code。readelf 與 objdump 只會讀取 bytes。在進行上述任何操作前,先確認下載的檔案確實是專案發布的檔案,因為依據專案發布的 checksum 驗證下載檔案是唯一能確認你目前持有的是誰所提供 bytes 的步驟。

實際會看到的錯誤,以及各自的含義

載入器指出它找不到的 GLIBC 版本。 該二進位檔是針對比目標環境更新的 glibc 編譯。安全的做法不是在目標環境安裝任何套件來修正。請從下列 4 個選項中選擇一個。

Shell 對明明看得到的檔案回報 No such file or directory。 缺少的是直譯器,而不是二進位檔。當 ELF 標頭中沒有載入器路徑時,execve 會回傳 ENOENT,而 shell 只能顯示這則訊息。執行 file 並查看直譯器路徑。在只有 glibc 的伺服器上執行以 musl 連結的二進位檔時,就會出現完全相同的症狀。

cannot execute binary file: Exec format error 通常表示架構不符,例如在 arm64 伺服器上執行 x86-64 二進位檔,或反之。也可能表示檔案根本不是有效的可執行檔,例如下載內容不完整或檔案已損壞。file 輸出的第一行會告訴你手上的檔案使用哪種架構,或指出該檔案不是可用的 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 種方式

每種方式都會帶來實際成本。請根據程式在執行期間的需求選擇,不要只看哪種方式聽起來最簡潔。

選項 1:在你支援的最舊發行版上建置

這是最不花俏的答案,通常也是正確的做法。在你承諾支援的最舊發行版容器映像中進行編譯。linker 只能記錄該舊版 glibc 實際具備的標籤,因此相容性下限會降至該版本,同時 binary 仍是一般的 dynamic binary,並保留 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。舊版 base image 也會包含舊版 compiler;當程式碼需要較新的 C++ 標準時,這可能造成問題。Go 和 Rust 大多能避開這項限制,因為它們的 toolchain 會獨立安裝在容器中,不依賴發行版套件。對於 C 和 C++,可以從發行版自己的 toolchain channel 安裝較新的 compiler,也可以使用 manylinux images;這些映像正是為了結合古老的 glibc 與目前版本的 GCC 而存在。Zig 內建的 C compiler 也能直接以指定的 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,且不包含 interpreter 欄位。若使用 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(名稱服務切換)解析使用者和主機名稱。程式執行期間,NSS 會以 dlopen 載入 libnss_* 模組。靜態連結的 glibc 無法執行這項操作,因此 linker 會顯示以下警告:

warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

musl 不實作 NSS,因此避開了這項警告。它有自己的 resolver,並直接讀取 /etc/resolv.conf 和 /etc/hosts。在一般 VPS 上,這種方式通常沒有問題,而且更簡單。但如果主機上的帳號或名稱來自 LDAP 或 SSSD,您的二進位檔將無法看到這些資訊,而主機上的其他程式仍然可以。musl 的 resolver 也比 glibc 新近開發:當 DNS 回應太大而無法放入 UDP 回應時,musl 直到 2023 年的 1.2.4 版才加入 TCP fallback。以較舊 musl 建置的程式無法查詢大型 DNS 記錄;如果遞迴 nameserver 在截斷的 UDP 回應中不提供部分結果,這些查詢也可能失敗。

dlopen 無法運作。 在靜態 musl 二進位檔中,dlopen 是一個永遠失敗的 stub。所有會在執行期間載入程式碼的功能都無法使用,包括 plugin system、PAM 模組、GPU driver,以及 glibc 自己的 iconv 字元集模組。如果程式需要 dlopen,就不能採用靜態連結,必須使用其他 3 種方案之一。

安全性更新會成為您的責任。 動態連結的二進位檔會在伺服器執行套件升級後立即取得 libc 修正。靜態二進位檔則永遠不會自動取得。當 libc 或您隨附的靜態 OpenSSL 出現 CVE(常見漏洞與暴露)項目時,您必須重新建置並重新散布;已部署的每個副本都會持續存在漏洞,直到有人替換該檔案。請記錄您連結進去的內容,因為目標主機上的任何資訊都無法告知管理員,您的單檔二進位檔包含兩年前的函式庫。

授權條款會改變。 glibc 採用 LGPL,而靜態連結會觸發 LGPL 的重新連結義務:您必須提供接收者所需的內容,讓其能將程式重新連結至不同版本的 glibc。musl 採用 MIT 授權,不包含這項條件。這是選擇 musl 而非靜態 glibc 來發布單檔二進位檔的主要原因。

兩項執行階段差異會造成難以判斷的崩潰。 musl 的預設執行緒 stack 大小為 128 KiB。glibc 則根據程式啟動時的 stack limit 設定預設值,通常介於 2 和 10 MB 之間;當該 limit 為 unlimited 時,在大多數架構上會退回 2 MB。因此,將大型 buffer 放在線程 stack 上的程式,可能在 musl 下發生 segfault,且沒有訊息或 log 紀錄。請使用 pthread_attr_setstacksize 明確設定大小,或將 buffer 移至 heap。musl 的 allocator 也針對小型配置與可預測行為設計,而不是針對大量執行緒同時配置記憶體進行最佳化,因此配置密集的多執行緒程式可能明顯較慢。請針對自己的工作負載進行 benchmark,不要只依賴任一函式庫的既有評價。

選項 3:將 loader 與函式庫一併封裝

將函式庫放在 binary 旁邊,並一併放入相符的 loader,然後透過該 loader 啟動程式。

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

只有一項規則決定這種方式是否可行:loader 與 libc.so.6 必須來自相同的 glibc build。兩者是相符的組合。將主機的 loader 與封裝的 libc 混用,會在啟動期間造成 crash,而不是顯示可讀的錯誤訊息。兩者都封裝,或兩者都不要封裝。

AppImage 就是將這種模式封裝起來的格式。它會將 payload 放在 squashfs image 中,再由小型 runtime 掛載該 image。這種格式有一項常被忽略的特性:大多數 AppImage 會使用主機的 glibc,而不是將 glibc 一併封裝。因此,若在目前版本的 distribution 上以這種方式建立 AppImage,放到舊伺服器上執行時,仍會因相同的版本錯誤而失敗。AppImage 官方的最佳實務建議是在不比所支援最舊系統更新的 base 上進行 build。如此一來,這類 AppImage 就是建立在選項 1 之上的交付格式,而不是選項 1 的替代方案。也可以將 glibc 一併封裝:appimage-builder 與 go-appimage 都能將 glibc 放入 AppImage,而以這種方式建立的 AppImage 可在比 build system 舊或新的 distribution 上執行。代價是檔案較大,且 libssl 等函式庫會固定在 bundle 中。

在無頭伺服器上,AppImage 需要 FUSE(filesystem in userspace)來掛載 payload,而精簡的 VPS image 通常不會包含 FUSE。使用較舊 runtime 建立的 AppImage 需要主機上的 FUSE 2 library;在 Debian 與 Ubuntu 中,其套件名稱為 libfuse2,在 Ubuntu 24.04 中則改名為 libfuse2t64。缺少該 library 時,程式會以 AppImages require FUSE to run. 結束。較新的 AppImage 內建 FUSE 3 支援,不再需要在主機上安裝 libfuse。使用 ./App.AppImage --appimage-extract-and-run 可以完全略過掛載程序;該選項會將內容解開至暫存目錄,並從該目錄執行。

選項 4:提供容器映像

將整個 userland 與程式一併移動。映像會攜帶自己的 libc,因此不再受主機的 glibc 影響,只需確保 kernel 與架構相容即可。這是最不需要技巧的選項,也能完全消除這類問題,因此許多伺服器軟體都採用這種發行方式。在 VPS 上執行 Docker 是使用這類映像的常見方式。

主機 kernel 仍會設定限制。較新的 glibc 會使用較新的 system call,而舊版容器主機可能會阻擋這些呼叫:glibc 2.34 及更新版本會使用 clone3,舊版預設 seccomp profile 會拒絕該呼叫。其症狀是立即失敗並顯示 Operation not permitted,且完全沒有提到 glibc。升級主機上的容器 runtime 即可修正,因為封鎖來自 runtime 的 syscall filter,而不是 kernel。

這些成本屬於一般性成本。目標環境需要容器 runtime,並且必須具備使用該 runtime 的權限。下載內容會從單一檔案增加到數十或數百 MB。現在必須負責維護 base image 及其修補時程,因此在選項 2 中省下的安全工作,會以重新建置映像的形式回來。對於長時間執行的服務,這是合理的取捨。對於只執行一次的命令列工具,則不值得。

應該選哪個選項?

  • 供你管理、且全部執行同一個發行版的伺服器使用的內部工具:在該發行版上動態建置,並停止於此。
  • 陌生使用者會下載並執行的單一檔案:如果程式不需要 dlopen,也不需要由 NSS 支援的查詢,則使用靜態 musl。
  • 具有外掛程式或 GPU 存取需求的程式:維持動態連結,在較舊的基礎映像上建置,並使用選項 3 來交付。
  • 在已有執行環境的機器上長時間執行的服務:交付映像。

無論選擇哪個方案,都應記錄並驗證。建置主機的 glibc 現在已成為發布流程的一部分。建置機器從一個 LTS 發行版升級到下一個版本時,可能會在不知不覺間提高最低需求,導致上個月仍能正常使用的使用者無法執行。請依標籤固定建置映像,並將輸出中的最高 GLIBC_ 標籤設為建置步驟的驗證條件。如此一來,檢查會在你的 pipeline 中失敗,而不是在其他人的終端機中才發現問題。

FAQ

為什麼我的伺服器出現「找不到版本 GLIBC_2.38」?

該二進位檔是在 glibc 比伺服器更新的機器上編譯。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。請將結果與伺服器使用 ldd --version 查出的版本比較。

musl 二進位檔會比 glibc 二進位檔慢嗎?

這取決於工作負載,最可靠的做法是實際測量。靜態 musl 二進位檔啟動較快,因為不需要執行載入器,也不需要在 exec 時進行重定位。不過,musl 的配置器是以體積小及行為可預測為設計目標,而不是針對大量執行緒同時配置記憶體進行最佳化;此外,musl 提供的手動最佳化字串與記憶體例程也少於 glibc。因此,配置密集或字串處理密集的多執行緒程式可能明顯較慢。請先在自己的伺服器上對自己的程式進行基準測試,再接受任何一種說法。

為什麼檔案明明存在,bash 卻說「沒有此檔案或目錄」?

缺少的是動態載入器,不是二進位檔。核心會從 ELF 標頭讀取直譯器路徑;路徑不存在時,核心會回傳 ENOENT,而 shell 只能以它可用的訊息回報這個錯誤。執行 file ./mytool 並讀取 interpreter 欄位。如果在 Debian 或 Ubuntu 伺服器上顯示 /lib/ld-musl-x86_64.so.1,表示你在 glibc 系統上執行 musl 連結的二進位檔。請安裝該發行版提供的 musl runtime(Debian 和 Ubuntu 上的 musl 套件),或改取得該程式的 glibc 建置版本或靜態建置版本。

可以從較新的伺服器複製 libc.so.6 來修正這個問題嗎?

不行。libc.so.6 與 ld-linux-x86-64.so.2 是來自同一個 glibc 建置版本的相符組合,而機器上的每個動態連結 glibc 程式都會使用它們,因此覆寫系統版本可能導致伺服器幾乎無法執行任何程式,包括用來復原變更的工具。如果必須在舊主機上執行單一較新的二進位檔,請將較新的 glibc 解壓縮到私有目錄,再使用 --library-path 透過其專用載入器啟動該程式;這只會影響該程序。針對較舊的 glibc 重新建置,仍是半年後不會帶來意外的做法。

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