构建软件注意事项

已经构建过 LFS 系统的人可能了解下载和解压软件的一般原则。此处重复一些信息,供初次构建自己软件的人参考。

每组安装说明都包含一个可下载软件包的 URL。而补丁则存储在 LFS 服务器上,可通过 HTTP 获取。安装说明中会在需要时引用这些补丁。

您可以将源代码文件保存在任何位置,但我们假定您已解压软件包,并进入解压过程所创建的目录(源代码目录)。我们还假定您已解压所有必需的补丁,并且它们位于源代码目录的上一级目录中。

怎么强调都不为过:您每次都应该从一个干净的源代码树开始。这意味着,如果在配置或编译过程中出现错误,通常最好删除源代码树,并在重试之前重新解压它。如果您是习惯于修改 Makefile 和 C 代码的高级用户,这显然不适用;但若有疑问,请从干净的源代码树开始。

以非特权(non-root)用户身份构建软件

Unix 系统管理的黄金法则是仅在必要时使用超级用户权限。因此,BLFS 建议您以非特权用户身份构建软件,仅在安装软件时才切换为 root 用户。本书中的所有软件包都遵循此原则。除非另有说明,所有指令都应以非特权用户身份执行。本书会提示哪些指令需要 root 权限。

解压软件

如果文件是 .tar 格式且已压缩,则通过运行以下命令之一解压:

tar -xvf filename.tar.gz
tar -xvf filename.tgz
tar -xvf filename.tar.Z
tar -xvf filename.tar.bz2
[注意]

注意

如果您希望抑制解压时列出所有文件的详细输出,可以在上述和下述命令中省略 v 参数。这有助于加快解压速度,并使解压过程中产生的任何错误更加明显。

您也可以使用另一个略微不同的方法:

bzcat filename.tar.bz2 | tar -xv

最后,有时我们会遇到 .patch.gz.patch.bz2 格式的压缩补丁文件。应用补丁的最佳方法是将解压程序的输出通过管道传递给 patch 工具。例如:

zcat ../patchname.patch.gz | patch -p1

对于使用 bzip2 压缩的补丁:

bzcat ../patchname.patch.bz2 | patch -p1

验证文件完整性

通常,为了验证下载的文件是否完整,许多软件包维护者还会发布文件的 md5sum。要验证下载文件的 md5sum,请将文件和相应的 md5sum 文件下载到同一目录(最好来自不同的在线位置),然后(假设 file.md5sum 是下载的 md5sum 文件)运行以下命令:

md5sum -c file.md5sum

如果有任何错误,会报告出来。请注意,BLFS 本书也包含了所有源代码文件的 md5sum。要使用 BLFS 提供的 md5sum,您可以创建一个 file.md5sum(将 md5sum 数据和下载文件的确切名称放在文件的同一行,用空格分隔),然后运行上述命令。或者,直接运行以下命令并将输出与 BLFS 书中显示的 md5sum 数据进行比较。

md5sum <下载的文件>

MD5 在密码学上并不安全,因此 md5sum 仅用于检测文件内容的非恶意更改。例如,网络传输过程中引入的错误或截断,或者上游对软件包进行“静默”更新(更新已发布 tarball 的内容而不是正确地发布新版本)。

没有 100% 安全的方法来确保源文件的真实性。假设上游正确管理其网站(私钥未泄露且域名未被劫持),并且在 BLFS 系统上使用 make-ca-1.16.1 正确设置了信任锚点,我们可以合理地信任上游官方网站 使用 https 协议 的下载 URL。请注意,BLFS 本书本身也发布在使用 https 的网站上,因此您应该已经对 https 协议有一定的信任,否则您也不会信任本书的内容。

如果软件包是从非官方位置(例如本地镜像)下载的,则可以使用密码学上安全的摘要算法(例如 SHA256)生成的校验和来验证软件包的真实性。从上游 官方 网站(或 您信任 的某个地方)下载校验和文件,并将其与从非官方位置下载的软件包的校验和进行比较。例如,可以使用以下命令检查 SHA256 校验和:

[注意]

注意

如果校验和与软件包来自同一不可信位置,则通过校验和验证软件包不会获得安全增强。攻击者可以伪造校验和并同时破坏软件包本身。

sha256sum -c 文件.sha256sum

如果安装了 GnuPG-2.5.21,您还可以使用 GPG 签名验证软件包的真实性。使用以下命令导入上游 GPG 公钥:

gpg --recv-key keyID

密钥 ID 应替换为来自 您信任 的某个地方的密钥 ID(例如,使用 https 从上游官方网站复制)。现在您可以使用以下命令验证签名:

gpg --verify 文件.sig 文件

签名的优势在于,一旦导入了可信的公钥,您就可以从同一非官方位置同时下载软件包及其签名,并使用公钥进行验证。这样您就不需要为每个新版本都连接到上游官方网站获取校验和。您只需在公钥过期或撤销时更新它即可。

创建安装过程中的日志文件

