OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 081

nix-ld:预编译 ELF、加载器与库路径

从文件存在却报 ENOENT 的原因,一步步连接 ELF 诊断、打包修补和 nix-ld 配置。

所在专题Nix 开发环境与二进制兼容 ↗
约 10 分钟阅读开始阅读

文件存在,为什么仍然是 ENOENT?#

动态链接 ELF 不仅需要可执行文件本身,还需要 ELF 中记录的解释器。普通 x86_64 Linux 发行版常把 glibc 加载器放在 /lib64/ld-linux-x86-64.so.2;NixOS 的已打包程序通常直接引用 /nix/store 中的加载器。未适配的下载文件找不到其固定加载器路径时,execve 返回 ENOENT,shell 便可能报出 No such file or directory。

下面是说明这一现象的历史示例输出,不是本机运行记录:

$ ./masterpdfeditor5
bash: ./masterpdfeditor5: No such file or directory
$ ls -la ./masterpdfeditor5
-rwxr-xr-x 1 user users 27160344 Jul 4 16:22 ./masterpdfeditor5

从系统调用查到 ELF 解释器

strace -f ./masterpdfeditor5
file ./masterpdfeditor5
patchelf --print-interpreter ./masterpdfeditor5

典型的诊断关系如下,地址、构建 ID 和 store hash 已省略:

execve("./masterpdfeditor5", ["./masterpdfeditor5"], ...) = -1 ENOENT (No such file or directory)
strace: exec: No such file or directory
masterpdfeditor5: ELF 64-bit LSB executable, x86-64, dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, stripped

file 是独立的软件包工具,不属于 binutils。strace 显示系统调用和返回值;file 与 patchelf 帮助区分文件权限、架构、解释器与依赖库问题。加载器路径缺失只是 ENOENT 的一种原因,脚本 shebang 指向不存在的解释器也会产生类似错误。

比较一个已打包的系统程序:

patchelf --print-interpreter /run/current-system/sw/bin/ls

其加载器常是 /nix/store/<hash>-glibc-<version>/lib/ld-linux-x86-64.so.2。这是 NixOS 的路径布局差异;NixOS 本身也是 Linux。

打包时的解决方案:autoPatchelfHook#

对有固定发布包的闭源软件,autoPatchelfHook 是 Nixpkgs 构建 hook:在打包阶段查找二进制依赖,改写 ELF 解释器并设置库搜索路径。buildInputs 中仍需提供相应依赖,hook 不会凭空下载缺失的库。

历史笔记用 Master PDF Editor 展示改写后的 RPATH:

nix-shell -p masterpdfeditor patchelf
command -v masterpdfeditor5
# 对真正的 ELF 文件检查;外层可能是启动 wrapper
patchelf --print-rpath /nix/store/<hash>-masterpdfeditor-<version>/bin/.masterpdfeditor5-wrapped

包名、版本和 wrapped 文件名应以所用 Nixpkgs 为准。历史输出的实质是 RPATH 包含多个 Nix store 库目录:

/nix/store/<hash>-sane-backends-<version>/lib:
/nix/store/<hash>-zlib-<version>/lib:
/nix/store/<hash>-qtsvg-5.15/lib:
/nix/store/<hash>-qtbase-5.15/lib:
/nix/store/<hash>-qtdeclarative-5.15/lib:
/nix/store/<hash>-libglvnd-<version>/lib:
/nix/store/<hash>-gcc-<version>-lib/lib

上面的换行只用于阅读,实际 RPATH 是冒号分隔的一行。Qt5 是这一历史包的依赖,不代表当前所有版本仍采用 Qt5。

哪些场景不适合每次 patch?

  • 编辑器扩展、npm 或 pip 等工具下载的可执行文件,更新后还要重新修补。
  • Java JAR 或其他归档把二进制藏在内部,直到运行时才解压。
  • 游戏或专有软件校验二进制完整性,修改后可能拒绝启动。
  • FPGA IDE 等体积很大的软件,复制到 store 后再处理可能很不方便。

