便宜的物联网板的普及意味着它不仅会控制应用程序,还会控制整个软件平台。 那么,如何构建一个针对特定用途的交叉编译应用程序的自定义发行版呢? 正如 Michael J. Hammel 在这里解释的那样,它并不像你想象的那么难。
为什么要定制?
以前,许多嵌入式项目都使用现成的发行版,然后出于种种原因,再将它们剥离到只剩下基本的必需的东西。首先,移除不需要的包以减少占用的存储空间。在启动时,嵌入式系统一般不需要大量的存储空间以及可用存储空间。在嵌入式系统运行时,可能从非易失性内存中拷贝大量的操作系统文件到内存中。第二,移除用不到的包可以降低可能的攻击面。如果你不需要它们就没有必要把这些可能有漏洞的包挂在上面。最后,移除用不到包可以降低发行版管理的开销。如果在包之间有依赖关系,意味着任何一个包请求从上游更新,那么它们都必须保持同步。那样可能就会出现验证噩梦。
然而,从一个现有的发行版中去移除包并不像说的那样容易。移除一个包可能会打破与其它包保持的各种依赖关系,以及可能在上游的发行版管理中改变依赖。另外,由于一些包原生集成在引导或者运行时进程中,它们并不能轻易地简单地移除。所有这些都是项目之外的平台的管理,并且有可能会导致意外的开发延迟。
一个流行的选择是使用上游发行版供应商提供的构建工具去构建一个定制的发行版。无论是 Gentoo 还是 Debian 都提供这种自下而上的构建方式。这些构建工具中最为流行的可能是 Debian 的 debootstrap 实用程序。它取出预构建的核心组件并允许用户去精选出它们感兴趣的包来构建用户自己的平台。但是,debootstrap 最初仅在 x86 平台上可用,虽然,现在有了 ARM(也有可能会有其它的平台)选项。debootstrap 和 Gentoo 的 catalyst 仍然需要从本地项目中将依赖管理移除。
一些人认为让别人去管理平台软件(像 Android 一样)要比自己亲自管理容易的多。但是,那些发行版都是多用途的,当你在一个轻量级的、资源有限的物联网设备上使用它时,你可能会再三考虑从你手中被拿走的任何资源。
系统引导的基石
一个定制的 Linux 发行版要求许多软件组件。其中第一个就是工具链toolchain。工具链是用于编译软件的一套工具集。包括(但不限于)一个编译器、链接器、二进制操作工具以及标准的 C 库。工具链是为一个特定的目标硬件设备专门构建的。如果一个构建在 x86 系统上的工具链想要用于树莓派,那么这个工具链就被称为交叉编译工具链。当在内存和存储都十分有限的小型嵌入式设备上工作时,最好是使用一个交叉编译工具链。需要注意的是,即便是使用像 JavaScript 这样的需要运行在特定平台的脚本语言为特定用途编写的应用程序,也需要使用交叉编译工具链编译。
图 1. 编译依赖和引导顺序
交叉编译工具链用于为目标硬件构建软件组件。需要的第一个组件是引导加载程序bootloader。当计算机主板加电之后,处理器(可能有差异,取决于设计)尝试去跳转到一个特定的内存位置去开始运行软件。那个内存位置就是保存引导加载程序的地方。硬件可能有内置的引导加载程序,它可能直接从它的存储位置或者可能在它运行前首先拷贝到内存中。也可能会有多个引导加载程序。例如,第一阶段的引导加载程序可能位于硬件的 NAND 或者 NOR 闪存中。它唯一的功能是设置硬件以便于执行第二阶段的引导加载程序——比如,存储在 SD 卡中的可以被加载并运行的引导加载程序。
引导加载程序能够从硬件中取得足够的信息,将 Linux 加载到内存中并跳转到正确的位置,将控制权有效地移交到 Linux。Linux 是一个操作系统。这意味着,在这种设计中,它除了监控硬件和向上层软件(也就是应用程序)提供服务外,它实际上什么都不做。Linux 内核中通常是各种各样的固件块。那些预编译的软件对象,通常包含硬件平台使用的设备的专用 IP(知识资产)。当构建一个定制发行版时,在开始编译内核之前,它可能会要求获得一些 Linux 内核源代码树没有提供的必需的固件块。
应用程序保存在根文件系统中,这个根文件系统是通过编译构建的,它集合了各种软件库、工具、脚本以及配置文件。总的来说,它们都提供各种服务,比如,网络配置和 USB 设备挂载,这些都是将要运行的项目应用程序所需要的。
总的来说,一个完整的系统构建要求下列的组件:
一个交叉编译工具链
一个或多个引导加载程序
Linux 内核和相关的固件块
一个包含库、工具以及实用程序的根文件系统
定制的应用程序
使用适当的工具开始构建
交叉编译工具链的组件可以手工构建,但这是一个很复杂的过程。幸运的是,现有的工具可以很容易地完成这一过程。构建交叉编译工具链的最好工具可能是 Crosstool-NG,这个工具使用了与 Linux 内核相同的 kconfig 菜单系统来构建工具链的每个细节和方面。使用这个工具的关键是,为目标平台找到正确的配置项。配置项通常包含下列内容:
目标架构,比如,是 ARM 还是 x86。
字节顺序:小端字节顺序(一般情况下,Intel 采用这种顺序)还是大端字节顺序(一般情况下,ARM 或者其它的平台采用这种顺序)。
编译器已知的 CPU 类型,比如,GCC 可以使用 -mcpu 或 --with-cpu。
支持的浮点类型,如果有的话,比如,GCC 可以使用 -mfpu 或 --with-fpu。
二进制实用工具binutils、C 库以及 C 编译器的特定版本信息。
图 2. Crosstool-NG 配置菜单
前四个一般情况下可以从处理器制造商的文档中获得。对于较新的处理器,它们可能不容易找到,但是,像树莓派或者 BeagleBoards(以及它们的后代和分支),你可以在像 嵌入式 Linux Wiki 这样的地方找到相关信息。
二进制实用工具、C 库、以及 C 编译器的版本,将与任何第三方提供的其它工具链分开。首先,它们中的每一个都有多个提供者。Linaro 为最新的处理器类型提供了最先进的版本,同时致力于将该支持合并到像 GNU C 库这样的上游项目中。尽管你可以使用各种提供者的工具,你可能依然想去使用现成的 GNU 工具链或者相同的 Linaro 版本。
在 Crosstool-NG 中的另外的重要选择是 Linux 内核的版本。这个选择将得到用于各种工具链组件的头文件headers,但是它没有必要一定与你在目标硬件上将要引导的 Linux 内核相同。选择一个不比目标硬件的内核更新的 Linux 内核是很重要的。如果可能的话,尽量选择一个比目标硬件使用的内核更老的长周期支持(LTS)的内核。
对于大多数不熟悉构建定制发行版的开发者来说,工具链的构建是最为复杂的过程。幸运的是,大多数硬件平台的二进制工具链都可以想办法得到。如果构建一个定制的工具链有问题,可以在线搜索像 嵌入式 Linux Wiki 这样的地方去查找预构建工具链。
引导选项
在构建完工具链之后,接下来的工作是引导加载程序。引导加载程序用于设置硬件,以便于越来越复杂的软件能够使用这些硬件。第一阶段的引导加载程序通常由目标平台制造商提供,它通常被烧录到类似于 EEPROM 或者 NOR 闪存这类的在硬件上的存储中。第一阶段的引导加载程序将使设备从这里(比如,一个 SD 存储卡)开始引导。树莓派的引导加载程序就是这样的,它样做也就没有必要再去创建一个定制引导加载程序。
尽管如此,许多项目还是增加了第二阶段的引导加载程序,以便于去执行一个多样化的任务。在无需使用 Linux 内核或者像 plymouth 这样的用户空间工具的情况下提供一个启动动画,就是其中一个这样的任务。一个更常见的第二阶段引导加载程序的任务是去提供基于网络的引导或者使连接到 PCI 上的磁盘可用。在那种情况下,一个第三阶段的引导加载程序,比如 GRUB,可能才是让系统运行起来所必需的。
最重要的是,引导加载程序加载 Linux 内核并使它开始运行。如果第一阶段引导加载程序没有提供一个在启动时传递内核参数的机制,那么,在第二阶段的引导加载程序中就必须要提供。
有许多的开源引导加载程序可以使用。U-Boot 项目 通常用于像树莓派这样的 ARM 平台。CoreBoot 一般是用于像 Chromebook 这样的 x86 平台。引导加载程序是特定于目标硬件专用的。引导加载程序的选择总体上取决于项目的需求以及目标硬件(可以去网络上在线搜索开源引导加载程序的列表)。
现在到了 Linux 登场的时候
引导加载程序将加载 Linux 内核到内存中,然后去运行它。Linux 就像一个扩展的引导加载程序:它进行进行硬件设置以及准备加载高级软件。内核的核心将设置和提供在应用程序和硬件之间共享使用的内存;提供任务管理器以允许多个应用程序同时运行;初始化没有被引导加载程序配置的或者是已经配置了但是没有完成的硬件组件;以及开启人机交互界面。内核也许不会配置为在自身完成这些工作,但是,它可以包含一个嵌入的、轻量级的文件系统,这类文件系统大家熟知的有 initramfs 或者 initrd,它们可以独立于内核而创建,用于去辅助设置硬件。
内核操作的另外的事情是去下载二进制块(通常称为固件)到硬件设备。固件是用特定格式预编译的对象文件,用于在引导加载程序或者内核不能访问的地方去初始化特定硬件。许多这种固件对象可以从 Linux 内核源仓库中获取,但是,还有很多其它的固件只能从特定的硬件供应商处获得。例如,经常由它们自己提供固件的设备有数字电视调谐器或者 WiFi 网卡等。
固件可以从 initramfs 中加载,也或者是在内核从根文件系统中启动 init 进程之后加载。但是,当你去创建一个定制的 Linux 发行版时,创建内核的过程常常就是获取各种固件的过程。
轻量级核心平台
Linux 内核做的最后一件事情是尝试去运行一个被称为 init 进程的专用程序。这个专用程序的名字可能是 init 或者 linuxrc 或者是由加载程序传递给内核的名字。init 进程保存在一个能够被内核访问的文件系统中。在 initramfs 这种情况下,这个文件系统保存在内存中(它可能是被内核自己放置到那里,也可能是被引导加载程序放置在那里)。但是,对于运行更复杂的应用程序,initramfs 通常并不够完整。因此需要另外一个文件系统,这就是众所周知的根文件系统。
图 3. 构建 root 配置菜单
initramfs 文件系统可以使用 Linux 内核自身构建,但是更常用的作法是,使用一个被称为 BusyBox 的项目去创建。BusyBox 组合许多 GNU 实用程序(比如,grep 或者 awk)到一个单个的二进制文件中,以便于减小文件系统自身的大小。BusyBox 通常用于去启动根文件系统的创建过程。
但是,BusyBox 是特意轻量化设计的。它并不打算提供目标平台所需要的所有工具,甚至提供的工具也是经过功能简化的。BusyBox 有一个“姊妹”项目叫做 Buildroot,它可以用于去得到一个完整的根文件系统,提供了各种库、实用程序,以及脚本语言。像 Crosstool-NG 和 Linux 内核一样,BusyBox 和 Buildroot 也都允许使用 kconfig 菜单系统去定制配置。更重要的是,Buildroot 系统自动处理依赖关系,因此,选定的实用程序将会保证该程序所需要的软件也会被构建并安装到 root 文件系统。
Buildroot 可以用多种格式去生成一个根文件系统包。但是,需要重点注意的是,这个文件系统是被归档的。单个的实用程序和库并不是以 Debian 或者 RPM 格式打包进去的。使用 Buildroot 将生成一个根文件系统镜像,但是它的内容不是单独的包。即使如此,Buildroot 还是提供了对 opkg 和 rpm 包管理器的支持的。这意味着,虽然根文件系统自身并不支持包管理,但是,安装在根文件系统上的定制应用程序能够进行包管理。
交叉编译和脚本化
Buildroot 的其中一个特性是能够生成一个临时树。这个目录包含库和实用程序,它可以被用于去交叉编译其它应用程序。使用临时树和交叉编译工具链,使得在主机系统上而不是目标平台上对 Buildroot 之外的其它应用程序编译成为可能。使用 rpm 或者 opkg 包管理软件之后,这些应用程序可以在运行时使用包管理软件安装在目标平台的根文件系统上。
大多数定制系统的构建都是围绕着用脚本语言构建应用程序的想法去构建的。如果需要在目标平台上运行脚本,在 Buildroot 上有多种可用的选择,包括 Python、PHP、Lua 以及基于 Node.js 的 JavaScript。对于需要使用 OpenSSL 加密的应用程序也提供支持。
接下来做什么
Linux 内核和引导加载程序的编译过程与大多数应用程序是一样的。它们的构建系统被设计为去构建一个专用的软件位。Crosstool-NG 和 Buildroot 是元构建metabuild。元构建是将一系列有自己构建系统的软件集合封装为一个构建系统。可靠的元构建包括 Yocto 和 OpenEmbedded。Buildroot 的好处是可以将更高级别的元构建进行轻松的封装,以便于将定制 Linux 发行版的构建过程自动化。这样做之后,将会打开 Buildroot 指向到项目专用的缓存仓库的选项。使用缓存仓库可以加速开发过程,并且可以在无需担心上游仓库变化的情况下提供构建快照。
一个实现高级构建系统的示例是 PiBox。PiBox 就是封装了在本文中讨论的各种工具的一个元构建。它的目的是围绕所有工具去增加一个通用的 GNU Make 目标架构,以生成一个核心平台,这个平台可以构建或分发其它软件。PiBox 媒体中心和 kiosk 项目是安装在核心平台之上的应用层软件的实现,目的是用于去产生一个构建平台。Iron Man 项目 是为了家庭自动化的目的而扩展了这种应用程序,它集成了语音管理和物联网设备的管理。
但是,PiBox 如果没有这些核心的软件工具,它什么也做不了。并且,如果不去深入了解一个完整的定制发行版的构建过程,那么你将无法正确运行 PiBox。而且,如果没有 PiBox 开发团队对这个项目的长期奉献,也就没有 PiBox 项目,它完成了定制发行版构建中的大量任务。
评论
查看更多