mcuboot简介
MCUboot 的目标是为引导加载程序,为微控制器系统上的系统闪存布局定义一个通用基础设施,并提供一个安全的引导加载程序,使软件升级变得容易。
–摘自mcuboot官网 mcuboot.
mcuboot一些基本概念
image header:
#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;
};
ih_load_addr的作用:在使用mcuboot image tool打包image package时,可以加入–load-addr选项,指定此image slot的加载地址。如果ih_load_addr与image slot的实际存储地址(RAM或者flash中)相同,那么当前代码运行模式为XIP模式。
以STM32F407 discovery开发板为例,它是XIP模式(代码直接可以从internal flash上运行): flash_base_addr = 0x08000000; image_header_size = 0x200; image_primary_slot_offset = 0x20000; ih_load_addr = 0x08020200 ih_load_addr与image slot在flash中存储的地址相同,则可以判定当前为XIP模式,可以直接从ih_load_addr处启动image,不需要做代码搬运动作。 如果ih_load_addr与image slot在flash中存储的地址不相同,则需要将image slot从flash中拷贝到ih_load_addr指定的地址上,再启动image。
image slot:
image slot是mcuboot管理image的最小单位,每一个slot都表示一个内容完整的,可以运行的image bin。flash的一部分可以划分为多个镜像区,每个镜像区包含两个image slot:一个primary slot和一个secondary slot。通常情况下,mcuboot bootloader只会运行primary slot中的image,因此必须配置image,以便它们可以从flash中的固定位置运行(例外情况是 direct-xip 和 ram-load 升级模式)。 如果mcuboot bootloader需要运行驻留在secondary slot中的image,则必须先将其内容复制到primary slot中,方法是交换两个镜像或覆盖主插槽的内容。 mcuboot bootloader支持基于交换或覆盖的镜像升级,但必须在构建时进行配置以选择这两种策略之一。 除了镜像区域的slot外,bootloader还需要一个scratch(暂存区)以允许可靠的镜像交换。
image trailer:
image trailer位于image的最后,在启动时,bootloader通过检查镜像尾部来确定引导交换类型,其结构如下:
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)。
- 加密密钥:密钥加密密钥 (KEY)。 镜像加密和解密需要这些密钥。
- 交换大小:当开始新的交换操作时,需要交换的总大小(基于具有最大镜像的插槽 + TLV)被写入此位置,以便在执行交换时重置的情况下更容易恢复。
- Swap info:占用一个字节,包含如下信息:
- 交换类型:存储在bit 0-3 中。 指示正在进行的交换操作的类型。 当 mcuboot 恢复中断的交换时,它使用此字段来确定要执行的操作类型。 该字段包含下表中的以下值之一。
- 镜像编号:存储在bit 4-7 中。 它在单镜像启动时始终为 0 值。 在多镜像引导的情况下,它指示中断发生时交换了哪个镜像(在所有镜像交换操作的情况下,使用相同的scratch区域。因此,如果在恢复交换操作时在scratch区发现启动状态,则该字段用于确定尾部属于哪个镜像)。
- 拷贝完成标志(copy done):一个字节的信息,用来指示这个插槽中的镜像是否拷贝完成(0x01:完成,0xff:没有完成)。
- 镜像有效标志(image ok):一个字节的信息,用来指示这个插槽中的镜像是否被用户确认完整有效(0x01:确认过,0xff:没有确认)。
- 魔数(magic):以下的16字节,以主机字节序列(host-byte-order HBO)写入:
const uint32_t boot_img_magic[4] = {
0xf395c277,
0x7fefd260,
0x0f505235,
0x8079b62c,
};
mcuboot flash map:
mcuboot使用结构体struct flash_area来管理flash分区,每一个flash area都被赋予一个唯一的ID,通过检索flash area id就可以查询到该flash area的offset和size。
/**
* @brief Structure describing an area on a flash device.
*
* Multiple flash devices may be available in the system, each of
* which may have its own areas. For this reason, flash areas track
* which flash device they are part of.
*/
struct flash_area {
uint8_t fa_id; /**This flash area's ID; unique in the system.*/
uint8_t fa_device_id;/*** ID of the flash device this area is a part of.*/
uint16_t pad16;
uint32_t fa_off; /*** This area's offset, relative to the beginning of its flash device's storage.*/
uint32_t fa_size; /*** This area's size, in bytes.*/
};
#define BOOTLOADER_ID 0
#define PRIMARY_ID 1
#define SECONDARY_ID 2
#define SCRATCH_ID 3
/*MCUBOOT_IMAGE_NUMBER = 1的情况*/
#define FLASH_AREA_BOOTLOADER BOOTLOADER_ID
#define FLASH_AREA_IMAGE_PRIMARY(x) PRIMARY_ID
#define FLASH_AREA_IMAGE_SECONDARY(x) SECONDARY_ID
#define FLASH_AREA_IMAGE_SCRATCH SCRATCH_ID
mcuboot image layout:
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
| +-----------------+ |
+---------------------+
| Padding | <- padding behind tlvs area
+---------------------+
| Trailer | <- image trailer
+---------------------+
- SHA256 hash:A–B之间内容的SHA256信息摘要,如果TLV area包含protected tlvs,则SHA256信息摘要包含image header + image payload + protected tlvs,如果TLV area不包含protected tlvs,则SHA256信息摘要仅包含image header + image payload.
- Keyhash:public key hash(使用3rd-party/mcuboot/scripts/imgtool.py getpub -k your-key.pem命令生成数组格式的public key)
- Signature:签名(使用private key对SHA256 hash做加密)
- Padding:填充
- Trailer:image trailer
image upgrade mode:
固件升级包含以下三种模式:
-
direct-xip mode: eXecute In Place(芯片内执行),是指CPU直接从存储器中读取程序代码执行,而不用再读到内存中。应用程序可以直接在flash闪存内运行,不必再把代码读到系统RAM中。在此模式下,bootloader可以直接从primary slot或secondary slot运行镜像(没有必须将其移动/复制到primary slot中)。在启动时,bootloader首先在插槽中查找镜像,然后检查镜像标头中的版本号。 它选择最新的镜像(具有最高版本号)和然后检查其有效性(完整性检查、签名验证等)
-
ram loading mode:primary slot & secondary slot的地位是相同的,与 direct-xip 模式一样,此模式也通过读取镜像头中的镜像版本号来选择最新的镜像。 但是不是就地执行它,而是将最新的镜像复制到用于执行的 RAM。
-
normal mode:mcuboot bootloader只会运行primary slot,如果需要运行secondary slot,则需要将其复制到primary slot中,根据primary slot & secondary slot中image的交换/移动策略,有下面三种方式:
-
overwrite :secondary slot直接覆盖掉primary slot
-
swap
- use scratch : 增加一个额外的临时区(scratch),用来作为交换primary slot & secondary slot的过渡区域
- use move :不使用临时区,而是在构建primary slot的存储区域时,比secondary slot多使用一个sector,然后完成image的交换
-
mcuboot image升级策略
-
fih_int boot_go(struct boot_rsp *rsp)
-
fih_int context_boot_go(struct boot_loader_state *state, struct boot_rsp *rsp)
-
三次遍历所有image的primary slot & secondary slot
-
第一次遍历:完成之前所有没中断的swap operation(如果有的话),确定此次启动的swap type。比较重要的函数:void boot_prepare_image_for_update(struct boot_loader_state *state, struct boot_status *bs)。
-
第二次遍历:根据上一次遍历返回的swap type,如果需要swap的话,执行int boot_perform_update(struct boot_loader_state *state, struct boot_status *bs),完成primary slot & secondary slot的交换。在primary slot image trailer中设置image ok & copy done标志。
#ifndef MCUBOOT_OVERWRITE_ONLY /* The following state needs image_ok be explicitly set after the * swap was finished to avoid a new revert. */ swap_type = BOOT_SWAP_TYPE(state); if (swap_type == BOOT_SWAP_TYPE_REVERT || swap_type == BOOT_SWAP_TYPE_PERM) { rc = swap_set_image_ok(BOOT_CURR_IMG(state)); if (rc != 0) { BOOT_SWAP_TYPE(state) = swap_type = BOOT_SWAP_TYPE_PANIC; } } if (BOOT_IS_UPGRADE(swap_type)) { rc = swap_set_copy_done(BOOT_CURR_IMG(state)); if (rc != 0) { BOOT_SWAP_TYPE(state) = BOOT_SWAP_TYPE_PANIC; } } #endif /* !MCUBOOT_OVERWRITE_ONLY */
-
第三次遍历:此时所有的image swap都已经完成,在启动primary slot之前,需要先验证下primary slot是否有效 (fih_int boot_validate_slot(struct boot_loader_state *state, int slot, struct boot_status *bs))。验证的大致步骤是:
- 1)计算image header + image payload ( + protected tlv)内容的sha256 hash,将sha256 hash计算结果同tlv area中的sha256 hash做比较,验证image的完整性。
- 2)从tlv area中读取出IMAGE_TLV_KEYHASH(public key hash),然后在用户的public key(bootutil_keys[])中搜索是否有相同的public key,这一步是验证public key的有效性。
- 3)从tlv area中读取出EXPECTED_SIG_TLV(签名,即对sha256 hash摘要使用private key加密),然后使用步骤2)中的public key解密EXPECTED_SIG_TLV,将解密结果同步骤1)中计算出的sha256 hash作比较,相同的话表示验签成功,否则验签失败。
-
-
最后返回primary slot的位置和image header信息,交由用户的代码启动primary slot
-
-
新的升级
MCUBOOT_OVERWRITE_ONLY
这种模式是直接用secondary slot覆盖primary slot,因为没有备份区域,所以覆盖之后无法恢复primary slot。
MCUBOOT_SWAP_USING_SCRATCH
增加一个额外的临时区(scratch),用来作为交换primary slot & secondary slot的过渡区域,交换之后如果secondary slot无法运行,还可以执行revert操作,恢复primary slot到交换之前的状态。此方式的优点是可以对原版本做保护,缺点也显而易见,flash的使用会增大,同时scratch区域会被频繁擦写,增加了flash损耗。
关于scratch,还有些补充说明,不论primary slot & secondary slot占用几个sector,假设scratch的大小设置为只占用一个sector的大小,此时primary slot & secondary slot交换的最小单位是一个flash sector。所以当primary slot & secondary slot较大时(占用多个sector size),那么scratch将会被频繁的擦写,这时scratch size的大小就需要重新慎重考虑,因为flash的擦写是有次数限制的。
MCUBOOT_SWAP_USING_MOVE
不使用临时区,而是在构建primary slot的存储区域时,比secondary slot多使用一个sector,然后完成image的交换:
1)将主插槽的所有扇区向上移动一个扇区。 从 N=0 开始:
2)将第 N 个扇区从次插槽复制到主插槽的第 N 个扇区。
3)将第 (N+1) 个扇区从主槽复制到辅助槽的第 N 个扇区。
4)重复第 2 步和第 3 步,直到交换所有插槽的扇区。
假设primary slot & secondary slot image内容都占用5个sector大小,假如使用not use scratch swap这种交换方式:
primary slot
+-----------+-----------+-----------+-----------+-----------+-----------+
| sector0 | sector1 | sector2 | sector3 | sector4 | |
+-----------+-----------+-----------+-----------+-----------+-----------+
整体向右移动一个sector
+-----------+-----------+-----------+-----------+-----------+-----------+
| | sector0 | sector1 | sector2 | sector3 | sector4 |
+-----------+-----------+-----------+-----------+-----------+-----------+
secondary slot
+-----------+-----------+-----------+-----------+-----------+
| sector0 | sector1 | sector2 | sector3 | sector4 |
+-----------+-----------+-----------+-----------+-----------+
然后以插空的方式完成primary slot & secondary slot的交换
#define BOOTLOADER_SIZE (64 * 1024 )
#define APPLICATION_SIZE (128 * 1024)
#define BOOTLOADER_START_ADDRESS (ADDR_FLASH_SECTOR_0)
#define APPLICATION_PRIMARY_START_ADDRESS (ADDR_FLASH_SECTOR_5)
#define APPLICATION_SECONDARY_START_ADDRESS (ADDR_FLASH_SECTOR_7)
#define BOOTLOADER_SECTOR_SIZE (16 * 1024)
#define APPLICATION_SECTOR_SIZE (128 * 1024)
static const struct flash_area bootloader = {
.fa_id = FLASH_AREA_BOOTLOADER,
.fa_device_id = FLASH_DEVICE_INTERNAL_FLASH,
.fa_off = BOOTLOADER_START_ADDRESS,
.fa_size = BOOTLOADER_SIZE,
};
static const struct flash_area primary_img0 = {
.fa_id = FLASH_AREA_IMAGE_PRIMARY(0),
.fa_device_id = FLASH_DEVICE_INTERNAL_FLASH,
.fa_off = APPLICATION_PRIMARY_START_ADDRESS,
.fa_size = APPLICATION_SIZE * 2,
};
static const struct flash_area secondary_img0 = {
.fa_id = FLASH_AREA_IMAGE_SECONDARY(0),
.fa_device_id = FLASH_DEVICE_INTERNAL_FLASH,
.fa_off = APPLICATION_SECONDARY_START_ADDRESS,
.fa_size = APPLICATION_SIZE,
};
void swap_run(struct boot_loader_state *state, struct boot_status *bs, uint32_t copy_size)
{
uint32_t sz;
uint32_t sector_sz;
uint32_t idx;
uint32_t trailer_sz;
uint32_t first_trailer_idx;
uint8_t image_index;
const struct flash_area *fap_pri;
const struct flash_area *fap_sec;
int rc;
sz = 0;
g_last_idx = 0;
sector_sz = boot_img_sector_size(state, BOOT_PRIMARY_SLOT, 0);
while (1) {
sz += sector_sz;
/* Skip to next sector because all sectors will be moved up. */
g_last_idx++;
if (sz >= copy_size) {
break;
}
}
/*
* When starting a new swap upgrade, check that there is enough space.
*/
if (boot_status_is_reset(bs)) {
sz = 0;
trailer_sz = boot_trailer_sz(BOOT_WRITE_SZ(state));
first_trailer_idx = boot_img_num_sectors(state, BOOT_PRIMARY_SLOT) - 1;
while (1) {
sz += sector_sz;
if (sz >= trailer_sz) {
break;
}
first_trailer_idx--;
}
if (g_last_idx >= first_trailer_idx) {
BOOT_LOG_WRN("Not enough free space to run swap upgrade");
bs->swap_type = BOOT_SWAP_TYPE_NONE;
return;
}
}
...省略部分代码
}
在mcuboot_config.h中打开MCUBOOT_SWAP_USING_MOVE,primary slot size配置为secondary slot size的两倍,且两个slot sector size相同,swap_run()会遇到"Not enough free space to run swap upgrade"错误,这里的逻辑无法理解。
swap use move的所有条件都已满足,且image payload size + image trailer size没有超过一个sector size.
恢复被中断的升级
MCUBOOT_OVERWRITE_ONLY
MCUBOOT_SWAP_USING_SCRATCH
MCUBOOT_SWAP_USING_MOVE
to do…
如何选择合适的升级方法呢?
升级方式 | 优点 | 缺点 |
---|---|---|
MCUBOOT_OVERWRITE_ONLY | 不会使用额外的flash区域,算法简单,维护简单 | 不能保护源版本,升级失败后设备可能变砖 |
MCUBOOT_SWAP_USING_SCRATCH | 可以保护源版本,升级失败后可以恢复到原来的版本,容错率高 | flash的使用量增加,scratch区域flash擦写次数成倍增加,损坏风险大大增加,算法复杂,维护困难 |
MCUBOOT_SWAP_USING_MOVE | 可以保护源版本,升级失败后可以恢复到原来的版本,容错率高,相对MCUBOOT_SWAP_USING_SCRATCH方式,flash擦写更加均匀 | 此算法仅限于拥有相同布局的flash, 所有插槽的扇区应具有相同的大小 |
根据flash容量、flash擦写损耗、升级算法的复杂程度几方面来选择最适合你的方案。
mcuboot安全
mcuboot没有实现自己的加密算法库,可以通过开启Mbed-TLS or Tinycrypt来启用第三方加密算法库:
/* Uncomment to use ARM's mbedTLS cryptographic primitives */
#define MCUBOOT_USE_MBED_TLS
/* Uncomment to use Tinycrypt's. */
#define MCUBOOT_USE_TINYCRYPT
mcuboot image tool脚本的使用
python3 ./3rd-party/mcuboot/scripts/imgtool.py -h
Usage: imgtool.py [OPTIONS] COMMAND [ARGS]...
Options:
-h, --help Show this message and exit.
Commands:
create Create a signed or unsigned image INFILE and OUTFILE are parsed...
getpriv Dump private key from keypair
getpub Dump public key from keypair
keygen Generate pub/private keypair
sign Create a signed or unsigned image INFILE and OUTFILE are parsed...
verify Check that signed image can be verified by given key
version Print imgtool version information