对于较大的软件包,创建日志文件比盯着屏幕希望能捕捉到特定错误或警告更方便。日志文件也便于调试和记录。以下命令允许您创建安装日志。将 <command> 替换为您要执行的命令。

( <command> 2>&1 | tee compile.log && exit $PIPESTATUS )

2>&1 将错误消息重定向到与标准输出相同的位置。tee 命令允许在记录结果到文件的同时查看输出。命令周围的括号使整个命令在子 shell 中运行,最后的 exit $PIPESTATUS 命令确保返回 <command> 的结果,而不是 tee 命令的结果。

使用多个处理器核心

对于许多具有多个处理器(或核心)的现代系统,可以通过设置环境变量或告诉 make 程序同时执行多个任务来执行“并行 make”,从而减少软件包的编译时间。

例如,Intel Core i9-13900K CPU 包含 8 个性能(P)核和 16 个能效(E)核,并且 P 核支持 SMT(同步多线程,也称为“超线程”),因此每个 P 核可以同时运行两个线程,Linux 内核会将每个 P 核视为两个逻辑核。因此,总共有 32 个逻辑核。要利用所有这些逻辑核运行 make,我们可以设置环境变量告诉 make 同时运行 32 个任务:

export MAKEFLAGS='-j32'

或者直接使用以下命令构建:

make -j32

如果您在 LFS 中构建 ninja 时应用了可选的 sed,则可以使用:

export NINJAJOBS=32

当软件包使用 ninja 时,或者直接:

ninja -j32

如果不确定你的逻辑核的数量,请运行 nproc 命令。

对于 make,默认任务数为 1。但对于 ninja,如果逻辑核数 N 大于 2,则默认任务数为 N + 2;如果 N 为 1 或 2,则为 N + 1。使用略大于逻辑核数的任务数是为了让所有逻辑处理器保持忙碌,即使某些任务正在进行 I/O 操作。

请注意,-j 开关仅限制 makeninja 启动的并行任务数,但每个任务仍可能自行生成进程或线程。例如,某些软件包的测试可能会生成多个线程来测试线程安全属性。构建系统无法通用地知道一个任务会生成多少进程或线程。因此,通常我们不应将 -j 传递的值视为要使用的逻辑核数的硬限制。如果您想设置这样的硬限制,请阅读 “使用 Linux cgroup 限制资源使用”一节

通常,进程数不应超过 CPU 支持的核心数太多。要列出系统上的处理器,请执行:grep processor /proc/cpuinfo

在某些情况下,使用多个进程可能导致竞争条件,即构建的成功取决于 make 程序运行的命令顺序。例如,如果可执行文件需要文件 A 和文件 B,在依赖组件之一可用之前尝试链接程序将导致失败。这种情况通常是因为上游开发人员没有正确指定 Makefile 中完成某个步骤所需的所有前提条件。

如果发生这种情况,最好的方法是回退到单处理器构建。在 make 命令中添加 -j1 将覆盖 MAKEFLAGS 环境变量中的类似设置。

[重要]

重要

现代 CPU 拥有大量核心时还可能遇到另一个问题。每个启动的任务都会消耗内存,如果每个任务所需内存的总和超过可用内存,您可能会遇到 OOM(内存不足)内核中断或严重的交换,从而使构建速度慢到不合理的程度。

某些使用 g++ 的编译可能消耗高达 2.5 GB 的内存,因此为安全起见,您应将任务数限制为(总内存 GB)/2.5,至少对于 LLVM、WebKitGtk、QtWebEngine 或 libreoffice 等大型软件包应如此。

使用 Linux cgroup 限制资源使用

有时我们希望限制构建软件包时的资源使用。例如,当有 8 个逻辑核时,我们可能只想使用 6 个核心构建软件包,保留另外 2 个核心用于播放电影。Linux 内核提供了名为控制组(cgroup)的功能来满足此类需求。

在内核配置中启用控制组,然后重新构建内核并在必要时重启:

General setup --->
  [*] Control Group support --->                                       [CGROUPS]
    [*] Memory controller                                                [MEMCG]
    [*] Cpuset controller                                              [CPUSETS]

确保安装了 Sudo-1.9.17p2。要使用前 4 个逻辑核和 8 GB 系统内存运行 make -j5,请执行:

bash -e << \EOF
  sudo mkdir /sys/fs/cgroup/$$
  sudo sh -c \
    "echo +memory +cpuset > /sys/fs/cgroup/cgroup.subtree_control"
  sudo sh -c \
    "echo 0-3 > /sys/fs/cgroup/$$/cpuset.cpus"
  sudo sh -c \
    "echo $(bc -e '8*2^30') > /sys/fs/cgroup/$$/memory.high"
  (
    sudo sh -c "echo $BASHPID > /sys/fs/cgroup/$$/cgroup.procs"
    exec make -j5
  )
  sudo rmdir /sys/fs/cgroup/$$
EOF

