Linux 内核将支持 $ORIGIN,某种程度上
摘要
Linux 内核计划通过 eBPF 和 binfmt_misc 来支持可重定位二进制文件的 $ORIGIN,从而让 Nix 等工具能够以编程方式选择解释器。
暂无内容
查看缓存全文
缓存时间: 2026/07/21 09:36
# Linux 内核将(某种程度上)支持 $ORIGIN
来源:https://fzakaria.com/2026/07/20/linux-kernel-will-support-origin-sort-of
不知为何,在 **TacoSprint 2026**(https://tacospring.org/)期间,我决定看看能否在 Nix 中攻克**可重定位二进制文件**(https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries)这一难题。我喜欢这些宏大的目标,它们推动 Nix 及其周边生态向前发展。我**虽傻但勇**。上一篇帖子末尾,我提出了一个可能的实现思路:
> 我们可以修补 Linux 内核,让 `PT_INTERP` 和 shebang 支持 `$ORIGIN`。
我艰难地走通了通过电子邮件发送补丁的流程(结果发现我其实很喜欢这种工作方式!),并向 Linux 内核邮件列表发送了一份提案。我的**第一次尝试**(https://lore.kernel.org/all/[email protected]/)只是简单提议在虚拟文件系统(VFS)子系统中直接支持 `$ORIGIN`。我忐忑地等待着。预料中的结果是——正如我在网上看到的那样——有人会很不客气地让我“**滚蛋**”,因为我遗漏、误解或没有考虑到某些东西。🤬
结果完全出乎意料。😲
VFS 维护者 **Christian Brauner**(https://brauner.io/)诚恳地回复了我,询问变更的理由,并最终提出了一些可能将该支持纳入子系统的方案。
> **注意**
> 有人帮忙说话确实很有帮助,比如 **John Ericson**(https://github.com/ericson2314)在邮件列表(https://lore.kernel.org/all/[email protected]/)中站出来解释:为何非固定解释器(`PT_INTERP`)对 Nix 以及其他用例(如 Buck 和 Bazel)非常有用。他提出,或许可以利用 **eBPF**(https://ebpf.io/)作为一种可编程的方式,通过 **binfmt_misc**(https://docs.kernel.org/admin-guide/binfmt-misc.html)来挑选解释器。
哇!🤯 我原本只是想允许 `$ORIGIN`,但可编程的挑选机制几乎能让我们做到任何事!这个想法显然深深吸引了他,因为不久之后——**在他度假期间**——Christian 就给出了该方案的第一版草稿。我们在邮件列表上来回讨论了几次,最终成果是一个**补丁系列**(https://lore.kernel.org/linux-fsdevel/20260716-wacholderbeere-zahlt-beraten-e872c3a4f59b@brauner/T/#ma8bbf0640f3154d76e1fd4607c61507b72609c6a),它将在不久的将来进入 `-next` 分支。
如果你还不了解 eBPF 或 `binfmt_misc`,那我们到底合作了啥?让我们来看看!
我不会全面介绍 eBPF(网上已有大量文章,因为它目前相当**流行**)。**一句话总结:** 你可以用 C 子集编写程序,编译成指令集,而它的虚拟机**运行在内核内部**。内核不应该超级快吗?是的,这些程序会被 JIT 编译为原生 CPU 指令,并且有固定的时间片。这难道不会给内核带来可怕的安全漏洞吗?任何代码在加载前都会经过“验证”以确保安全。更多信息请查看**这份指南**(https://ebpf.io/what-is-ebpf/)。
现在,我们可以用一个相对简单的 eBPF 程序来支持 `$ORIGIN`:
```c
SEC("struct_ops.s/match")
bool BPF_PROG(nix_match, struct linux_binprm *bprm) {
return !bpf_strncmp(bprm->buf, 4, "\x7f" "ELF");
}
SEC("struct_ops.s/load")
int BPF_PROG(nix_load, struct linux_binprm *bprm) {
char path[256];
long n;
n = bpf_path_d_path(&bprm->file->f_path, path, sizeof(path));
if (n < 0) return n;
/* 从二进制文件的路径推导加载器的位置 */
return bpf_binprm_set_interp(bprm, path, sizeof(path));
}
SEC(".struct_ops.link")
struct binfmt_misc_ops nix = {
.match = (void *)nix_match,
.load = (void *)nix_load,
.name = "nix",
};
```
一旦上述程序被加载并注册到内核,我们就可以让 `binfmt_misc` 子系统触发它。如果希望看到完整示例,请查看**这个帖子**(https://lore.kernel.org/linux-fsdevel/[email protected]/)。
```bash
> bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
> echo ':origin:B::::nix:' > /proc/sys/fs/binfmt_misc/register
```
这意味着什么?意味着每个二进制文件现在都会触发上面的 `nix_match` 函数——这里匹配任何 **ELF** 文件,但也可以是带有新段(如 `PT_INTERP_NIX`)的可执行文件——然后内核会调用 `nix_load` 来**动态**决定要使用的解释器。我们的特殊 BPF 程序已经支持 `$ORIGIN` 💥
还能做什么?嗯,现在我们甚至可以用 BPF 程序**完全替代传统的 QEMU `binfmt_misc` 注册脚本**(https://github.com/qemu/qemu/blob/master/scripts/qemu-binfmt-conf.sh),就像**这个**(https://gist.github.com/fzakaria/bef27d2e21b0e36ffccda1cbf417b636)一样。
还能做什么?既然我们可以**基于文件中的任何信息**以编程方式选择解释器,那就能做很多事了。我非常期待听到你的建议和想法 💡。一些小事情包括,我们甚至可以非常容易地在 shebang(`#!$ORIGIN/bin/ld.so`)中支持 `$ORIGIN`,就像**这里**(https://gist.github.com/fzakaria/2e1e1c44fa488a951674f8761c672366)展示的那样:只需查看文件的前 256 字节,寻找 `$ORIGIN` 来触发。
传统 `binfmt_misc` 移交的一个缺点或**副作用**是:调用目标二进制文件的方式是**非透明的**。注册的解释器**成为**了进程本身。它拥有完整的进程标识,而你原本想要运行的二进制文件被降级为一个参数。对于 `wine` 或 `qemu` 来说这是可以接受的(因为它们是模拟器),但对于一个可能挑选传统 `ld.so` 的按二进制 BPF 加载器而言,这就不太合理了。这种机制在一些令人头疼的方面暴露出来,最简单的包括:
- `argv[0]` 和 `/proc/<pid>/cmdline` 显示的是*解释器的调用*,而不是你实际执行的程序。
- `/proc/self/exe` 指向的是解释器。可重定位程序通常通过 `/proc/self/exe` 定位*自身*,结果却找到了动态链接器。😩
Christian 也针对这个问题发送了一个大型补丁系列。他最新的**补丁系列**(https://lore.kernel.org/linux-fsdevel/20260720-work-bpf-binfmt_misc-ptinterp-v1-0-ddb76c9a508e@kernel.org/T/#m5c7c7cbf4e19d2f045a69f5a1284220d6c35d88c)添加了**两种**新的分发模式,从两端弥合了这一差距,并解决了其他几个这些模式可以修复的“陷阱”。
我最感兴趣的是**加载器替换**模式 `L`——这对 Nix 来说非常令人兴奋。使用 `L` 标志后,内核会**原生地**将匹配的二进制文件作为主镜像执行,仅仅将注册的解释器替换为二进制文件 `PT_INTERP` 中命名的加载器。`binfmt_misc` 不再是移交机制,而变成了简单的 `PT_INTERP` 覆盖。没有合约需要重建,也没有身份需要重建,因此**标准的动态加载器可以原样工作**。
这让我们走到了哪里?我会跟踪 Linux 内核的发布,一旦这个特性进入 `-next` 并随某个标记版本发布,我计划上游一个**NixOS 模块**,在启动时注册 `$ORIGIN` 支持。🎉 计划是通过一个新的 `PT_INTERP_NIX` 段来触发,而不是匹配每个 ELF 文件。这样可以保持**向后兼容**:BPF 处理程序只对明确选择加入(携带新段)的二进制文件生效。这意味着 Nix 生成的二进制文件在没有 BPF 处理程序的情况下仍能正常工作,但那些带有该段的二进制文件则可以提升到*可重定位状态*。
> 港湾里的船是安全的,但那不是造船的目的。 —— 约翰·A·谢德
相似文章
Nix 需要可重定位的二进制文件
文章指出了一个问题是 Nix 二进制文件不可重定位,当存储前缀改变时会导致哈希变化和重新编译,并提出使用带有 $ORIGIN 的相对路径在 RUNPATH 中实现可重定位,而不会使缓存失效。
字节码虚拟机在意外场景中的应用 (2024)
本文探讨了字节码虚拟机的出人意料的应用,特别是Linux内核中的eBPF以及编译后二进制文件中用于调试信息的DWARF表达式。
@alswl: 你知道 Linux 内核有一个 1997 年就引入的特性,让 Apple Silicon Mac 能高效运行 x86 容器吗? 它叫 binfmt_misc,Linux 2.1.43 引入,距今快 30 年了。 原理极其简单: Linux…
文章介绍了Linux内核自1997年引入的binfmt_misc特性,它允许通过注册规则将二进制文件交给指定解释器执行,如今在Apple Silicon Mac上结合Rosetta实现高效运行x86容器,性能接近原生70-90%。
用地道 Rust 重写的 Linux 0.11,可在 QEMU 中启动
一个用现代 Rust 重写的 Linux 0.11 内核,可在 i386 上的 QEMU 中启动,包含完整的用户空间(shell 和 coreutils),支持一键构建镜像和 devcontainer 环境。
zinnia: 一个用Rust编写的模块化64位类Unix内核
Zinnia 是一个用Rust编写的模块化64位类Unix内核,旨在在UEFI系统上启动并运行带有Wayland或X11的现代桌面。它实现了POSIX API和常见的Linux/BSD扩展。