nix-ld 的垫片机制#

nix-ld 放在普通发行版加载器所在的路径,再转交给真正的 Nix store 动态加载器。它让未 patch 的文件保留原来的内容,不要求将整个软件复制到 store。

NIX_LD 指定真正的加载器;NIX_LD_LIBRARY_PATH 指定额外库目录,使用冒号分隔,不是逗号分隔。垫片为加载器提供库搜索路径。上游也支持按架构设置的变量,例如 NIX_LD_x86_64_linux;后缀将 system 中的连字符替换为下划线。

系统模块配置

{ pkgs, ... }: {
  programs.nix-ld.enable = true;
  programs.nix-ld.libraries = with pkgs; [
    stdenv.cc.cc
    zlib
    fuse3
    icu
    nss
    openssl
    curl
    expat
    # 按应用缺失依赖继续添加
  ];
}

该模块生效后,再运行目标二进制。仅把配置写入文件或构建系统,并不等于已经启用了垫片。上述依赖列表是起点,不能保证每个商业软件都可以直接运行。

原文写于 2022 年,讨论了当时 upcoming 23.05 的库配置体验;这里把发布预告改为历史说明。原作者的扩展配置可见 Mic92 的 nix-ld 模块。

项目局部的环境变量

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  NIX_LD = pkgs.stdenv.cc.bintools.dynamicLinker;
  NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
    pkgs.stdenv.cc.cc
    pkgs.openssl
  ];
}

如果垫片已存在而加载器配置缺失,错误信息可能明确指出尚未配置 NIX_LD。应按安装版本提示核对配置。

与 Python 虚拟环境的边界

Nixpkgs 的 Python 已链接到 store 内加载器,不会因为设置了 NIX_LD_LIBRARY_PATH 就自动走 nix-ld。PyPI 扩展模块缺库时,需要在项目局部准备正确的系统库与运行环境;直接全局设置 LD_LIBRARY_PATH 可能把错误版本注入已经打包好的程序。Python/Qt 的具体排查见 Python 环境配置与 Qt 排错。

steam-run 与打包方案的取舍#

偶尔运行未适配软件,可以考虑 steam-run 提供的 FHS 环境:

steam-run ./my-app

命令要求 steam-run 已可用。它提供的是兼容环境,适用范围与 nix-ld 不完全相同;打包时则通常优先考虑 autoPatchelfHook 或其他 Nixpkgs 构建机制。

日常使用的 QQ、WeChat、VS Code 等软件先在 Nixpkgs 包搜索 查找现成包,再根据具体依赖选择兼容方案。nix-ld 解决动态加载器与库路径问题,不会自动修复缺失资源、内核功能、所有 /usr/bin 硬编码路径或其他 ABI 差异。硬编码脚本路径是另一个话题,可进一步了解 envfs。

来源与范围#

本篇合并基础配置笔记与 Jörg Thalheim、Jill Thornhill 于 2022-12-31 发布的 nix-ld 原文,去掉重复中英对照,保留系统调用、ELF、RPATH、适用场景和配置的完整技术链。命令及输出均用于解释,不是本机验证结果。

参考与延伸

  1. nix-ld 上游说明 ↗
  2. 原作者历史文章 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

Nix 开发环境与二进制兼容

  1. 01Nix-shell、devShell 与 direnv 的递进用法
  2. 02Nix 开发环境:多语言、数据库与全栈示例
  3. 03Python、uv 与 Matplotlib/Qt 的依赖边界
  4. 04nix-ld:预编译 ELF、加载器与库路径
  5. 05Overlays、override 与 overrideAttrs
CONTINUE READING / 下一篇Nix-shell、devShell 与 direnv 的递进用法

把临时工具、项目锁定环境和目录自动加载分成三个明确步骤。