把 Linux 当解释器:一位黑客把操作系统写成了“套娃程序”

安全 2026年3月29日
把 Linux 当解释器:一位黑客把操作系统写成了“套娃程序”
一篇看似极客自娱自乐的技术博客,实际上戳中了现代操作系统一个很少被大众讨论的真相:Linux 内核不只是“系统”,它也可以被理解为 initrd 的解释器。更妙的是,作者还用 kexec 做了一个会不断重启并执行自己的“递归 Linux”,这不是单纯炫技,而是在重新提醒我们:软件边界,远没有文件扩展名看起来那么清晰。

一条 curl | sh,居然能把整个 Linux 跑起来

科技圈里有些命令,光看一眼就让人后背发凉。比如 curl https://... | sh,这几乎是安全工程师见了就想报警的经典危险动作。可这一次,作者 Astrid 偏偏就拿这样一条命令做开场,还故意卖了个关子:你下载下来执行的,到底是一个 shell 脚本,还是一个操作系统?

答案有点离谱,也有点迷人。这个 20MB 的“shell 脚本”,前半截确实是普通 sh 能读懂的文本,后半截却塞进了一大团 base64 编码数据。脚本运行后,会把这团数据解码成一个 cpio 包,再从 cpio 里抠出一个 Linux 内核镜像 k,随后用 kexec 直接把当前系统切换到这个新内核上。换句话说,你表面上是在执行一个脚本,实际上是在把另一套 Linux 当场“热启动”起来。

这也是本文最精彩的地方:它故意让你误以为自己在执行脚本,最后却发现自己喂给 sh 的,其实是一个完整的、可自举的最小 Linux 环境。那种感觉很像拆俄罗斯套娃,拆到一半才发现里面不是木偶,而是一台发动中的引擎。

这不是木马演示,而是一堂关于“解释器”的操作系统课

Astrid 真正想讨论的,不是怎么藏 payload,也不是怎么恶作剧式地重启系统,而是一个更底层、也更有启发性的观点:Linux 内核,其实可以被视作 initrd 的解释器。

通常我们说“解释器”,脑海里跳出来的是 Python、Bash、Node.js。这些程序读入文本、解析语法、执行逻辑。可如果把视角拉远一点,会发现 Linux 启动过程也有类似味道。内核加载 initrd,挂载内存文件系统,寻找 /init,再把这个脚本或程序跑起来。对于 initrd 来说,内核不就是那个“读入并执行我”的东西吗?

这个比喻乍听有点反常识,但仔细想很成立。尤其在云原生和不可变基础设施越来越普及的今天,操作系统早已不像过去那样是硬盘里一整块笨重的“底座”,它越来越像一个可装配、可替换、可通过网络分发的运行时容器。你可以从 bootloader 启它,从虚拟机启它,也可以像作者这样用 kexec 在当前系统里直接切过去。于是“OS 是平台,程序跑在上面”这条经典分层,突然变得没那么牢固了。某种意义上,OS 本身也变成了可执行对象。

这件事为什么重要?因为它动摇的是我们对“文件类型”和“执行边界”的直觉。一个 cpio 归档,平时看起来只是打包文件;一个 shell 脚本,平时看起来只是文本命令;一个内核镜像,平时看起来是只能给引导器消费的二进制。但在这套玩法里,它们串起来后就成了一条完整的执行链。谁是程序,谁是数据,很多时候不是由扩展名决定,而是由谁来解释它决定。

一个会对自己 kexec 的 Linux,像极了尾递归优化

如果只是“脚本启动 Linux”,这事已经足够极客了,但 Astrid 又往前走了一步:她把这个 initrd 做成了一个会不断对自己执行 kexec 的递归系统。

具体做法不复杂,甚至带着一点恶作剧式的简洁。initrd 里的 /init 脚本先挂载 /proc,再把当前内存中的整个根文件系统重新打包成一个新的 cpio,然后把它作为新的 initrd,连同同一个内核一起再次 kexec。新内核启动后,又会跑到同一个 /init,再打包、再切换、再执行。它不是在一个进程调用栈里无限自我调用,而是在不断用新内核替换旧内核。

作者把这件事类比成“尾调用优化的递归函数”,这个比喻我很喜欢。因为它准确,也有点诗意。普通递归会爆栈,尤其在课堂上的 Fibonacci 示例里,你几乎能听见栈帧一层层堆起来的声音。但这里没有传统意义上的栈积累,每次 kexec 都像是把旧舞台灯一关,另一个舞台在内存的别处亮起,演员继续把同一出戏演下去。上一幕没有被压在下一幕下面,而是被整个换景了。

