自举构建:方法与原因
摘要
这篇文章涵盖了一场在FOSSY会议上的演讲,解释了自举构建,该构建方法能够从最小可信源构建整个系统,以增强软件安全性和可重现性。
暂无内容
查看缓存全文
缓存时间: 2026/08/28 06:25
# 可引导构建:方法与意义
来源:https://lwn.net/Articles/1088279/
> **请考虑订阅LWN** 订阅是LWN.net的命脉。如果您欣赏这些内容并希望看到更多,您的订阅将帮助确保LWN持续蓬勃发展。请访问此页面(https://lwn.net/Promo/nst-nag1/subscribe)加入并支持LWN。
今年的自由与开源软件年度会议(https://2026.fossy.ca/),更广为人知的名称是“FOSSY”,从其前三届的举办地美国俄勒冈州波特兰移师至加拿大温哥华美丽的(且巨大的)英属哥伦比亚大学(UBC)校园。FOSSY会议涵盖多种不同类型的演讲,从深度的技术内核轨道议题,到法律与社区问题的探讨,再到“日常生活中的开源软件”主题分享。在“工具链及其他开发工具”轨道中,蒂莫西·桑普尔(Timothy Sample)发表了一场关于可引导构建(https://bootstrappable.org/)的演讲,这个话题相较于其近亲——可重现构建(https://reproducible-builds.org/)——知名度稍低,尽管LWN在约两年前(https://lwn.net/Articles/983340/)曾探讨过该主题。简而言之,可引导构建是指从一个微小的程序开始,该程序能构建一个稍大的程序,后者再构建另一个,如此迭代,最终从一个微小的种子构建出完整的现代Linux用户空间。其最终结果是产生一个来源完全明晰的代码——这与当今典型的Linux用户空间不同。
他首先询问与会者是否听说过可引导构建以及是否大体了解其概念;他似乎对大多数人知晓这个术语、约半数与会者了解更深感到印象深刻。他说,自己踏上可引导构建之路始于近十年前,当时他开始使用GNU Guix(https://guix.gnu.org/)(他发音为“geeks”——这让我感到意外)。他笑着说,在那个时候,如果你使用Guix,你就是在为其做贡献。Guix是一个“函数式包管理器”,与Nix(https://nixos.org/)相似(https://lwn.net/Articles/962788/)(并受其启发)。
[![[Timothy Sample]](https://static.lwn.net/images/2026/fossy-sample-sm.png)](https://lwn.net/Articles/1088544/)对于Guix和Nix而言,系统中的所有软件都表示在一个“派生图”中,该图描述了如何构建其中的每个程序。构建特定程序需要各种输入,这些输入在图中被指定。构建每个输入的方式(当然,还有输入的输入等等)也表示在图中。“现代软件中有成百上千的节点,其复杂程度令人惊骇。”
他举了一个Python程序的例子。显然,它需要Python才能运行,但Python是一个C程序,因此需要一个C编译器。那个C编译器是用某种语言编写的,所以又需要该语言的编译器。如此循环。Guix将所有这些收集到图中,这个图是一个可供查看和探索的对象。“于是你开始好奇,是谁编译了我编译器的编译器的编译器,这个过程在哪里终止?”
对于像Debian这样的系统,它终止于某人上传到仓库的一个C编译器二进制文件。对于Guix,最初的终止点是一个250MB的静态链接GNU用户空间程序块。当然,所有这些代码的来源并不完全清晰,这让Guix开发者感到不满。桑普尔说,这个块可以被可重现地构建,这很好,但并不能解决全部问题。
#### 可引导构建
可引导构建背后的基本理念是创建一个不依赖于预构建产物就能构建的系统。“我们能否从零开始,到达现代,而无需仅仅假设这些已为我们构建好的产物的存在?”制作酸奶的经典配方需要一些酸奶来启动过程,这类似于我们今天通常构建C编译器的方式——从一个现有的C编译器二进制文件开始。你可能会想到用你祖母从老家带来的酵头来制作酸面包;“我们基本上就是在用从贝尔实验室带来的丹尼斯·里奇(Dennis Ritchie)的酵头来制作C编译器。”
当然,这不仅仅是C语言,大多数语言都是如此。某种语言通过用该语言本身编写编译器和其他工具来实现“自举”,这对语言而言是一种自豪的体现。语言开发者这样做很自然,因为显然他们的语言是最好的,但这留下了一个类似“先有鸡还是先有蛋”的问题。可引导构建就是一项致力于超越这一点、从零开始构建这些工具的努力。
可重现构建让人们“更有信心你正在使用的、实际在计算机上执行的二进制文件对应着源代码”。用户可以收到一个声称来自一组源文件的二进制文件,但如何能确定呢?通过可重现构建,他们可以自己创建二进制文件,并检查确保其与收到的逐位相同。
可引导构建做着完全相同的事情,但它处理的是不同的故障模式。如果可重现构建验证失败,那是因为构建二进制文件的人对其来源撒谎或存在误解。可引导构建可以防止肯·汤普森(Ken Thompson)在其著名的图灵奖演讲《对可信的反思》(https://dl.acm.org/doi/epdf/10.1145/1283920.1283940)中描述的那种问题。
桑普尔说,汤普森在那次演讲中举了一个例子,询问在C编译器中哪里可以找到“\n”的定义。查看编译器的源代码不会显示定义,只会提供循环定义,即“\n”就是“\n”;将“\n”转换为ASCII十的实现蕴含在C编译器二进制文件本身中。汤普森继续他的演讲指出,更危险的东西,例如登录程序的后门,也可以用同样的方式隐藏在编译器中。
不仅仅是C编译器,甚至不仅仅是编译器,都可能有这种缺陷;任何自举程序都可能潜在地成为其牺牲品。这类程序可以从源代码中移除细节,并使其以二进制形式持续存在。在他准备演讲时,一位同事指给他看一篇最近的论文(https://arxiv.org/pdf/2607.24888)(“通过二进制操作对整个Linux发行版实施信任信任攻击”),该论文展示了一种此类的实际攻击。研究人员在NixOS的strip(https://man7.org/linux/man-pages/man1/strip.1.html)程序中插入了一个后门,该程序几乎在该系统上构建的每一个二进制文件上都会运行。“他们能够在系统上对几乎所有程序植入后门,其方式从源代码分析中是完全不可见的。”这正是可引导构建旨在防范的那种攻击。
桑普尔说,虽然安全方面是可引导构建最大的益处,但它也涉及软件自由方面。能够阅读源代码是有用的,但知道源代码与运行的程序相对应同样重要。许多程序员还有一种自豪感,他们希望自己的代码清晰易懂。确保代码可被检查,并且其所有细节都存在于某个可用的源代码主体中,是其中的一部分。
他说,处理自举的最佳方式是主动进行。在编译器自举之前,通常它是用其他语言编写的;保留该代码并在自举版本旁边维护它,提供了一种确保二进制文件中没有隐藏内容的方法。这正是GNU Guile(https://www.gnu.org/software/guile/)项目所做的;它是Scheme(https://www.scheme.org/)的一个版本,被Guix和各种其他项目使用。Guile仍然保留了一个可用于自举编译器的C语言实现。GNU Make(https://www.gnu.org/software/make/)当然有makefile,但它还有一个shell脚本,以防没有make可用。“我们认识到我们是一个基础构建工具,这里应该有另一个入口。”
对于那些只支持自举构建的工具,他和其他可引导构建社区成员使用一些技巧来规避这种缺失。第一种是“考古挖掘”,即利用项目历史找到一个非自举版本;然后使用那个时代的工具构建该版本。“接着你在历史中逐个版本地推进……直到得到现代工具。”有时可以跳过一些版本步骤,但总体上这是一个缓慢的过程。“在某些方面,这有点像把问题推迟;从技术上讲,你确实拥有所有源代码”,但要求某人查看,比如,一个工具的12个不同版本是相当令人生畏的;如果只有一个版本可供验证就好了。
一位与会者问这是否类似于使用Rust编译器的OCaml版本来引导当前的Rust;桑普尔说确实如此,只是OCaml Rust编译器已不再可用。另一位与会者指出,用于构建Go(https://go.dev/)编程语言的Plan 9(https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs)C编译器仍然可用,构建该C编译器的更早的Plan 9 C编译器也是如此。桑普尔说,对于Guix来说,这类构建链是正常的。对于Rust,它目前从基于C++的mrustc(https://github.com/thepowersgang/mrustc#mutabahs-rust-compiler)开始,构建Rust 1.54或1.56版本;现代Rust是1.97,中间几乎每个版本都必须构建,这相当缓慢。桑普尔说,当重建Guix系统时,“遇到这一连串的Rust编译非常令人沮丧”;一位与会者说他在自己的Arm笔记本上构建这些花了三天时间。
由于需要验证的步骤太多,这些链的最终结果并不那么理想。另一种选择是“专门构建一个新工具来引导某个东西”,这正是mrustc所做的。结果更令人满意,有一个代码库可供检查,但开发这样的定制工具“需要花费很长时间”。他已经做了一些这方面的工作,在某种程度上结果并不令人满意,因为该工具的存在仅仅是为了让另一个工具能够被引导。除此之外,该工具还需要维护以跟上目标工具的发展,但这不是那种能吸引其他开发者的特别令人兴奋的工作。
最成功的可引导构建结合了这两种方法。就像Rust和mrustc一样,他们回溯到一个更简单的版本,该版本可以用替代的定制工具构建,然后推进到当今版本。事实上,mrustc现在可以构建Rust 1.90,但尚未集成到Guix中;他计划在不久的将来完成此事。
#### 相关项目
他回到了Guix及其种子,如今其大小仅为约256字节,而不是那个250MB的块。它包含一个名为hex0(https://guix.gnu.org/manual/1.5.0/en/html_node/Full_002dSource-Bootstrap.html)的程序,该程序可以“通过很多很多层构建,最终得到GCC 2、GCC 4和现代GCC,以及现代Guile和所有这些工具”。虽然这“非常酷”,但有很多注意事项;最大的一点是仍然使用一个静态链接的Guile“来处理一堆事情”。他说这“绝对是作弊”,但已有计划修复此问题,他正在努力,但需要一些时间才能实现。
另一件常让人们失望的事情是,对于如何引导内核没有解决方案。Guix假定存在一个内核;“它可以从无到有引导所有用户空间,但内核超出了本次讨论的范围”。
一个相关的项目是live-bootstrap(https://github.com/fosslinux/live-bootstrap#live-bootstrap),它与Guix合作并使用许多相同的工具和方法;然而,live-bootstrap进展更快,并已探索使用Fiwix(https://www.fiwix.org/)内核来引导内核。live-bootstrap还重新生成可能伴随源代码发行版创建的任何机器生成文件(例如configure);桑普尔说,“他们做得如此彻底令人钦佩”,但“缺点是它极其复杂”。
为了说明这一点,他展示了引导该系统的182步流程(https://github.com/fosslinux/live-bootstrap/blob/master/parts.rst)。它按构建基本Linux系统所需的顺序列出了需要构建的工具。它从hex0开始,构建各种不同的工具,包括复杂度递增的多个C编译器、多个版本的Perl以引导Automake(https://www.gnu.org/software/automake/)和Autoconf(https://www.gnu.org/software/autoconf/)等等。它不包括Rust和Go等工具;“它只是现代的GNU/Linux基础系统”。
桑普尔说,“他们完成了这项工作非常棒,这相当疯狂和复杂,改进它将是件好事”。
hex0程序提供了一种将一串十六进制文本转换为包含这些字节的二进制文件的方法。通常,这用于构建hex1和hex2(https://man.sr.ht/~oriansj/bootstrappable/stage0.md),它们也是十六进制转换器,分别添加了单字符标签(hex1)和更完整的标签,允许更花哨的寻址模式(hex2)。使用这些,可以构建M0(https://github.com/oriansj/stage0#m0),它允许使用汇编助记符,而不是十六进制操作码。
桑普尔说他省略了一些步骤,但最终可以构建M2-Planet(https://github.com/oriansj/m2-planet#m2-planet),“这几乎像C”;代码可以编译,但有时某些C特性会缺失,因此需要重写以避免它们。到了那时,一切都切换到使用GNU Mes(https://www.gnu.org/software/mes/),这是一个用M2-Planet方言C编写的Scheme解释器。Mes有一个C库(Meslibc)和一个用Scheme编写的C编译器(MesCC)。这些允许构建Tiny C Compiler(https://bellard.org/tcc/)(TCC),“它是一个简单的C编译器,但比MesCC更完整”。然后可以使用TCC构建现代开发工具。
#### Germ
这正是Guix和live-bootstrap所采取的路径。“它有效,[...]但一切都极其复杂。”他正在研究一种不同的方法,称为Germ(或Germ Lisp)(https://git.ngyro.com/germ/tree/README?id=dd840f291cc26f71e99cc859f25a63ecf9839ddf),在他的网站上的一篇博客文章(https://ngyro.com/blog/introducing-germ-lisp.html)中有所介绍。在那篇文章中,他指出,现有机制从C到Scheme再回到C,最终到达基于Scheme的Guix。但他认识到,“一个原始的Lisp解释器并不比一个原始的汇编器复杂多少”。
他研究Mes并喜欢它;“有时我受雇研究Mes,Mes很棒”。但他也在编写Mes的替代品,这并不罕见。例如,有人正在用Haskell(https://www.haskell.org/)和ML(https://en.wikipedia.org/wiki/ML_%28programming_language%29)编写Mes的替代品;他说,每个看到这个问题的人都想立刻用Forth(https://forth-standard.org/)来编写替代品。通过十六进制监视器引导Forth是该语言一个显而易见的应用,但他认为大多数开发者不想用Forth编写其余的代码;“我宁愿阅读二进制文件”。从观众席上,基思·帕卡德(Keith Packard)说:“汇编语言比Forth更容易编写。”
桑普尔说,研究Scheme的人则不同,并且
相似文章
自举 Rust 被认为有害
对 Rust 编译器自举过程的批判性分析,指出与 OCaml 等其他语言相比,其体积过大和依赖臃肿的问题,并主张采用更轻量的方法。
Jinx:用于引导操作系统发行版的元构建系统
Jinx是一个用于引导操作系统发行版的元构建系统,其设计灵感来源于xbstrap以及Void Linux和Arch Linux的包构建系统。
后现代构建系统
一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。
在Bazel中一个从357字节开始的C++工具链
一篇博客文章,描述了一种新的Bazel工具链,它从357字节的stage0引导种子开始编译C++代码,在Bazel中实现了完全源码引导且封闭的C++工具链。作者演示了它从Bazel Central Registry构建未打补丁的Abseil和GoogleTest。
使用Nix构建系统软件
一篇博客文章,讨论Nix如何帮助解决构建系统软件时的依赖和可重现性问题,特别是针对像BPF和io_uring这样快速演进的子系统。