使用 8589934592bc -e '8*2^30' 的输出,2^30 表示 230,即 1 GB)作为 memory.high 条目的值, 设置了内存使用的软限制。如果 cgroup 中的进程(make 及其所有后代进程)总共使用超过 8 GB 的系统内存,内核将限制这些进程并尝试从它们回收系统内存。但它们仍然可以使用超过 8 GB 的系统内存。如果您想设置硬限制,请将 memory.high 替换为 memory.max 但这样做如果 8 GB 不够用,会导致进程被杀死。

0-3(在 cpuset.cpus 条目中) 使内核仅在编号为 0、1、2 或 3 的逻辑核上运行 cgroup 中的进程。您可能需要根据逻辑核与物理核的映射关系调整此设置。例如,对于 Intel Core i9-13900K CPU,逻辑核 0、2、4、……、14 映射到 8 个物理 P 核的第一个线程,逻辑核 1、3、5、……、15 映射到物理 P 核的第二个线程,逻辑核 16、17、……、31 映射到 16 个物理 E 核。因此,如果我们想使用来自四个不同 P 核的四个线程,需要指定 0,2,4,6 而不是 0-3。请注意,其他 CPU 型号可能使用不同的映射方案。如果不确定逻辑核与物理核之间的映射,请运行 lscpu --extended 命令,该命令会在 CPU 列输出逻辑核 ID,在 CORE 列输出物理核 ID。

nprocninja 命令在 cgroup 中运行时,它会将分配给 cgroup 的逻辑核数用作“系统逻辑核数”。例如,在分配了逻辑核 0-3 的 cgroup 中,nproc 将输出 4,而如果没有明确给出 -j 设置,ninja 将同时运行 6(4 + 2)个任务。

阅读 Linux 内核源代码树中的 Documentation/admin-guide/cgroup-v2.rst 文件,以获取命令中所引用的 cgroup2 伪文件系统条目的详细说明。

自动化构建过程

有时自动化构建软件包会非常方便。每个人都可能有自己自动化构建的理由,并且各有各的方法。创建 MakefileBash 脚本、Perl 脚本,或者仅仅是一组用于剪切粘贴的命令列表,都只是您可以用来自动化构建 BLFS 软件包的一些方法。详细说明并提供各种自动化构建方法示例超出了本节的范围。本节将向您介绍使用文件重定向和 yes 命令,以帮助提供自动化构建的思路。

文件重定向自动化输入

在您的 BLFS 旅程中,您会遇到一些软件包,其命令会提示您输入信息。这些信息可能是配置细节、目录路径或对许可协议的响应。这给自动化构建该软件包带来了挑战。有时,您会在一系列问题中被提示输入不同的信息。自动化此类场景的一种方法是将所需的响应放入一个文件,并使用重定向,以便程序使用该文件中的数据作为问题的答案。

这实际上使测试套件使用文件中的响应作为问题的输入。有时您可能需要进行一些反复试验来确定某些内容的输入文件的确切格式,但一旦弄清楚并记录下来,您就可以使用它来自动化构建软件包。

使用 yes 自动化输入

有时您只需要提供一个响应,或者对许多提示提供相同的响应。对于这些情况,yes 命令非常有效。yes 命令可用于为一个或多个问题实例提供(相同的)响应。它可以用来模拟仅按 Enter 键、输入 Y 键或输入一串文本。展示其用法的最简单方法可能是在示例中。

首先,通过输入以下命令创建一个简短的 Bash 脚本:

cat > blfs-yes-test1 << "EOF"
#!/bin/bash

echo -n -e "\n\n请键入一些内容(或什么都不键入)并按 Enter 键 ---> "

read A_STRING

if test "$A_STRING" = ""; then A_STRING="仅按了 Enter 键"
else A_STRING="您输入了 '$A_STRING'"
fi

echo -e "\n\n$A_STRING\n\n"
EOF
chmod 755 blfs-yes-test1

现在从命令行运行脚本 ./blfs-yes-test1。它将等待一个响应,可以是任何内容(或空),然后按 Enter 键。输入内容后,结果将回显到屏幕上。现在使用 yes 命令自动化输入响应:

yes | ./blfs-yes-test1

请注意,仅将 yes 通过管道传给脚本,会导致将 y 传递给脚本。现在尝试使用一串文本:

yes '这是一些文本' | ./blfs-yes-test1

该确切字符串被用作脚本的响应。最后,尝试使用空(null)字符串:

yes '' | ./blfs-yes-test1

请注意,这会导致仅将按 Enter 键传递给脚本。这在提示的默认答案就足够时非常有用。在 Net-tools 指令中使用了此语法,以接受配置步骤中许多提示的所有默认值。如果愿意,您现在可以删除测试脚本。

文件重定向自动化输出

为了自动化构建某些软件包,特别是那些要求您逐页阅读许可协议的软件包,需要使用一种避免按键显示每一页的方法。在这些情况下,将输出重定向到文件可用于辅助自动化。本节前一页提到了创建构建输出的日志文件。那里显示的重定向方法使用 tee 命令将输出重定向到文件,同时还将输出显示在屏幕上。这里,输出将仅发送到文件。

