可复现的构建

通常希望使用相同的工具集构建相同的源代码是可复现的,即输出始终完全相同。这使得验证二进制发行版或嵌入式系统的构建基础架构是否已被破坏成为可能。这也可以更轻松地验证源代码或工具更改是否对生成的二进制文件没有任何影响。

可复现构建项目 包含有关此一般主题的更多信息。本文档涵盖了构建内核可能无法复现的各种原因,以及如何避免这些原因。

时间戳

内核在三个地方嵌入时间戳

  • uname() 公开并在 /proc/version 中包含的版本字符串

  • 嵌入式 initramfs 中的文件时间戳

  • 如果通过 CONFIG_IKHEADERS 启用,则内核头文件(嵌入在内核或相应模块中)的文件时间戳,通过 /sys/kernel/kheaders.tar.xz 公开

默认情况下,时间戳是当前时间,对于 kheaders 来说,是各种文件的修改时间。必须使用 KBUILD_BUILD_TIMESTAMP 变量覆盖此值。 如果您是从 git 提交构建,则可以使用其提交日期。

内核使用 __DATE____TIME__ 宏,并且如果使用它们,则会启用警告。如果您合并了确实使用这些宏的外部代码,则必须通过设置 SOURCE_DATE_EPOCH 环境变量来覆盖它们对应的时间戳。

用户,主机

内核将构建用户和主机名嵌入到 /proc/version 中。必须使用 KBUILD_BUILD_USER 和 KBUILD_BUILD_HOST 变量覆盖这些值。如果您是从 git 提交构建,则可以使用其提交者地址。

绝对文件名

当内核在树外构建时,调试信息可能包含源文件的绝对文件名。 必须通过在 KCFLAGS 变量中包含 -fdebug-prefix-map 选项来覆盖此值。

根据使用的编译器,__FILE__ 宏也可能在树外构建中扩展为绝对文件名。 如果支持,Kbuild 会自动使用 -fmacro-prefix-map 选项来防止这种情况。

可复现构建网站包含有关这些 prefix-map 选项的更多信息。

源码包中的生成文件

tools/ 子目录下某些程序的构建过程并不完全支持树外构建。 这可能会导致以后使用例如 make rpm-pkg 构建的源码包包含生成的文件。您应该通过运行 make mrpropergit clean -d -f -x 来确保源码树是原始的,然后再构建源码包。

模块签名

如果您启用 CONFIG_MODULE_SIG_ALL,则默认行为是为每个构建生成不同的临时密钥,导致模块无法复现。但是,在源代码中包含签名密钥可能会破坏签名模块的目的。

一种方法是划分构建过程,以便可以将不可复现的部分视为来源

  1. 生成持久签名密钥。将密钥的证书添加到内核源代码。

  2. 设置 CONFIG_SYSTEM_TRUSTED_KEYS 符号以包含签名密钥的证书,将 CONFIG_MODULE_SIG_KEY 设置为空字符串,并禁用 CONFIG_MODULE_SIG_ALL。构建内核和模块。

  3. 为模块创建分离的签名,并将它们作为来源发布。

  4. 执行第二次构建,附加模块签名。它可以重建模块或使用步骤 2 的输出。

结构随机化

如果您启用 CONFIG_RANDSTRUCT,您将需要预先在 scripts/basic/randstruct.seed 中生成随机种子,以便每个构建使用相同的值。 有关详细信息,请参见 scripts/gen-randstruct-seed.sh

调试信息冲突

这不是无法复现的问题,而是生成的文件过于可复现的问题。

一旦您为可复现的构建设置了所有必要的变量,即使对于不同的内核版本,vDSO 的调试信息也可能完全相同。这可能会导致不同内核版本的调试信息包之间发生文件冲突。

为了避免这种情况,您可以通过在 vDSO 中包含任意字符串的“盐”来使 vDSO 对于不同的内核版本有所不同。 这由 Kconfig 符号 CONFIG_BUILD_SALT 指定。

Git

Git 中未提交的更改或不同的提交 ID 也可能导致不同的编译结果。 例如,在执行 git reset HEAD^ 之后,即使代码相同,编译期间生成的 include/config/kernel.release 也会有所不同,这最终会导致二进制差异。 有关详细信息,请参见 scripts/setlocalversion