SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-21

Linux二进制无法运行:glibc与musl兼容性

遇到“GLIBC_2.34 not found”并非文件损坏,而是构建主机设定了glibc最低版本。了解二进制的真实依赖,并从四种方案中选择可移植发布方式。

较旧服务器无法运行 Linux 二进制文件的原因

由于 glibc(GNU C 库)只提供单向兼容,构建可移植的 Linux 二进制文件并不容易。旧二进制文件可以在新版 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 导出的每个函数都有一个版本标签。printflibc.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。该版本将 libpthreadlibdl 合并到 libc 中,并将 __libc_start_main(每个 C 程序的启动函数)移到了 GLIBC_2.34 标签。因而,在 glibc 2.34 或更高版本的任何发行版上编译的 hello-world 都会要求 GLIBC_2.34,即使源代码没有调用任何现代功能。这就是为什么代码多年未变的用户会突然遇到此问题。

我的发行版提供哪个 glibc 版本?

Chartglibc version in each distribution's own repositories
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 年 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 完全没有输出,说明该二进制文件是静态链接的,因此没有动态符号表,也没有可供输出的版本要求。

通过 filereadelf -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, ...

解释器是关键字段。它是内核在运行程序前启动的动态加载器,并且目标机器上必须存在该确切路径。除非有人在 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 不同,这表示 ABI 有意不兼容。RUNPATH 告诉加载器首先搜索哪些目录,$ORIGIN 会展开为存放该二进制文件的目录,因此自包含软件包可以找到自己的库。

不要对不信任的二进制文件运行 ldd。在 glibc 上,ldd 可能会通过让程序在加载器下运行来解析依赖,因此恶意文件可以执行代码。readelfobjdump 只读取字节。在执行上述任何操作前,先确认下载的文件确实是项目发布的文件,因为根据项目发布的校验和验证下载文件是唯一能确认你持有的字节来自谁的步骤。

实际会看到的错误及其含义

加载器指定了它找不到的 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 的输出第一行,即可确认当前文件的架构。

error while loading shared libraries: libssl.so.3: cannot open shared object file表示缺少 NEEDED 库,或者该库以不同的 soname 存在。请安装匹配的发行版软件包。不同发行版系列的软件包名称不同,因此从 README 复制 apt 行之前,请先完成转换;dnf 和 apt 命令等价写法介绍了这种对应关系。

musl 构建产生的单独 Segmentation fault,但程序在 glibc 下可以正常运行。通常是线程栈大小问题,选项 2 会介绍这一问题。

发布便携式 Linux 二进制文件的 4 种方式

每种方案都会带来实际成本。应根据程序在运行时的行为选择方案,而不是根据哪种方案听起来最简洁来选择。

选项 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 单行命令,并检查最高标签是否为您预期的值。

代价在于工具链。较旧的基础镜像也包含较旧的编译器;当代码需要较新的 C++ 标准时,这会造成问题。Go 和 Rust 基本可以避开这一点,因为它们的工具链会独立安装到容器中,不依赖发行版软件包。对于 C 和 C++,您可以从发行版自己的工具链渠道安装较新的编译器,也可以使用 manylinux 镜像。这些镜像正是为了将旧版 glibc 与当前 GCC 结合而存在的。Zig 捆绑的 C 编译器也可以直接面向指定的 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(名称服务切换)解析用户和主机名。程序运行时,NSS 会使用 dlopen 加载 libnss_* 模块。静态链接的 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 更新较晚:musl 直到 2023 年的 1.2.4 版本才支持对超过 512 字节的 DNS 响应回退到 TCP,因此针对旧版 musl 构建的程序会截断较大的响应。

dlopen 不可用。 在静态 musl 二进制文件中,dlopen 是一个始终失败的存根。任何需要在运行时加载代码的功能都无法使用,包括插件系统、PAM 模块、GPU 驱动程序,以及 glibc 自身的 iconv 字符集模块。如果程序需要 dlopen,就不能采用静态链接,必须选择其他三种方案之一。

安全更新由你负责。 动态链接的二进制文件会在服务器执行软件包升级后立即使用 libc 修复。静态二进制文件不会这样做。当针对你的 libc 或随程序打包的静态 OpenSSL 出现 CVE(常见漏洞和暴露)条目时,你必须重新构建并重新分发程序。已经部署的每个副本都会一直存在漏洞,直到有人替换该文件。记录所链接的组件,因为目标系统无法告知管理员这个单文件二进制文件包含两年前的库。

许可证也会改变。 glibc 使用 LGPL,静态链接会触发 LGPL 的重新链接义务:你必须向接收者提供所需内容,使其能够将程序重新链接到其他 glibc。musl 使用 MIT 许可证,不附带此类条件。这是发布单文件二进制文件的项目选择 musl 而不是静态 glibc 的主要原因。