同样,展示该技术的最简单方法是通过示例。首先,执行命令:

ls -l /usr/bin | less

当然,因为使用了 less 过滤器,您需要逐页查看输出。现在尝试相同的命令,但这次将输出重定向到文件。可以使用特殊文件 /dev/null 代替所示文件名,但这样就没有日志文件可供检查了:

ls -l /usr/bin | less > redirect_test.log 2>&1

请注意,这次命令立即返回到 shell 提示符,而无需逐页翻阅输出。您现在可以删除日志文件。

最后一个示例将结合使用 yes 命令和输出重定向,以绕过必须逐页翻阅输出然后对提示输入 y 的步骤。这种技术可用于那些否则您需要逐页翻阅文件(如许可协议)然后回答“您是否接受上述内容?”的情况。为此,需要另一个简短的 Bash 脚本:

cat > blfs-yes-test2 << "EOF"
#!/bin/bash

ls -l /usr/bin | less

echo -n -e "\n\n您喜欢阅读这些内容吗?(y,n) "

read A_STRING

if test "$A_STRING" = "y"; then A_STRING="您输入了 'y' 键"
else A_STRING="您没有输入 'y' 键"
fi

echo -e "\n\n$A_STRING\n\n"
EOF
chmod 755 blfs-yes-test2

此脚本可用于模拟需要您阅读许可协议,然后适当响应以接受协议,之后程序才会安装任何内容的程序。首先,在不使用任何自动化技术的情况下运行脚本:./blfs-yes-test2

现在执行以下命令,它使用了两种自动化技术,适合在自动化构建脚本中使用:

yes | ./blfs-yes-test2 > blfs-yes-test2.log 2>&1

如果需要,执行 tail blfs-yes-test2.log 查看分页输出的末尾,并确认 y 已传递给脚本。一旦确认其工作正常,您可以删除脚本和日志文件。

最后,请记住,有很多方法可以自动化或编写构建命令的脚本。没有唯一的“正确”方法。您的想象力是唯一的限制。

依赖关系

对于每个描述的软件包,BLFS 都会列出已知的依赖关系。这些依赖关系按几个标题列出,其含义如下:

  • 必需 意味着目标软件包在未首先安装依赖关系的情况下无法正确构建,除非该依赖关系被标注为“运行时”,这意味着目标软件包可以构建,但缺少它则无法运行。

    请注意,目标软件包可以以多种微妙的方式开始“运行”:已安装的配置文件可以使 init 系统、cron 守护进程或总线守护进程自动运行某个程序;另一个使用目标软件包作为依赖关系的软件包可以在构建系统中运行目标软件包中的程序;BLFS 本书中的配置章节也可能运行刚安装的软件包中的程序。因此,如果您在未安装 必需(运行时) 依赖关系的情况下安装目标软件包,则应在安装目标软件包后尽快安装该依赖关系。

  • 推荐 意味着 BLFS 强烈建议首先安装此软件包(除非标注为“运行时”,见下文),以获得干净且无故障的构建,在构建过程或运行时不会出现问题。本书中的指令假定已安装这些软件包。在许多情况下,如果未安装推荐的依赖关系(不仅仅是“运行时”),构建的软件包可能缺少某些重要功能(例如,视频播放器可能只能播放音频)。有时需要修改本书指令以禁用这些重要功能。在其他情况下,软件包的构建系统可能会构建源代码树中附带的依赖关系副本(通常已过时,有时存在已知安全漏洞),或者在构建过程中从互联网下载。这会增加构建时间和磁盘使用量,并可能导致其他问题。 如果推荐的依赖关系被标注为“运行时”,则意味着 BLFS 强烈建议在使用该软件包之前安装该依赖关系,以获得完整功能。

  • 可选 意味着可以安装此软件包以获得附加功能。BLFS 通常会描述该依赖关系以说明由此产生的附加功能。某些可选依赖关系在安装后会被目标软件包自动识别,而另一些则还需要在构建目标软件包时启用额外的配置选项。此类额外选项通常记录在 BLFS 书中。如果可选依赖关系被标注为“运行时”,则意味着您可以在安装目标软件包后安装该依赖关系,以支持目标软件包的某些可选功能(如果您需要这些功能)。

    可选依赖关系可能不在 BLFS 中。如果您需要此类 外部 可选依赖关系以实现某些功能,请阅读 超越 BLFS 以获取关于安装非 BLFS 软件包的一般提示。请注意,BLFS 编辑通常不会测试带有外部软件包的配置,因此书中列出的外部依赖关系绝对没有质量保证。外部依赖关系列表可能不完整或包含某些冗余项。在最坏情况下,系统上仅存在某个外部依赖关系就可能触发软件包中的错误,导致构建或运行时失败。

使用最新的软件源码

有时您可能会遇到书中某个软件包无法构建或无法正常工作的情况。尽管编辑们努力确保书中每个软件包都能构建并正常工作,但有时某个软件包可能被忽视,或者未在此特定版本的 BLFS 上进行测试。