这种思路会让很多系统工程师会心一笑,因为它和现代基础设施的一些核心哲学暗暗相通:不要试图在原地修修补补,而是构造一个新的运行环境,再原子性切换过去。Kubernetes 滚动更新是这样,NixOS/Guix 那种声明式系统也是这样,甚至很多固件升级和微虚拟机启动也遵循类似逻辑。Astrid 只是把这种“替换而非修改”的思想,玩成了一段非常纯粹、非常极客的递归艺术。

从 shell、ELF 到 ld.so:原来大家都在“解释”彼此

文章后半段最有嚼头的部分,不是那个会自我重启的 Linux,而是作者顺着“解释器”这条线一路追问:如果 Linux 内核是 initrd 的解释器,那谁又是 Linux 可执行文件的解释器?

这个问题会把你带到 ELF、动态链接器和 shebang 的老地盘。我们平时执行一个 shell 脚本,内核会看到 #!/bin/sh 这样的头部,于是把脚本交给 /bin/sh 去处理;执行一个动态链接的 ELF 文件时,内核又会依据 ELF 头里的信息,把程序先交给 ld-linux.so 这样的动态链接器来装载依赖、完成重定位,然后才把控制权交给真正的程序入口。

这说明什么?说明“解释”这件事,在计算机里远比我们日常理解得更广泛。它不一定意味着“逐行读脚本”;只要某个程序或机制负责读取另一种格式、理解其结构、准备其运行环境并把它跑起来,它在某种抽象层上就是解释器。按这个思路看,shell 在解释脚本,ld.so 在解释 ELF,Linux 内核在解释 shebang、解释静态 ELF,也在解释 initrd 所代表的启动语义。

这套观察并不只是哲学游戏。它会影响安全、运维和软件分发的实际判断。为什么攻击者总喜欢利用“看起来不像可执行物”的载荷?因为人类和很多安全策略都太依赖表面标签。为什么容器镜像、unikernel、initramfs 这些东西总让传统软件边界变得暧昧?因为它们都在挑战一句老话:程序是程序,数据是数据。现实是,在解释链条足够长时,数据随时可以变成程序,而程序也可能只是另一层运行时的数据输入。

这类“技术玩具”为何值得认真看

有人可能会说,这不就是黑客圈常见的那种脑洞作品吗?不能拿去做主流产品,也谈不上改变行业格局。这个判断不能算错,但它低估了这类实验的价值。

很多真正改变行业的思路,最初都不是从商业需求里长出来的,而是从一些“看起来没什么用”的技术玩具开始。Unix 当年也不是奔着全球云计算底座去的,容器最早也只是隔离机制的工程拼装,WebAssembly 一开始在很多人眼里也只是浏览器里跑代码的异类。Astrid 这篇文章当然不会直接催生一个新赛道,但它把 Linux 启动、kexec、initrd、自举、Quine 和解释器语义串成了一张极其鲜活的知识地图,这种地图感,本身就是稀缺价值。

我尤其欣赏她把 Quine——也就是“输出自身的程序”——引入 initrd 世界的那一段。那一刻你会突然意识到,原来一个最小 Linux 启动环境,不只是一堆启动文件,它也可以被理解成一个自我描述、自我复制、自我再执行的程序对象。这种想法听上去像计算机科学系半夜两点的宿舍聊天,但它其实正好击中今天软件工业最现实的问题:系统越来越像流水线装配的产物,我们是否还理解每一层究竟在替我们做什么?

当然,这篇文章也有让人警觉的一面。把完整操作系统塞进脚本、再通过 curl | sh 下发,本质上也展示了一种很有迷惑性的分发方式。即便作者是善意的,这种形式仍然提醒我们:未来的供应链风险,未必总是“一个恶意二进制”,它也可能是一个长得像安装脚本的系统切换器,一个你以为在安装工具、其实在替换运行环境的 payload。技术上越优雅,治理上越不能掉以轻心。

如果非要问这篇文章留给行业的一个问题,我觉得是这句:当操作系统都能被视作解释器时,我们今天的软件边界、安全边界和责任边界,是否还配得上这种流动性?这个问题没有标准答案,但它显然比“这段代码酷不酷”更值得继续追问。

有时候,最好的技术文章不是教你做什么产品,而是突然把你脑子里某个过于稳固的概念拧松一点。Astrid 这篇文章就是这种作品。它没发新品,没融大钱,也没讲 AI,却让我久违地感到:计算机这东西,原来还能这么有趣。"
Summary: 这篇文章表面上是在玩 `kexec`、initrd 和递归启动的技术把戏,实质上却是在重画“程序、数据与操作系统”的边界。我的判断是,这类作品不会直接变成主流产品,但会持续影响底层系统设计者的思考方式。未来无论是轻量化操作系统、不可变基础设施,还是供应链安全审计,都会越来越需要这种“把系统当可执行对象”的视角——它既是创造力的来源,也可能是风险的新入口。
Linuxkexecinitrdcurl | sh操作系统边界AstridcpioLinux 内核递归 Linux热启动