本文将讨论在连接的设备中更新引导加载程序的问题。所讨论的原则适用于任何软件系统,我们将专门讨论运行 linux 的系统。
现代电子设备越来越复杂,并与互联网连接。作为一般规则,复杂性与安全性背道而驰,不安全的互联网连接设备已经成熟,罪魁祸首可以滥用。在设计这些系统时,我们必须假设所有软件都会有错误,其中一些错误将是可利用的漏洞。解决这些问题的第一步是确保软件更新可以传送到您的系统,最好是自动和无线 (OTA)。欧盟“消费者物联网网络安全:基线要求”标准草案 (ETSI EN 303 645),”具体包括及时、自动更新作为其要求之一。它确实为不可变的第一阶段引导加载程序例外,以最大程度地降低现场设备处于非引导状态(也称为“砖块 ”板)的风险。
本文将讨论更新连接设备中的引导加载程序的问题。请注意,虽然这里讨论的原则适用于任何软件系统,但我们将专门讨论运行 Linux 的系统。使用更小、更定制设计的系统可能会为这些系统提供更多独特的选择。
系统设计
图 1 显示了一个通用的 Linux 系统,其中包含可能更新的主要组件。存储介质将是某种块设备,例如 eMMC 或 SATA 硬盘驱动器。在该设备中,将有引导加载程序、内核、设备树(取决于所使用的 CPU)和包含构建系统所需的所有文件的根文件系统。在某些情况下会使用更复杂的架构,但出于讨论的目的,我们将其限制为最简单的情况。
诸如 Mender 、 swupdate等系统更新实用程序能够开箱即用地更新内核、设备树和根文件系统,并且在许多情况下,这种级别的可更新性就足够了。
引导加载程序是系统的组件,负责在上电时初始化系统,从 CPU 复位指令开始。它负责以下任务:
? 初始化和清理 RAM
? 将所有外设设置为已知的静止状态以避免意外中断。
? 加载并启动 Linux 内核
如前所述,所有软件都有错误,因此我们可以假设也会有引导加载程序错误。我们可以通过最小化引导加载程序的功能来减少攻击面,但是我们可以完全消除错误的风险。为什么更新引导加载程序比更新系统的其他组件更复杂?如果我们尝试有什么风险?如果我们不尝试会有什么风险?
具有无线功能的系统
此框图显示了能够进行强大的无线 (OTA) 更新的系统的基本系统设计。[4] 引导加载程序负责系统初始化并与 OTA 客户端交互以选择使用哪个内核、设备树和根文件系统。稳健性是通过具有运行 Linux 映像所需的组件的完全冗余来提供的。这可确保在 OTA 更新中断的情况下始终有一个已知良好的映像可以回滚。此外,这确保了完全原子更新,因为更新客户端是系统中唯一知道更新正在进行中的组件,直到更新完成并准备好运行。
OTA 更新的任何组件都可能导致设备无法正常工作,因此系统的稳健性与引导加载程序处理回滚到先前已知良好配置的能力直接相关。这意味着系统中必须有一个组件,它是不可变的以正确处理错误的更新。
引导加载程序更新
在大多数情况下,处理回滚的不可变组件是引导加载程序。在一个典型的嵌入式 Linux 应用程序中是Das U-Boot [5]。如果我们尝试更新引导加载程序,由于没有冗余,我们就有可能使我们的电路板变砖。如果在我们开始写入新的引导加载程序映像之后,但在写入完成之前,板电源循环,那么我们的映像包含旧版本的一部分和新版本的一部分。这种情况下的行为是未定义的,唯一的缓解措施是能够物理访问设备以编写正确的引导加载程序,通常使用 USB 或其他硬接线连接。
但是我们为什么要更新引导加载程序呢?至少,引导加载程序只是用作初始化硬件然后将控制权交给 Linux 内核的一种方式。由于功能有限,引导加载程序出现问题的风险已降至最低。
对于许多设计,这种风险水平是可以接受的,架构师可以决定在其部署的设备中不提供 OTA 引导加载程序更新。作为最后的手段,仍然可以使用硬连线机制。
然而,对于许多设计来说,这种风险水平被认为是不可接受的,并且必须为引导加载程序的 OTA 更新提供一些机制。此外,许多设计在引导加载程序中添加了更多功能;系统诊断或其他特定于应用程序的要求可能会在引导加载程序中实现,从而导致需要更新的可能性更大。那么我们该如何处理呢?
提供引导加载程序更新的选项
有许多选项可以更新引导加载程序。本讨论并非旨在提供完整的解决方案,而是对可能适用于您的设计的方法进行高级描述。每个都有其权衡。
选项 1:无冗余
如果特定应用程序可以接受砖块板的风险,那么您可以简单地尝试部署引导加载程序更新 OTA,并在它发生时处理后果。如果您的车队规模很小,并且物理访问设备的成本很低,那么这可能会很好。如果需要更新引导加载程序,并且 OTA 尝试失败,那么您的尝试并不会更糟。OTA 引导加载程序更新失败的情况与没有 OTA 引导加载程序更新能力的情况相同。即,您必须获得对设备的物理访问权限并使用制造商提供的机制来重新刷新引导加载程序。
选项 2:多阶段引导加载程序
此架构将引导加载程序功能分为两个阶段(或更多阶段,具体取决于您的设计的复杂性)。最终,这仍然需要在阶段 1 中使用不可变的代码。您在更新阶段 2 时确实具有冗余性和稳健性,因此如果您仔细选择在何处实现功能,您可以提供引导加载程序功能的 OTA 更新。这是一个不错的选择,因为不可变的第一阶段二进制文件中的代码量减少了,从而降低了整体风险。
U-Boot 使用 SPL(Secondary Program Loader)和 TPL(Tertiary Program Loader)实现多阶段引导。引入此机制是为了支持具有单独引导 ROM 的系统,这些引导 ROM 太小而无法存储完整的 U-Boot 映像。在这种情况下,U-Boot SPL 映像将包含足够的初始化代码来加载和启动完整的 U-Boot 映像,通常来自 MMC 等大型块设备。SPL 需要能够初始化足够的 RAM 和包含完整 U-Boot 映像的设备。
即使对于没有小型引导 ROM 限制的设备,我们也可以利用这种架构在第 2 阶段实现我们的可更新功能,同时在第 1 阶段保留最低限度,包括正确处理冗余块。
第 1 阶段存在问题,需要物理访问才能解决。鉴于第 1 阶段的功能减少,在许多情况下,这种风险水平是可以接受的。
选项 3:并行引导加载程序
许多板提供从多个设备启动的能力。例如,许多板可以从板载 eMMC 或可移动 SD/MMC 卡启动。或者,他们可以为引导加载程序使用专用的 NOR 闪存设备,但仍然能够从 eMMC 块媒体运行引导加载程序。
这些类型的板可以配置为将不可变引导加载程序存储在一个受支持的设备中,然后将 OTA 可更新引导加载程序存储在另一个设备中。通常,可更新的引导加载程序与根文件系统位于相同的媒体(即 eMMC)中,因此很容易更新。由于“备用”媒体中的引导加载程序是不可变的,因此可以依靠它从“标准”位置的引导加载程序的损坏 OTA 更新中恢复。
这种方法的问题是引导设备的选择通常需要物理访问电路板以移动跳线或更改开关设置。如果您的设备位于最终用户可以访问它们的位置,这可能是一个可行的选择,因为最终用户可以在出现故障时选择恢复媒体。这可以通过文件或支持人员的指示来完成。
一些系统使用外部硬件来选择引导加载程序。运行 RTOS 的小型 MCU 可以监控适当的系统活动,并在 Linux 系统未运行的情况下选择备用引导加载程序。使用外部源正确检测这可能会很棘手,但切换 GPIO 引脚或写入共享内存的看门狗定时器可能就足够了。这也是一个更复杂的设计,需要根据您的系统要求进行考虑。请注意,您可能需要考虑对 MCU 固件映像进行 OTA 更新,这是另一个复杂程度。
选项 4:eMMC 引导分区
eMMC [6]规范的 4.3 版需要 2 个单独的硬件引导分区。这些分区通常为每个 4MB,用于存储引导加载程序。这些分区在 Linux 用户空间是可读写的,但默认情况下它们是只读的;通过写入 /sys 伪文件系统中的文件来启用读写功能:
曼德的方法
利用 eMMC 引导分区,对分区的更新是原子的,并且独立于对根文件系统的更新。eMMC 引导分区之间没有自动故障转移,因此这并不能减轻由于引导加载程序更新失败而导致设备变砖的问题。但是,这确实可以轻松地为引导加载程序提供更新,而无需为根文件系统进行任何特定调整。
由于在提供引导加载程序更新时存在板卡风险和 OTA 更新过程的稳健性降低,Mender 不提供开箱即用的引导加载程序更新。正如所讨论的,很难以通用的方式进行,并且最终可能会非常特定于应用程序和硬件。Mender更新模块框架允许插件架构支持自定义更新类型。Mender 可以使用自定义更新模块支持任何任意有效负载类型。此插件架构允许提供处理特定负载类型的自定义脚本。允许在特定系统中更新引导加载程序可以使用更新模块来实现。 根据应用程序的需要以及所用硬件的能力,可以使用上述任何一种方法。
包起来
在现场部署的设备中上传系统引导加载程序存在许多风险。不合时宜的电源故障可能会使设备在现场陷入困境,从而导致潜在的昂贵的召回过程。但是,不提供引导加载程序更新机制可能会带来不可接受的风险,具体取决于特定应用程序的配置文件。我们介绍了许多允许引导加载程序更新的方法,并讨论了每种方法的优缺点。这有望让您作为系统设计人员为您的系统做出适当的选择,并帮助您在适当了解设计风险的情况下快速进入市场。
审核编辑:郭婷
评论
查看更多