如果您发现某个软件包无法构建或无法正常工作,您应该查看是否有更新版本的软件包。通常这意味着您需要访问维护者的网站并下载最新的 tarball,然后尝试构建该软件包。如果您无法通过查看下载 URL 确定维护者的网站,请使用 Google 查询软件包名称。例如,在 Google 搜索栏中输入:“package_name download”(不含引号)或类似内容。有时输入:“package_name home page” 会使您找到维护者的网站。

再谈剥离

在 LFS 中,已经多次讨论了调试符号和不需要的符号表条目的剥离。构建 BLFS 软件包时,通常没有特别说明再次进行剥离。剥离可以在安装软件包时进行,也可以在之后进行。

安装软件包时剥离

有几种方法可以剥离软件包安装的可执行文件。它们取决于所使用的构建系统(见下文 关于构建系统的章节),因此这里只能列出一些通用方法:

[注意]

注意

以下使用构建系统(autotools、meson 或 cmake)功能的方法不会剥离任何已安装的静态库。幸运的是,BLFS 中静态库不多,并且始终可以安全地手动运行 strip --strip-unneeded 来剥离静态库。

  • 使用 autotools 的软件包通常在其生成的 Makefile 文件中有一个 install-strip 目标。因此,安装剥离后的可执行文件只需使用 make install-strip 代替 make install

  • 使用 meson 构建系统的软件包可以在运行 meson 时接受 -D strip=true。如果您忘记在运行 meson 时添加此选项,也可以运行 meson install --strip 代替 ninja install

  • cmakeUnix MakefilesNinja 生成器(Linux 上默认是 Unix Makefiles)生成 install/strip 目标。因此只需运行 make install/stripninja install/strip 代替相应的 install 目标。

  • 移除(或不生成)调试符号也可以通过移除 C/C++ 调用中的 -g<something> 选项来实现。如何做到这一点对于每个软件包都非常具体。而且,这不会移除不需要的符号表条目。因此这里不再详细解释。另请参阅下面关于优化的段落。

剥离已安装的可执行文件

