本文是对mcuboot docs/design.md文档的翻译,水平有限,如有错误欢迎指正。
mcuboot github链接
本文档的链接
Bootloader
总结
mcuboot包含了两个软件包:
1)bootutil library(目录:boot/bootutil)
2)boot application(每个移植示例都有自已的应用,目录在boot/port)
bootutil拥有bootloader的大部分功能,特别是,缺少的部分是最后跳转到应用镜像(application image)这一步骤,最后缺少的这一步骤应该由boot application来实现。bootloader功能上以这种方式分离,以便支持单元测试。library可以进行单元测试,但是application不能。因此,尽可能将功能委托给bootutil library.
限制
当前bootloader仅支持拥有如下特性的image:
1)编译出且从flash运行
2)编译出且从一个固定的位置运行
Image格式
如下的定义用来描述image格式:
#define IMAGE_MAGIC 0x96f3b83d
#define IMAGE_HEADER_SIZE 32
struct image_version {
uint8_t iv_major;
uint8_t iv_minor;
uint16_t iv_revision;
uint32_t iv_build_num;
};
/** Image 头部. 所有栏位使用小端模式. */
struct image_header {
uint32_t ih_magic;
uint32_t ih_load_addr;
uint16_t ih_hdr_size; /* image头部的大小 (bytes). */
uint16_t ih_protect_tlv_size; /* 受保护的TLV大小 (bytes). */
uint32_t ih_img_size; /* image大小,不包括头部. */
uint32_t ih_flags; /* IMAGE_F_[...]. */
struct image_version ih_ver;
uint32_t _pad1;
};
#define IMAGE_TLV_INFO_MAGIC 0x6907
#define IMAGE_TLV_PROT_INFO_MAGIC 0x6908
/** Image TLV header. All fields in little endian. */
struct image_tlv_info {
uint16_t it_magic;
uint16_t it_tlv_tot; /* size of TLV area (including tlv_info header) */
};
/** Image trailer TLV format. All fields in little endian. */
struct image_tlv {
uint8_t it_type; /* IMAGE_TLV_[...]. */
uint8_t _pad;
uint16_t it_len; /* Data length (not including TLV header). */
};
/*
* Image header flags.
*/
#define IMAGE_F_PIC 0x00000001 /* Not supported. */
#define IMAGE_F_NON_BOOTABLE 0x00000010 /* Split image app. */
#define IMAGE_F_RAM_LOAD 0x00000020
/*
* Image trailer TLV types.
*/
#define IMAGE_TLV_KEYHASH 0x01 /* hash of the public key */
#define IMAGE_TLV_SHA256 0x10 /* SHA256 of image hdr and body */
#define IMAGE_TLV_RSA2048_PSS 0x20 /* RSA2048 of hash output */
#define IMAGE_TLV_ECDSA224 0x21 /* ECDSA of hash output */
#define IMAGE_TLV_ECDSA256 0x22 /* ECDSA of hash output */
#define IMAGE_TLV_RSA3072_PSS 0x23 /* RSA3072 of hash output */
#define IMAGE_TLV_ED25519 0x24 /* ED25519 of hash output */
#define IMAGE_TLV_ENC_RSA2048 0x30 /* Key encrypted with RSA-OAEP-2048 */
#define IMAGE_TLV_ENC_KW128 0x31 /* Key encrypted with AES-KW-128 */
#define IMAGE_TLV_ENC_EC256 0x32 /* Key encrypted with ECIES-P256 */
#define IMAGE_TLV_ENC_X25519 0x33 /* Key encrypted with ECIES-X25519 */
#define IMAGE_TLV_DEPENDENCY 0x40 /* Image depends on other image */
#define IMAGE_TLV_SEC_CNT 0x50 /* security counter */
可选项目TLV(type-length-value) 记录器包含了image的元数据,并且TLV的位置紧随image后。
ih_protect_tlv_size
栏位指示受保护的TLV区域的大小。如果存在受保护的 TLV,则包含IMAGE_TLV_PROT_INFO_MAGIC
的魔数(magic)的 TLV 信息标头必须存在,并且受保护的 TLV(加上信息头本身)必须包含在哈希计算中。 否则哈希仅在image头部和image本身上计算。 在这个情况下(没有受保护的TLV), ih_protect_tlv_size
字段的值为 0。
ih_hdr_size
字段指示标头的长度,因此也是image本身的偏移量。 该字段提供向后兼容,以便支持image格式变化的情况。
Flash分布
设备的flash根据其flash映射进行分区。 在高级别上,flash隐射将数字ID映射到flash区域。 flash区域是具有以下属性的磁盘区域:
- 可以完全擦除一个区域而不影响任何其他区域。
- 对一个区域的写入不限制对其他区域的写入。
bootloader使用以下flash区域 ID:
/* Independent from multiple image boot */
#define FLASH_AREA_BOOTLOADER 0
#define FLASH_AREA_IMAGE_SCRATCH 3
/* If the boot loader is working with the first image */
#define FLASH_AREA_IMAGE_PRIMARY 1
#define FLASH_AREA_IMAGE_SECONDARY 2
/* If the boot loader is working with the second image */
#define FLASH_AREA_IMAGE_PRIMARY 5
#define FLASH_AREA_IMAGE_SECONDARY 6
bootloader区域包含bootloader镜像本身。 其他区域是在后续章节中描述。 flash可以包含多个可执行文件,因此主要和次要区域的flash区域 ID是基于活动镜像的数量进行分配的(bootloader当前正在其上运行)。
image槽
flash的一部分可以划分为多个镜像区,每个镜像区包含两个镜像槽:一个主槽和一个辅助槽。通常情况下,bootloader只会运行主槽中的镜像,因此必须构建镜像,以便它们可以从flash中的固定位置运行(例外情况是 direct-xip 和 ram-load 升级模式)。 如果bootloader需要运行驻留在辅助插槽中的镜像,则必须先将其内容复制到主插槽中,方法是交换两个镜像或覆盖主插槽的内容。 bootloader支持基于交换或覆盖的镜像升级,但必须在构建时进行配置以选择这两种策略之一。
使用scratch的交换
当使用此交换策略时,除了镜像区域的插槽外,bootloader还需要一个scratch(暂存区)以允许可靠的镜像交换。 scratch区的大小必须足以存储至少要交换的最大扇区。 许多设备具有大小相同的小flash扇区,例如 4K,而其他设备具有可变大小的扇区,其中最大扇区可能为 128K 或 256K,因此scratch(暂存区)必须足够大才能存储它。 scratch 仅在交换固件时使用,这意味着仅在进行升级时使用。 鉴于此,主要使用较大尺寸的scratch的原因是flash磨损分布更均匀,因为例如,单个扇区的写入次数是使用两个扇区的两倍。 要为您的用例评估scratch的理想尺寸,以下参数是相关的:
-
镜像大小/scratch大小的比率
-
flash硬件支持的擦除周期数
使用镜像大小(而不是插槽大小)是因为仅复制实际用于存储镜像的插槽扇区。 镜像/scratch比率是每次升级时擦除scrach的次数。 擦除周期数除以镜像/划痕比率将为您提供在设备超出规格之前可以执行升级的次数。
num_upgrades = number_of_erase_cycles / (image_size / scratch_size)
例如,假设一个设备具有 10000 个擦除周期,镜像大小为 150K,划痕为 4K(4K 扇区设备的通常最小大小)。 这将导致总共:10000 / (150 / 4) ~ 267
增加scratch大小到16K,我们将得到:10000 / (150 / 16) ~ 1067
没有最佳比率,因为正确的大小取决于用例。 需要考虑的因素包括设备在现场和开发期间升级的次数,以及任何所需的安全余量,制造商指定的擦除周期数。 通常,建议使用允许在生产中进行数百到数千次现场升级的比率。swap-using scratch 算法假定主插槽和次插槽区域大小相等。 该应用程序可用的最大镜像尺寸为:
maximum-image-size = image-slot-size - image-trailer-size
其中image-slot-size是镜像插槽的大小,image-trailer-size是镜像尾部大小。
不使用scratch的交换
该算法是 swap-using-scratch 算法的替代方案。 它使用主插槽中的附加扇区来实现交换。 该算法的工作原理如下:
- 将主插槽的所有扇区向上移动一个扇区。 从 N=0 开始:
- 将第 N 个扇区从次插槽复制到主插槽的第 N 个扇区。
- 将第 (N+1) 个扇区从主槽复制到辅助槽的第 N 个扇区。
- 重复第 2 步和第 3 步,直到交换所有插槽的扇区。
该算法被设计成使得主插槽的较高扇区仅用于扇区向上移动。 因此,内存大小最有效的插槽布局是主插槽恰好比辅助插槽大一个扇区,尽管也允许使用相同大小的插槽。 该算法仅限于支持相同扇区布局的扇区。 所有插槽的扇区应具有相同的大小。 使用此算法时,应用程序可用的最大图像大小为:
maximum-image-size = (N-1) * slot-sector-size - image-trailer-sectors-size
其中: N 是主槽中的扇区数。 image-trailer-sectors-size 是镜像尾部的大小,向上取整为其占用的扇区的总大小。 例如,如果 image-trailer-size 等于 1056 B,扇区大小等于 1024 B,则 image-trailer-sectors-size 将等于 2048 B。
该算法在每次交换期间在主插槽上执行两次擦除,在辅助插槽上执行一个。 假设 DFU 应用程序接收新镜像需要在辅助插槽上执行 1 个擦除周期,这应该会使得插槽之间的flash磨损均衡。
该算法使用 MCUBOOT_SWAP_USING_MOVE 选项启用。
direct xip模式
当启用 direct-xip 模式时,活动镜像标志在镜像升级期间在插槽之间“移动”,与上述相反,bootloader可以直接从主插槽或辅助插槽运行镜像(没有必须将其移动/复制到主插槽中)。 因此,下载新镜像的镜像更新客户端必须知道哪个插槽包含活动镜像,哪个插槽充当临时区域,它负责将正确的镜像加载到正确的插槽中。 所有这一切都需要镜像构建为从相应的插槽执行。 在启动时,bootloader首先在插槽中查找镜像,然后检查镜像标头中的版本号。 它选择最新的镜像(具有最高版本号)和然后检查其有效性(完整性检查、签名验证等)。 如果镜像无效,MCUboot 会擦除其内存槽并开始验证其他镜像。 在成功验证所选镜像后,bootloader会链式加载它。
还支持额外的“还原”机制。 更多信息,请阅读对应部分。 将主槽和副槽同等对待有其缺点。 由于镜像不会在插槽之间移动,因此无法支持动态镜像加密/解密(它仅适用于将镜像存储在设备的外部flash中,加密镜像数据的传输仍然可行 ).覆盖和 direct-xip 升级策略比镜像交换策略更容易实现,特别是因为bootloader必须正常工作,即使它在镜像交换中间被重置。 出于这个原因,文档的其余部分描述了它在配置为在升级过程中交换镜像时的行为。
RAM loading模式
在 ram-load 模式下,插槽是相等的。 与 direct-xip 模式一样,此模式也通过读取镜像头中的镜像版本号来选择最新的镜像。 但是不是就地执行它,而是将最新的镜像复制到用于执行的 RAM。 加载地址,即镜像被复制到的 RAM 中的位置,存储在镜像头中。 当 SoC 中没有内部flash时,ram-load 升级模式很有用,但有足够大的内部 RAM 来保存镜像。 通常在这种情况下,镜像存储在外部存储设备中。 从外部存储执行有一些缺点(执行速度较低,镜像容易受到攻击)因此在认证和执行之前,镜像总是被复制到内部 RAM 中。 Ram-load 模式要求要构建的镜像从 RAM 地址范围而不是存储设备地址范围执行。 如果启用 ram-load,则平台必须定义以下参数:
#define IMAGE_EXECUTABLE_RAM_START <area_base_addr>
#define IMAGE_EXECUTABLE_RAM_SIZE <area_size_in_bytes>
启用 ram-load 后,在对镜像进行签名时,还必须使用 imgtool
脚本的 --load-addr <addr>
选项。 此选项在镜像标头中设置 RAM_LOAD
标志,指示应将镜像加载到RAM 并在镜像头中设置加载地址。
ram-load模式目前只支持单镜像启动,不支持镜像加密功能。
Boot交换类型
当设备在正常情况下首次启动时,每个主插槽中会有一个最新的固件镜像,mcuboot 可以验证然后加载。 在这种情况下,不需要镜像交换。 然而,在设备升级过程中,新的候选镜像出现在次级插槽中,这如上所述,mcuboot 必须在启动前换入主插槽。通过交换将旧镜像升级为新镜像可以分为两个步骤。 在这个过程中,mcuboot 执行flash中镜像数据的“测试”交换并启动新镜像,否则它将在操作期间执行。 然后新镜像可以在运行时更新 flash 的内容以将其自身标记为“OK”,并且 mcuboot 将仍然选择在下次启动时运行它。 发生这种情况时,交换是“永久”。 如果这没有发生,mcuboot 将执行“还原”交换在下次引导期间通过将镜像交换回其原始位置,并尝试启动旧镜像。根据用例,第一次交换也可以直接永久化。在这种情况下,mcuboot 将永远不会尝试在下一次重置时恢复镜像。
支持测试交换以提供回滚机制以防止设备从被坏固件“变砖”。 如果设备立即崩溃启动新的(坏的)镜像后,mcuboot 在下一次设备重置时将恢复为旧的(工作的)镜像,而不是再次启动坏镜像。 这使得仅在执行自检成功后才使“测试交换”永久生效的设备固件。
在启动时,mcuboot 检查flash的内容来决定每个镜像执行哪些“交换类型”; 这个决定决定了它如何收益。
可能的交换类型及其含义是:
-
BOOT_SWAP_TYPE_NONE
:“通常”或“无升级”情况; 尝试启动主插槽的内容。 -
BOOT_SWAP_TYPE_TEST
:通过交换引导辅助插槽的内容镜像。 除非交换是永久性的,否则在下次启动时恢复。 -
BOOT_SWAP_TYPE_PERM
:永久交换镜像,并启动升级后的镜像固件。 -
BOOT_SWAP_TYPE_REVERT
:之前的测试交换不是永久的;换回旧镜像,其数据现在位于辅助插槽中。 如果旧镜像在引导时将其自身标记为“OK”,下一次引导将具有交换类型BOOT_SWAP_TYPE_NONE
。 -
BOOT_SWAP_TYPE_FAIL
:交换失败,因为要运行的镜像无效。 -
BOOT_SWAP_TYPE_PANIC
:交换遇到不可恢复的错误。
“交换类型”是引导启动的高级表示。 随后的部分描述了 mcuboot 如何确定交换类型flash 的位级内容。
direct-xip模式下的回退机制
direct-xip 模式还支持“恢复”机制,相当于交换模式的“恢复”交换。 它可以通过 MCUBOOT_DIRECT_XIP_REVERT 配置选项启用,并且还必须将镜像尾部添加到签名镜像中(必须使用 imgtool
脚本的“–pad”选项)。 有关这方面的更多信息,请阅读 Image Trailer 部分和 imgtool 文档。 与交换模式一样,也支持使镜像永久化(将它们标记为提前确认)。 direct-xip 模式的“还原”机制的各个步骤如下:
- 选择存放最新潜在镜像的插槽。
- 是否是之前选择要运行的镜像(在之前的引导期间)?
- 是:镜像是否将自己标记为“OK”(自检成功)?
- 是的。
- 继续第 3 步。
- 没有。
- 从插槽中删除镜像以防止它在下次启动时再次被选中。
- 返回到步骤 1(bootloader将尝试选择,并且如果有的话,可能会启动以前的镜像)。
- 是的。
- 没有。
- 将镜像标记为“已选择”(在image尾部中设置 copy_done 标志)。
- 继续第 3 步。
- 是:镜像是否将自己标记为“OK”(自检成功)?
- 进行镜像验证…
镜像尾部(Image Trailer)
为了使bootloader能够确定当前状态以及在当前引导操作期间应采取什么操作,它使用存储在镜像flash区域中的元数据。 交换时,其中一些元数据会临时复制到scratch区或从scratch区移出。此元数据位于镜像flash区域的末尾,称为镜像尾部。 镜像尾部具有以下结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
~ ~
~ Swap status (BOOT_MAX_IMG_SECTORS * min-write-size * 3) ~
~ ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Encryption key 0 (16 octets) [*] |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Encryption key 1 (16 octets) [*] |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Swap size (4 octets) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Swap info | 0xff padding (7 octets) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Copy done | 0xff padding (7 octets) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Image OK | 0xff padding (7 octets) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MAGIC (16 octets) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*:星标表示,仅当加密选项启用时才显示(MCUBOOT_ENC_IMAGES)。
紧跟在这样一个记录之后的偏移量表示下一个记录的开始。
注意:“min-write-size”是flash硬件的属性。如果硬件允许将单个字节写入任意地址,那么Min-write-size为1。如果硬件只允许写偶数地址, 那么min-write-size为2,依此类推。
一个镜像追踪器拥有如下几个区域:
1.交换状态(swap status):记录镜像交换进度的一系列记录。 要交换整个镜像,一次在两个镜像区域之间交换一个或多个扇区,需要如下几个步骤:
- 主插槽中的扇区数据被复制到scratch,然后被擦除
- 辅助槽中的扇区数据被复制到主槽中, 然后擦除
- scratch 中的扇区数据被复制到辅助插槽中
当交换镜像时,bootloader更新“交换状态”字段,以允许它计算每个扇区的交换操作进度。 因此,如果bootloader在交换操作正在进行并稍后重置时暂停,则交换状态字段可用于恢复交换操作。 BOOT_MAX_IMG_SECTORS
值是 mcuboot 为每个镜像支持的可配置最大扇区数; 它的值默认为 128,但允许要么减小这个大小,以限制 RAM 的使用,要么在具有大量flash或非常小的扇区的设备中增加它,因此需要更大的配置以允许处理所有插槽的扇区。 min-write-sz 的系数是由于flash硬件的行为。
-
加密密钥:密钥加密密钥 (KEK)。 镜像加密和解密需要这些密钥。 有关详细信息,请参阅 加密镜像 文档。
-
交换大小:当开始新的交换操作时,需要交换的总大小(基于具有最大镜像的插槽 + TLV)被写入此位置,以便在执行交换时重置的情况下更容易恢复。
-
Swap info:占用一个字节,包含如下信息:
交换类型:存储在bit 0-3 中。 指示正在进行的交换操作的类型。 当 mcuboot 恢复中断的交换时,它使用此字段来确定要执行的操作类型。 该字段包含下表中的以下值之一。
| Name | Value |
| ------------------------- | ----- |
| `BOOT_SWAP_TYPE_TEST` | 2 |
| `BOOT_SWAP_TYPE_PERM` | 3 |
| `BOOT_SWAP_TYPE_REVERT` | 4 |
镜像编号:存储在bit 4-7 中。 它在单镜像启动时始终为 0 值。 在多镜像引导的情况下,它指示中断发生时交换了哪个镜像。 在所有镜像交换操作的情况下,使用相同的划痕区域。因此,如果在恢复交换操作时在scratch区发现启动状态,则该字段用于确定尾部属于哪个镜像。
5.拷贝完成标志(copy done):一个字节的信息,用来指示这个插槽中的镜像是否拷贝完成(0x01:完成,0xff:没有完成)。
6.镜像有效标志(image ok):一个字节的信息,用来指示这个插槽中的镜像是否被用户确认完整有效(0x01:确认过,0xff:没有确认)。
7.魔数(magic):以下的16字节,以主机字节序列(host-byte-order HBO)写入:
const uint32_t boot_img_magic[4] = {
0xf395c277,
0x7fefd260,
0x0f505235,
0x8079b62c,
};
在启动时,bootloader通过检查镜像尾部来确定引导交换类型。 当使用术语“镜像尾部”时,它的意思是两个镜像插槽的尾部提供的聚合信息。
新的交换(不涉及恢复)
对于新的交换,mcuboot 必须检查一组字段以确定要执行哪个交换操作。
镜像尾部记录是围绕flash硬件施加的限制构建的。 因此,它们没有非常直观的设计,仅通过查看镜像尾部很难了解设备的状态。 最好通过一组表将所有可能的尾部
状态映射到上述交换类型。 这些表格转载如下。
(注意:关于下面描述的表格的一个重要警告是它们必须按照此处显示的顺序进行评估。 在测试镜像尾部时,较低的状态编号必须具有较高的优先级。)
State I
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Any | Good |
image-ok | Any | Unset |
copy-done | Any | Any |
-----------------+--------------+----------------'
result: BOOT_SWAP_TYPE_TEST |
-------------------------------------------------'
State II
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Any | Good |
image-ok | Any | 0x01 |
copy-done | Any | Any |
-----------------+--------------+----------------'
result: BOOT_SWAP_TYPE_PERM |
-------------------------------------------------'
State III
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Good | Unset |
image-ok | 0xff | Any |
copy-done | 0x01 | Any |
-----------------+--------------+----------------'
result: BOOT_SWAP_TYPE_REVERT |
-------------------------------------------------'
上述任何一种状态将导致mcuboot尝试执行镜像交换。否则,mcuboot不会尝试执行镜像交换,而是如下三种交换类型中的一种,他们被描述为状态IV。
State IV
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Any | Any |
image-ok | Any | Any |
copy-done | Any | Any |
-----------------+--------------+----------------'
result: BOOT_SWAP_TYPE_NONE, |
BOOT_SWAP_TYPE_FAIL, or |
BOOT_SWAP_TYPE_PANIC |
-------------------------------------------------'
在状态 IV 中,当没有错误发生时,mcuboot 将尝试直接引导主插槽的内容,结果为 BOOT_SWAP_TYPE_NONE
。 如果主插槽中的镜像无效,则结果为“BOOT_SWAP_TYPE_FAIL”。 如果在引导期间发生致命错误,则结果为 BOOT_SWAP_TYPE_PANIC
。 如果结果是 BOOT_SWAP_TYPE_FAIL
或 BOOT_SWAP_TYPE_PANIC
,mcuboot 将挂起而不是引导无效或受损的镜像。
注意:上面的一个重要警告是当请求交换并且由于散列或签名错误而无法验证辅助插槽中的镜像时的结果。 此状态的行为与状态 IV 一样,带有将主插槽中的镜像标记为“OK”的额外操作,以防止进一步尝试交换。
恢复(之前被中断的)交换
如果 mcuboot 确定它正在恢复中断的交换(即,在交换中间发生重置),它会通过从活动尾部读取“交换信息”字段并从位 0-3 中提取交换类型来完全确定要恢复的操作 . 上一节中的表格集在恢复案例中不是必需的。
高级操作
定义好术语后,我们现在可以探索bootloader的操作。 首先,提供了引导过程的高级概述。 然后,以下部分更详细地描述了该过程的每个步骤。
程序步骤:
1.检查swap状态区域; 是否恢复中断的交换?
- 是:完成部分交换操作; 跳到第 3 步。
- 否:继续执行步骤 2。
- 检查镜像尾部; 是否要求交换?
- 是的:
- 请求的镜像是否有效(完整性和安全检查)?
- 是的。
- A. 执行交换操作。
- B. 完成镜像尾部的交换过程。
- C. 继续执行步骤 3。
- 没有
- A. 删除无效镜像。
- B. 镜像尾部的交换过程失败。
- C. 继续执行步骤 3。
- 是的。
- 请求的镜像是否有效(完整性和安全检查)?
- 否:继续第 3 步。
- 是的:
- 引导至主插槽中的镜像。
多镜像启动
当flash包含多个可执行镜像时,bootloader的操作会稍微复杂一些,但类似于前面描述的一个镜像的过程。 每个镜像都可以独立更新,因此flash被进一步分区,为每个镜像安排两个插槽。
+--------------------+
| MCUBoot |
+--------------------+
~~~~~ <- memory might be not contiguous
+--------------------+
| Image 0 |
| primary slot |
+--------------------+
| Image 0 |
| secondary slot |
+--------------------+
~~~~~ <- memory might be not contiguous
+--------------------+
| Image N |
| primary slot |
+--------------------+
| Image N |
| secondary slot |
+--------------------+
| Scratch |
+--------------------+
MCUBoot 还能够处理镜像之间的依赖关系。 例如,如果需要恢复镜像,则可能也有必要恢复另一个镜像(例如,由于 API 不兼容),或者只是为了防止由于不满足的依赖关系而被更新。 因此,必须完成所有中止的交换,并且必须在依赖性检查之前为每个镜像确定所有交换类型。 依赖性处理在以下部分。 多镜像启动过程以循环方式组织,循环遍历所有固件镜像。 引导过程的高级概述如下所示。
-
循环 1. 遍历所有镜像
-
1.检查当前镜像的swap状态区域; 是否恢复被中断的交换?
- 是的:
- 查看先前确定的其他镜像交换类型的有效性。
- 完成部分交换操作。
- 将交换类型标记为“None”。
- 跳到下一个镜像。
- 否:继续执行步骤 2。
- 是的:
-
2.检查主副槽中的镜像尾部; 是否要求镜像交换?
- 是:审查先前确定的其他镜像交换类型的有效性。 请求的镜像是否有效(完整性和安全性检查)?
- 是的:
- 为当前镜像设置先前确定的交换类型。
- 跳到下一个镜像。
- 否:
- 删除无效镜像。
- 交换程序对尾部镜像的持续失败。
- 将交换类型标记为“失败”。
- 跳到下一个镜像。
- 是的:
- 否:
- 将交换类型标记为“无”。
- 跳到下一张镜像。
- 是:审查先前确定的其他镜像交换类型的有效性。 请求的镜像是否有效(完整性和安全性检查)?
-
-
循环 2. 遍历所有镜像
- 当前镜像是否依赖于其他镜像?
- 是:是否满足所有镜像依赖项?
- 是:跳到下一个镜像。
- 否:
- 根据之前的类型修改交换类型。
- 从第一张镜像重新开始依赖性检查。
- 否:跳到下一个镜像。
- 是:是否满足所有镜像依赖项?
- 当前镜像是否依赖于其他镜像?
-
循环 3. 遍历所有镜像
- 1.是否要求换镜像?
- 是的:
- 执行镜像更新操作。
- 持续完成镜像尾部的交换过程。
- 跳到下一个镜像。
- 否:跳到下一个镜像。
- 是的:
- 1.是否要求换镜像?
-
循环 4. 遍历所有镜像
-
- 验证主插槽中的镜像(完整性和安全性检查)或至少进行基本的健全性检查以避免引导至空白flash区域。
-
-
引导至第 0 个镜像位置的主插槽中的镜像(引导链中的其他镜像由另一个镜像启动)。
镜像交换
bootloader交换两个镜像槽的内容有两个原因:
- 用户发出了“set pending”操作; 次插槽中的镜像应该运行一次(状态 I)或重复运行(状态 II),具体取决于是否指定了永久交换。
- 测试镜像未经确认(“boot_set_confirmed”)重启; bootloader应恢复为当前位于辅助插槽中的原始镜像(状态 III)。 如果镜像尾部指示应运行辅助插槽中的镜像,则bootloader需要将其复制到主插槽。 当前在主插槽中的镜像也需要保留在flash中,以便以后使用。 此外,如果bootloader在交换操作的中间重置,则两个镜像都需要是可恢复的。 根据以下过程交换两个镜像:
- 确定两个插槽是否足够兼容以交换镜像。为了兼容,两者都必须有可以放入scratch区的扇区,如果其中一个的扇区比另一个大,则它必须能够完全适合另一个插槽的一些四舍五入的扇区数。在接下来的步骤中,我们将使用术语“区域”来表示复制/擦除的数据总量,因为这可以是任意数量的扇区,具体取决于有多少扇区能够 适合一些交换操作。
- 按降序迭代区域索引列表(即,以最大索引值开始);仅复制预先确定为镜像一部分的区域; 当前元素=“索引”。
- a: 擦除划痕区域。
- b: 将 secondary_slot[index] 复制到scratch区。
-如果这是槽中的最后一个区域,则scratch区有一个临时状态区被初始化以存储初始状态,因为必须擦除主槽的最后一个区域,在这种情况下,只复制计算出与镜像相等的数据。- 否则如果这是第一个交换区域而不是最后一个区域插槽,初始化主插槽中的状态区域并复制完整区域内容。
- 否则,复制整个区域的内容。
- C:写入更新的交换状态 (i)。
- d:擦除 secondary_slot[index]
- e:根据先前在步骤 b 复制的数量,将 primary_slot[index] 复制到 secondary_slot[index]。
- 如果这不是插槽中的最后一个区域,则擦除辅助插槽中的尾部,以始终使用主插槽中的尾部。
- f:写入更新的交换状态 (ii)。
- g:擦除 primary_slot[索引]。
- h:根据先前在步骤 b 中复制的数量,将scratch区复制到 primary_slot[index]。
- 如果这是插槽中的最后一个区域,则从头开始读取状态(临时存储的位置)并在主插槽中重新写入。
- i:写入更新的交换状态 (iii)。
- 将交换程序的完成坚持到主插槽镜像尾部。
步骤 2f 中的额外注意事项是必要的,以便用户可以在以后写入辅助插槽镜像尾部。 在未写入镜像尾部的情况下,用户可以在辅助插槽中测试镜像(即转换到状态 I)。
注1:如果被复制的区域包含最后一个扇区,则在此操作期间交换状态暂时保持在 scratch 上,否则始终使用主槽的区域。
注2:bootloader尝试仅复制使用过的扇区(基于安装在任何插槽上的最大镜像),最大限度地减少复制的扇区数量并减少交换操作所需的时间。
第 3 步的细节取决于镜像是否正在测试、永久使用、还原或在请求交换时发生辅助插槽的验证失败:
* 测试交换:
o Write primary_slot.copy_done = 1
(Swap导致写入以下值:
primary_slot.magic = BOOT_MAGIC
secondary_slot.magic = UNSET
primary_slot.image_ok = Unset)
* 永久交换:
o Write primary_slot.copy_done = 1
(Swap导致写入以下值:
primary_slot.magic = BOOT_MAGIC
secondary_slot.magic = UNSET
primary_slot.image_ok = 0x01)
* 回退交换:
o Write primary_slot.copy_done = 1
o Write primary_slot.image_ok = 1
(Swap导致写入以下值:
primary_slot.magic = BOOT_MAGIC)
* 辅助槽验证失败:
o Write primary_slot.image_ok = 1
当完成上述操作后,主插槽中的镜像将被启动。
交换状态(swap status)
交换状态区域允许bootloader恢复,以防它在镜像交换操作的中间重新启动。 交换状态区域由一系列单字节记录组成。 这些记录是独立写入的,因此必须根据flash硬件规定的最小写入大小进行填充。 在下图中,为简单起见,假定最小写入大小为 1。 交换状态区域的结构如下图所示。 在此图中,为简单起见,假设最小写入大小为 1。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|sec127,state 0 |sec127,state 1 |sec127,state 2 |sec126,state 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|sec126,state 1 |sec126,state 2 |sec125,state 0 |sec125,state 1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|sec125,state 2 | |
+-+-+-+-+-+-+-+-+ +
~ ~
~ [Records for indices 124 through 1 ~
~ ~
~ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
~ |sec000,state 0 |sec000,state 1 |sec000,state 2 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
以上可能根本没有帮助; 这里有详细说明。
每个镜像槽都被划分为一系列flash扇区。 如果我们要枚举单个槽中的扇区,从 0 开始,我们将有一个扇区索引列表。 由于有两个镜像槽,每个扇区索引将对应于一对扇区。 例如,扇区索引 0 对应于主插槽中的第一个扇区和辅助插槽中的第一个扇区。 最后,反转索引列表,使列表以索引“BOOT_MAX_IMG_SECTORS - 1”开始并以 0 结束。
交换状态区域是此反转列表的表示。
在交换操作期间,每个扇区索引都会通过四种不同的状态进行转换:
0. primary slot: image 0, secondary slot: image 1, scratch: N/A
1. primary slot: image 0, secondary slot: N/A, scratch: image 1 (1->s, erase 1)
2. primary slot: N/A, secondary slot: image 0, scratch: image 1 (0->1, erase 0)
3. primary slot: image 1, secondary slot: image 0, scratch: N/A (s->0)
更加直观的状态转换图(译者添加):
每次扇区索引转换为新状态时,引导加载程序都会将记录写入交换状态区域。 从逻辑上讲,引导加载程序每个扇区索引只需要一个记录来跟踪当前的交换状态。 然而,由于闪存硬件的限制,当索引的状态改变时,记录不能被覆盖。 为了解决这个问题,引导加载程序为每个扇区索引使用三个记录,而不是一个。每个扇区状态对表示为一组三个记录。 记录值映射到上述四种状态如下:
| rec0 | rec1 | rec2
--------+------+------+------
state 0 | 0xff | 0xff | 0xff
state 1 | 0x01 | 0xff | 0xff
state 2 | 0x01 | 0x02 | 0xff
state 3 | 0x01 | 0x02 | 0x03
交换状态区域可以容纳“BOOT_MAX_IMG_SECTORS”扇区索引。 因此,该区域的大小(以字节为单位)为“BOOT_MAX_IMG_SECTORS * min-write-size * 3”。 索引计数的唯一要求是它足够大以说明最大尺寸的镜像(即,至少与镜像槽中的总扇区数一样大)。 如果设备的镜像槽已配置为 BOOT_MAX_IMG_SECTORS: 128
并且使用少于 128 个扇区,则写入的第一条记录将在该地区的中间。 例如,如果一个槽使用 64 个扇区,则第一个被交换的扇区索引是 63,它对应于该区域内的确切中间点。
注意:由于scratch区只需要记录最后一个扇区的交换,因此它最多使用 min-write-size * 3 个字节作为它自己的状态区。
恢复被中断的交换(reset recovery)
如果bootloader在交换操作期间重置,则两个镜像在flash中可能不连续。 Bootutil 通过使用镜像尾部确定镜像部分在flash中的分布方式从这种情况中恢复。
第一步是确定相关交换状态区域的位置。 因为这个区域嵌入在镜像槽中,所以它在flash中的位置在交换操作期间发生变化。 下面的一组表格将镜像尾部内容映射到交换状态位置。 在这些表中,“源”字段指示交换状态区域所在的位置。 在多镜像引导的情况下,镜像主要区域和单个scratch区域始终成对检查。 如果在scratch区域发现交换状态,则它可能不属于当前镜像。 交换状态的swap_info字段存储对应的镜像编号。 如果不匹配,则返回“source: none”。
| primary slot | scratch |
----------+--------------+--------------|
magic | Good | Any |
copy-done | 0x01 | N/A |
----------+--------------+--------------'
source: none |
----------------------------------------'
| primary slot | scratch |
----------+--------------+--------------|
magic | Good | Any |
copy-done | 0xff | N/A |
----------+--------------+--------------'
source: primary slot |
----------------------------------------'
| primary slot | scratch |
----------+--------------+--------------|
magic | Any | Good |
copy-done | Any | N/A |
----------+--------------+--------------'
source: scratch |
----------------------------------------'
| primary slot | scratch |
----------+--------------+--------------|
magic | Unset | Any |
copy-done | 0xff | N/A |
----------+--------------+--------------|
source: primary slot |
----------------------------------------+-------------------------------------+
这代表如下两种可能的情况: |
o 从来没有交换(没有状态可读,所以检查没有害处) |
o 恢复过程中; 状态字段在主插槽中. |
出于这个原因,我们假设主插槽作为源,以触发检查状态区域,并检查是否有交换正在进行 |
------------------------------------------------------------------------------'
如果交换状态区域指示镜像不连续,mcuboot 会通过读取活动镜像尾部中的“交换信息”字段并从位 0-3 中提取交换类型来确定被中断的交换操作类型,然后恢复操作 . 换句话说,它应用上一节中定义的过程,将镜像 1 移动到主插槽中,将镜像 0 移动到辅助插槽中。 如果引导状态指示镜像部分存在于scratch区,则通过区域交换过程中的步骤 e 或步骤 h 开始将此部分复制到正确位置,具体取决于该部分属于镜像 0 还是镜像 1.
交换操作完成后,bootloader继续进行,就好像它刚刚启动一样。
完整性检查(Integrity Check)
在将镜像复制到主插槽之前,会立即检查镜像的完整性。 如果bootloader不执行镜像交换,那么它可以在设置了“MCUBOOT_VALIDATE_PRIMARY_SLOT”的情况下对主插
槽中的镜像执行可选的完整性检查,否则它不会执行完整性检查。
在完整性检查期间,bootloader会验证镜像的以下方面:
- 32 位魔数必须正确 (
IMAGE_MAGIC
)。- 镜像必须包含一个
image_tlv_info
结构,由它的 magic(IMAGE_TLV_PROT_INFO_MAGIC
或IMAGE_TLV_INFO_MAGIC
)标识,紧跟固件(hdr_size
+img_size
)。 如果发现“IMAGE_TLV_PROT_INFO_MAGIC”,则在“ih_protect_tlv_size”字节之后,必须存在另一个魔数等于“IMAGE_TLV_INFO_MAGIC”的“image_tlv_info”。 - 镜像必须包含 SHA256 TLV。
- 计算出的 SHA256 必须匹配 SHA256 TLV 内容。
- 镜像 可能 包含签名 TLV。 如果是,它还必须有一个KEYHASH TLV,带有用于签名密钥的哈希值。 然后将遍历键列表以查找匹配的键,然后将使用该键来验证镜像内容。
- 镜像必须包含一个
安全(Security)
如上所述,完整性检查的最后一步是签名确认。 bootloader可以在构建时嵌入一个或多个公钥。 在签名验证期间,bootloader验证镜像是否使用与嵌入的 KEYHASH TLV 相对应的私钥签名。
有关在bootloader中嵌入公钥以及生成签名镜像的信息,请参阅:[signed_images(signed_images.md)。
如果要启用和使用加密镜像,请参阅:encrypted_images。
注意:选择direct-xip或ram-load升级策略时不支持镜像加密。
[使用硬件密钥进行验证]
默认情况下,整个公钥嵌入在bootloader代码中,其哈希值作为 KEYHASH TLV 条目添加到镜像清单中。 作为替代方案,可以通过设置“MCUBOOT_HW_KEY”选项使bootloader独立于密钥。 在这种情况下,必须将公钥的哈希提供给目标设备,并且 mcuboot 必须能够从那里检索密钥哈希。 出于这个原因,目标必须为 boot_retrieve_public_key_hash() 函数提供一个定义,该函数在boot/bootutil/include/bootutil/sign_key.h
。 也需要使用--public-key-format
imgtool 参数的 full
选项,以便将整个公钥 (PUBKEY TLV) 添加到镜像清单而不是其哈希 (KEYHASH TLV)。 在启动过程中,公钥在用于签名验证之前先经过验证,mcuboot 从 TLV 区域计算公钥的哈希值,并将其与从设备检索到的密钥哈希值进行比较。 这样 mcuboot 就独立于公钥。 密钥可以随时由他方提供。
受保护的TLV(Protected TLVS)
如果 TLV 区域包含受保护的 TLV 条目,通过以带有魔数值“IMAGE_TLV_PROT_INFO_MAGIC”的“struct image_tlv_info”开头,那么这些 TLV 的数据也必须受到完整性和真实性保护。 除了存储在 image_tlv_info
中的受保护 TLV 的完整大小之外,受保护 TLV 的大小连同 image_tlv_info
的大小结构本身也保存在标头。
每当镜像具有受保护的 TLV 时,SHA256 不仅要计算镜像标头和镜像,还要计算 TLV 信息标头和受保护的 TLV。
A +---------------------+
| Header | <- struct image_header
+---------------------+
| Payload |
+---------------------+
| TLV area |
| +-----------------+ | struct image_tlv_info with
| | TLV area header | | <- IMAGE_TLV_PROT_INFO_MAGIC (optional)
| +-----------------+ |
| | Protected TLVs | | <- Protected TLVs (struct image_tlv)
B | +-----------------+ |
| | TLV area header | | <- struct image_tlv_info with IMAGE_TLV_INFO_MAGIC
C | +-----------------+ |
| | SHA256 hash | | <- hash from A - B (struct image_tlv)
D | +-----------------+ |
| | Keyhash | | <- indicates which pub. key for sig (struct image_tlv)
| +-----------------+ |
| | Signature | | <- signature from C - D (struct image_tlv), only hash
| +-----------------+ |
+---------------------+