两项运行时差异会导致难以排查的崩溃。 musl 的默认线程栈为 128 KiB,而 glibc 为 8 MiB。因此,在某个线程栈上分配大型缓冲区的代码可能会直接触发段错误,不显示消息,也不产生日志。使用 pthread_attr_setstacksize 显式设置大小,或将缓冲区移到堆上。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 之上的交付格式,而不是选项 1 的替代方案。

在无头服务器上,AppImage 需要 FUSE(用户空间文件系统)来挂载其负载,而精简的 VPS 镜像通常不包含该组件。错误信息会指出 libfuse.so.2。您可以使用 ./App.AppImage --appimage-extract-and-run 完全跳过挂载过程。该选项会将内容解包到临时目录,然后从那里执行。

选项 4:发布容器镜像

将整个用户空间与程序一起交付。镜像自带 libc,因此主机的 glibc 不再重要,只需内核和架构匹配即可。这是最直接的选项,也消除了整类问题,因此许多服务器软件都采用这种分发方式。在 VPS 上运行 Docker 是使用它的常见方式。

主机内核仍会设置限制。较新的 glibc 会使用较新的系统调用,旧版容器主机可能会阻止这些调用:glibc 2.34 及更高版本会使用 clone3,而旧版默认 seccomp 配置文件会拒绝该调用。其表现是立即失败,并显示 Operation not permitted,但完全不提 glibc。升级主机上的容器运行时即可解决,因为阻止规则位于运行时的系统调用过滤器中,而不是内核中。

这些成本都很常见。目标环境需要容器运行时,并且当前用户必须有权使用它。下载内容会从一个文件增加到几十或几百 MB。现在还需要负责基础镜像及其补丁计划,因此选项 2 中规避的安全维护工作会以重建镜像的形式重新出现。对于长期运行的服务,这种取舍通常合理。对于只运行一次的命令行工具,则不值得。

应选择哪个选项?

  • 在由您控制、且全部运行同一发行版的服务器上使用的内部工具:在该发行版上动态构建,然后停止在这一步。
  • 供陌生用户下载并运行的单个文件:如果程序不需要 dlopen,也不需要基于 NSS 的查找,则使用静态 musl。
  • 需要插件或 GPU 访问的程序:保持动态链接,在较旧的基础环境上构建,并使用选项 3 交付。
  • 在已具备运行时的计算机上长期运行的服务:交付镜像。

无论选择哪种方式,都应记录下来并进行检查。构建主机上的 glibc 现在已成为发布流程的一部分。将构建机从一个 LTS 版本升级到下一个版本,可能会在不知不觉中提高最低兼容版本,导致上个月还能正常运行的用户现在无法使用。按标签固定构建镜像,并在构建步骤中断言输出中的最高 GLIBC_ 标签。这样检查会在您的流水线中失败,而不是在其他人的终端中失败。

FAQ

为什么服务器上会出现“version GLIBC_2.38 not found”?

该二进制文件是在 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 二进制文件启动更快,因为无需运行加载器,也无需在执行时进行重定位。另一方面,musl 的内存分配器侧重于较小体积和可预测行为,而不是支持大量线程同时分配内存;此外,musl 中经过手工优化的字符串和内存例程少于 glibc。因此,对于分配密集型或字符串处理密集型的多线程程序,musl 可能明显更慢。在接受任何一种说法之前,请在自己的服务器上测试自己的程序。

为什么文件明明存在,bash 却提示“No such file or directory”?

缺少的是动态加载器,而不是二进制文件本身。内核从 ELF 头中读取解释器路径;当该路径不存在时,内核返回 ENOENT,shell 只能使用它获得的这条消息进行报告。运行 file ./mytool 并读取 interpreter 字段。如果该字段在 Debian 或 Ubuntu 服务器上显示为 /lib/ld-musl-x86_64.so.1,说明这是一个使用 musl 链接的二进制文件,而服务器使用的是 glibc,因此需要下载兼容 musl 的版本,或使用静态构建版本。

可以从更新的服务器复制 libc.so.6 来修复此问题吗?

不可以。libc.so.6ld-linux-x86-64.so.2 是同一次 glibc 构建生成的匹配组件,机器上的每个进程都会使用它们,因此覆盖系统副本可能导致服务器无法运行任何程序,包括用于恢复更改的工具。如果必须在旧主机上运行某个更新的二进制文件,请将更新的 glibc 解压到专用目录,并通过其自身的加载器使用 --library-path 启动该程序。这样只会影响该进程。针对旧版 glibc 重新构建,仍然是不会在 6 个月后给你带来意外的方案。

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