strip 工具会原地修改文件,如果文件已加载到内存中,可能会破坏任何使用它的程序。请注意,如果文件正在使用但只是从磁盘删除(即未覆盖或修改),则这不是问题,因为内核可以使用“已删除”的文件。查看 /proc/*/maps,您很可能会看到一些 (deleted) 条目。mv 仅从目录中删除目标文件,但不修改其内容,因此满足内核使用旧(已删除)文件的条件。但这种方法可能会将硬链接拆分为重复副本,导致膨胀,这显然是我们不希望的,因为我们剥离是为了减小系统大小。如果同一文件系统中的两个文件共享相同的 inode 编号,则它们是彼此的硬链接,我们应该重建链接。下面的脚本只是一个示例。它应以 root 用户身份运行:

cat > /usr/sbin/strip-all.sh << "EOF"
#!/usr/bin/bash

if [ $EUID -ne 0 ]; then
  echo "需要 root 权限"
  exit 1
fi

last_fs_inode=
last_file=

{ find /usr/lib -type f -name '*.so*' ! -name '*dbg'
  find /usr/lib -type f -name '*.a'
  find /usr/{bin,sbin,libexec} -type f
} | xargs stat -c '%m %i %n' | sort | while read fs inode file; do
       if ! readelf -h $file >/dev/null 2>&1; then continue; fi
       if file $file | grep --quiet --invert-match 'not stripped'; then continue; fi

       if [ "$fs $inode" = "$last_fs_inode" ]; then
         ln -f $last_file $file;
         continue;
       fi

       cp --preserve $file    ${file}.tmp
       strip --strip-unneeded ${file}.tmp
       mv ${file}.tmp $file

       last_fs_inode="$fs $inode"
       last_file=$file
done
EOF
chmod 744 /usr/sbin/strip-all.sh

如果您在 /opt/usr/local 等其他目录中安装程序,您可能也希望剥离那里的文件。只需在大括号之间的 find 命令组合列表中添加其他要扫描的目录。

有关剥离的更多信息,请参阅 https://www.technovelty.org/linux/stripping-shared-libraries.html

使用不同的构建系统

目前有三种常见的构建系统用于将 C 或 C++ 源代码转换为编译后的程序或库,它们的细节(特别是了解可用选项及其默认值)各不相同。从 CFLAGSCXXFLAGSLDFLAGS 环境变量开始,可能最容易理解某些选择(通常导致执行缓慢或意外使用或省略优化)引起的问题。还有一些程序使用 Rust。

大多数 LFS 和 BLFS 构建者可能都了解 CFLAGSCXXFLAGS 的基本知识,用于改变程序的编译方式。通常,上游开发人员会使用某种优化形式(-O2-O3),有时还会创建调试符号(-g)作为默认值。

如果存在矛盾的标志(例如多个不同的 -O 值),则 最后 一个值将被使用。有时这意味着环境变量中指定的标志会在 Makefile 中硬编码的值之前被拾取,从而被忽略。例如,如果用户指定了 -O2,随后是 -O3,则构建将使用 -O3

还可以在 CFLAGS 或 CXXFLAGS 中传递各种其他内容,例如允许使用特定微架构可用的指令集扩展(例如 -march=amdfam10-march=native),为特定微架构调整生成的代码(例如 -mtune=tigerlake-mtune=native,如果未使用 -mtune=,则将使用 -march= 设置中的微架构),或指定 C 或 C++ 的特定标准(例如 -std=c++17)。但现在已经浮现的一件事是,程序员可能会在代码中包含调试断言,期望通过使用 -D NDEBUG 在发布版本中禁用它们。特别是,如果 Mesa-26.1.3 启用了这些断言进行构建,那么某些活动(例如加载游戏关卡)即使使用高端显卡也可能花费极长的时间。

Autotools 与 Make

这种组合通常被描述为“CMMI”(configure、make、make install),此处也用于涵盖少数具有非 autotools 生成的 configure 脚本的软件包。

有时运行 ./configure --help 会生成关于可能使用的开关的有用选项。在其他时候,查看 configure 的输出后,您可能需要查看脚本的细节以了解它实际在查找什么。

许多 configure 脚本会从环境中拾取任何 CFLAGS 或 CXXFLAGS,但 CMMI 软件包在如何将这些与环境中的标志混合(各不相同:忽略、用于替换程序员的建议、在程序员建议之前使用、或在程序员建议之后使用)方面各不相同。

在大多数 CMMI 软件包中,运行 make 将列出每个命令并运行它,其间夹杂任何警告。但有些软件包试图“静默”,只显示正在编译或链接的文件,而不显示命令行。如果您需要检查命令,无论是由于错误,还是仅仅为了查看正在使用的选项和标志,在 make 调用中添加 V=1 可能会有所帮助。

CMake

CMake 的工作方式非常不同,它有两个可以在 BLFS 上使用的后端:makeninja。默认后端是 make,但 ninja 在多处理器的大型软件包上可能更快。要使用 ninja,请在 cmake 命令中指定 -G Ninja。然而,有些软件包在其 ninja 文件中会产生致命错误,但使用默认的 Unix Makefiles 却能成功构建。

使用 CMake 最难的部分是了解您可能希望指定哪些选项。获取软件包已知选项列表的唯一方法是运行 cmake -LAH 并查看该默认配置的输出。

关于 CMake 最重要的一点可能是它有多种 CMAKE_BUILD_TYPE 值,这些值会影响标志。默认情况下未设置此值,且不生成任何标志。环境中的任何 CFLAGSCXXFLAGS 都将被使用。如果程序员编写了任何调试断言,除非使用 -D NDEBUG,否则它们将被启用。以下 CMAKE_BUILD_TYPE 值将生成如下所示的标志,这些标志将 位于 环境中的任何标志 之后,并因此优先。

标志
Debug -g
Release -O3 -D NDEBUG
RelWithDebInfo -O2 -g -D NDEBUG
MinSizeRel -Os -D NDEBUG

CMake 试图生成安静的构建。要查看正在运行的命令的详细信息,请使用 make VERBOSE=1ninja -v

默认情况下,CMake 处理文件安装的方式与其他构建系统不同:如果文件已存在且不比要覆盖的文件更新,则该文件不会被安装。如果用户想记录文件属于哪个软件包,无论是使用 LD_PRELOAD 还是通过列出比时间戳更新的文件,这可能是个问题。可以通过在 环境 中将变量 CMAKE_INSTALL_ALWAYS 设置为 1 来更改默认设置,例如通过 export 导出。

Meson

Meson 与 CMake 有一些相似之处,但也有许多不同之处。要获取您可能希望更改的定义的详细信息,您可以查看通常位于顶层目录中的 meson_options.txt

如果您已经通过运行 meson 配置了软件包,现在希望更改一个或多个设置,您可以删除构建目录、重新创建它并使用更改后的选项,或者在构建目录内运行 meson configure,例如设置一个选项:

meson configure -D <某个选项>=true

如果这样做,文件 meson-private/cmd_line.txt 将显示 最后 使用的命令。

Meson 提供以下 buildtype 值,它们启用的标志 位于 环境中提供的任何标志 之后,并因此优先。

  • plain:不添加任何标志。这供发行版提供自己的 CFLAGSCXXFLAGSLDFLAGS。在 BLFS 中没有明显的理由使用此选项。

  • debug:-g —— 如果在 meson.build 或命令行中未指定任何内容,则为默认值。但它会产生庞大且缓慢的二进制文件,因此我们应在 BLFS 中覆盖它。

  • debugoptimized:-O2 -g —— 这是某些软件包的 meson.build 中指定的默认值。

  • release:-O3(偶尔某个软件包会在此处强制使用 -O2)—— 这是我们在 BLFS 中对大多数使用 Meson 构建系统的软件包使用的 buildtype。

对于某些软件包(例如 Mesa-26.1.3),release buildtype 隐含了 -D NDEBUG 标志。也可以通过传递 -D b_ndebug=true 显式提供它。

要查看使用 meson 的软件包中正在运行的命令的详细信息,请使用 ninja -v

Rustc 和 Cargo

大多数发布的 rustc 程序以 crate(源代码 tarball)形式提供,它们会查询服务器以检查依赖项的当前版本,然后根据需要下载它们。这些软件包使用 cargo --release 构建。理论上,您可以操作 RUSTFLAGS 来更改优化级别(--release 的默认值为 3,即 -Copt-level=3,类似于 -O3)或强制其为编译所在的机器构建,使用 -Ctarget-cpu=native,但实践中这似乎没有显著差异。

如果您通过直接运行 rustc 编译独立的 Rust 程序(作为未打包的 .rs 文件),您应指定 -O-Copt-level=2 的缩写)或 -Copt-level=3,否则它将进行未优化的编译并运行 慢得多。如果您正在为调试而编译程序,请将 -O-Copt-level= 选项替换为 -g,以生成带有调试信息的未优化程序。

ninja 一样,默认情况下 cargo 使用所有逻辑核。这通常可以通过导出 CARGO_BUILD_JOBS=<N> 或将 --jobs <N> 传递给 cargo 来解决。对于编译 rustc 本身,为 x.py 调用指定 --jobs <N>(连同 CARGO_BUILD_JOBS 环境变量,这看起来像“双保险”方法,但似乎是必要的)大部分有效。例外是运行 rustc 的测试时,至少从 rustc-1.42.0 开始,其中一些测试仍将使用所有在线 CPU。

使用 Cargo 离线构建

Cargo 的问题之一是所需的 crate 会在构建过程中动态下载,这使得 cargo build 无法离线运行。另一个令人烦恼的是所需的 crate 会下载到目录 ~/.cargo,这可能会弄乱用户或 root 的主目录。对于那些希望对 crate 的下载位置有更多控制权的人来说,可以在环境中将变量 CARGO_HOME 设置为所需位置,例如:

export CARGO_HOME=/sources/cargo

也可以在构建软件包之前一次性下载所有需要的 crate。我们将给出两种方法,可能需要根据所构建的特定软件包进行调整。最简单的方法是从包含顶层 Cargo.toml 文件的目录运行 cargo fetch 来下载所需的软件包。这些软件包将下载到 $CARGO_HOME(或 ~/.cargo),并且可以在构建指令中离线使用。但在某些情况下可能会失败,例如对于在其构建系统中重新定义 CARGO_HOME 的软件包。在这种情况下,以下指令(从包含顶层 Cargo.toml 的目录运行)将创建一个包含源代码 crate 的“vendor”目录,并离线使用它。请注意,cargo vendor 命令也会将 crate 下载到 $CARGO_HOME

cargo vendor    &&
mkdir -v .cargo &&
cat > .cargo/config.toml << EOF
[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "vendor"
EOF

之后,可以按照书中的说明运行指令。

优化构建

许多人倾向于通过提供 CFLAGSCXXFLAGS 来按需优化编译。有关 gcc 和 g++ 可用选项的介绍,请参阅 https://gcc.gnu.org/onlinedocs/gcc-16.1.0/gcc/Optimize-Options.html。相同内容也可以在 info gcc 中找到。

有些软件包默认使用 -O2 -g,另一些使用 -O3 -g,如果提供了 CFLAGSCXXFLAGS,它们可能会被添加到软件包的默认值中、替换软件包的默认值,甚至被忽略。关于一些桌面软件包的详细信息(大部分截至 2019 年 4 月)可在 https://www.linuxfromscratch.org/~ken/tuning/ 找到——特别是 README.txttuning-1-packages-and-notes.txttuning-notes-2B.txt。需要特别记住的是,如果您想尝试一些更有趣的标志,您可能需要强制详细构建以确认实际使用了哪些标志。

显然,如果您在优化自己的程序,可以花时间对其进行分析,如果太慢,甚至可能重新编写部分代码。但对于构建整个系统,这种方法不切实际。一般来说,-O3 通常比 -O2 产生更快的程序。指定 -march=native 也是有益的,但这意味着您不能将二进制文件移动到不兼容的机器上——这也适用于较新的机器,而不仅仅是较旧的机器。例如,为 amdfam10 编译的程序可以在旧 Phenom、Kaveri 和 Ryzen 上运行,但为 Kaveri 编译的程序不能在 Ryzen 上运行,因为缺少某些操作码。类似地,如果您为 Haswell 构建,并非所有内容都能在 SandyBridge 上运行。

[注意]

注意

请注意,-march 设置的名称并不总是与同名的微架构基线匹配。例如,基于 Skylake 的 Intel Celeron 处理器根本不支持 AVX,但 -march=skylake 假设支持 AVX 甚至 AVX2。

当 GCC 构建共享库时,默认启用名为“语义介入”的功能。当共享库引用具有外部链接和默认可见性的符号名称时,如果该符号同时存在于共享库和主可执行文件中,语义介入保证始终使用主可执行文件中的符号。此功能旨在使链接共享库和链接静态库的行为尽可能相似。如今只有少数软件包仍然依赖语义介入,但该功能在 GCC 中默认仍然开启,导致共享库的许多优化被禁用,因为它们与语义介入冲突。可以将 -fno-semantic-interposition 选项传递给 gccg++ 以禁用语义介入并为共享库启用更多优化。此选项被某些软件包(例如 Python-3.14.6)用作默认值,也是 Clang 的默认值。

还有其他各种选项,有些人声称它们有益。最坏的情况下,您会重新编译和测试,然后发现在您的使用场景中这些选项并未带来好处。

如果构建 Perl 或 Python 模块,通常使用的 CFLAGSCXXFLAGS 是那些“父”软件包所使用的。

对于 LDFLAGS,可以使用三个选项进行优化。它们使用起来相当安全,并且某些软件包的构建系统默认使用其中一些选项。

使用 -Wl,-O1,链接器将优化哈希表以加速动态链接。注意 -Wl,-O1 与编译器优化标志 -O1 完全无关。

使用 -Wl,--as-needed,链接器将忽略命令行中不必要的 -lfoo 选项,即仅当可执行文件或正在链接的共享库中确实引用了 libfoo 中的符号时,才会链接该共享库。这有时可以缓解由 libtool 引起的“对共享库的过度依赖”问题。

使用 -Wl,-z,pack-relative-relocs,链接器会为 PIE 和共享库生成更紧凑的相对重定位条目形式。这减小了链接后的 PIE 或共享库的大小,并加快了 PIE 或共享库的加载速度。

-Wl, 前缀是必要的,因为尽管变量名为 LDFLAGS,但其内容实际上是在链接阶段传递给 gcc(或 g++clang 等),而不是直接传递给 ld

构建加固选项

即使在桌面系统上,仍然存在大量可利用的漏洞。其中许多攻击通过浏览器中的 javascript 进行。通常,一系列漏洞被用于获取数据(有时或用于 pwn,即控制机器并安装 rootkit)。大多数商业发行版会应用各种加固措施。

过去曾有 Hardened LFS,其中 gcc(更旧的版本)被强制使用加固(并提供了按软件包关闭其中一些功能的选项)。当前的 LFS 和 BLFS 书籍通过将 PIE(-fPIE -pie)和 SSP(-fstack-protector-strong)作为 GCC 和 clang 的默认值来延续其部分精神。而且,链接器(ld)自 Binutils 2.27 起也默认启用了 -Wl,-z,relro,它使全局偏移表(GOT)的一部分变为不可变。这里要讨论的内容不同——首先您必须确保软件包确实使用了您添加的标志,而不是覆盖了它们。

对于成本合理的加固选项,上述“tuning”链接中有一些讨论(偶尔,其中一个或多个选项可能不适合某个软件包)。这些选项是 -D _FORTIFY_SOURCE=2(或 -D _FORTIFY_SOURCE=3,更安全但性能开销更大)和(对于 C++)-D _GLIBCXX_ASSERTIONS。在现代机器上,这些应该对运行速度影响很小,通常不会明显感觉到。

主流发行版使用更多,例如:

  • -Wl,-z,now:禁用延迟绑定以增强 -Wl,-z,relro,从而使整个 GOT 变为不可变。

  • -fstack-clash-protection:防止攻击者使用足够大且未充分检查的偏移量来跳越内核放置的堆栈保护页和 -fstack-protector=strong 放置的堆栈金丝雀,并从堆地址修改堆栈,反之亦然。

  • -ftrivial-auto-var-init=zero:如果某些变量未通过其他方式初始化,则用零字节填充它们。

  • -fcf-protection=full:利用 Intel 和 AMD CET 技术限制控制流转移指令的目标地址。要使它对某个软件包真正有效,所有为该软件包提供共享库的软件包都必须使用此选项构建,该软件包本身也必须使用此选项构建,Glibc 必须启用 --enable-cet 选项配置,并且系统必须在 Intel Tiger Lake 或更新版本、或 AMD Zen 3 或更新版本上运行。如果不满足这些条件,使用此选项编译的程序仍会运行,但不会真正受到 CET 保护。

在 GCC 14 中,选项 -fhardened 是启用上述所有加固选项的简写。它设置 -D _FORTIFY_SOURCE=3 而不是 -D _FORTIFY_SOURCE=2

您还可能遇到所谓的“用户空间 retpoline”(-mindirect-branch=thunk 等),这相当于 2018 年底应用于 Linux 内核的 spectre 缓解措施。内核缓解措施引起了大量关于性能损失的抱怨,如果您有生产服务器,您可能希望考虑测试它以及其他可用选项,以查看性能是否仍然足够。

虽然 gcc 有许多加固选项,但 clang/LLVM 的优势在其他地方。据说 gcc 提供的某些选项在 clang/LLVM 中效果较差。