恢复引导程序 (Recovery-Bootloader)

📅 2026/8/13 11:37:58
恢复引导程序 (Recovery-Bootloader)
本文将介绍如何通过恢复引导程序 (Recovery Bootloader) 功能安全地对引导程序 (bootloader) 执行 OTA 更新。设想这样一个场景设备已经部署到现场并正常运行此时安全审查发现了一个引导程序漏洞。修复该漏洞需要对现场设备的引导程序进行更新。虽然这看起来与常规 OTA 更新类似但替换引导程序的风险要远高于更新应用程序。如果因为写入中断、镜像不完整或校验失败导致引导程序失效设备可能无法再启动。恢复引导程序功能正是为了应对这一风险而设计在 flash 的另一个位置保留一份额外的、已知可用的引导程序镜像。当主引导程序无法加载时系统会转而使用该恢复镜像。本文将说明如何在尽量降低设备无法启动风险的前提下完成引导程序的 OTA 更新。文中示例基于 ESP-IDF v6.1 与 ESP32-C5但相同的思路同样适用于其他支持该功能的芯片。应用程序 OTA 与引导程序 OTA应用程序的 OTA 更新通常会使用多个应用分区。新镜像会被写入一个空闲的分区而当前正在运行的应用程序则保持不变。如果新镜像校验失败或无法成功启动系统可返回到之前的程序中。默认情况下引导程序并不提供同样的保护。典型的 flash 布局中只包含一份引导程序镜像。一旦该镜像被擦除、部分覆写或损坏就无法继续启动流程设备将处于无法正常工作的状态直到通过物理连接方式重新进行烧录。由于引导程序体积很小、改动频率也很低历史上引导程序更新并不常见。但引导程序中也可能存在漏洞因此仍需要对已经部署的设备进行引导程序更新。恢复功能还需要芯片 ROM 引导程序的支持。因此恢复引导程序功能仅在实现对应 ROM 级回退机制的芯片上可用。引导程序更新方案更新引导程序主要有两种思路借助恢复引导程序的更新方式在替换主引导程序之前应用程序会先将一份已知可用的引导程序镜像写入恢复引导程序所在位置。如果主引导程序失效ROM 引导程序可以加载该恢复镜像。这是推荐做法也是本文重点介绍的方案。直接更新方式应用程序直接将新镜像覆写到主引导程序位置不预先准备恢复镜像。该方式使设备变砖的风险更高通常只在不支持恢复引导程序的芯片上作为唯一可行方案。关键术语在深入介绍之前有必要先厘清各个启动阶段及其相互关系。一级引导程序 (1st stage bootloader)即ROM bootloader是芯片上电后运行的第一段代码固化在芯片内部的只读存储器中无法更改。其作用是在 flash 中找到有效的2nd stage bootloader镜像对其进行校验、加载并将控制权移交给它。二级引导程序 (2nd stage bootloader)即通常所说的bootloader是 ESP-IDF 的引导程序二进制文件负责读取分区表、选择应用分区、校验并加载该分区然后启动应用程序。引导程序 OTA 更新替换的正是这个镜像。主引导程序 (Primary bootloader)是位于常规引导程序偏移地址通常为0x0000、0x1000或0x2000的引导程序。恢复引导程序 (Recovery bootloader)是存放在不同 flash 偏移地址上的、已知可用的引导程序副本。当 ROM 引导程序在无法加载主引导程序时才会使用它正常情况下恢复引导程序不会被用到。正常启动流程当主引导程序存在且有效时启动流程如下上电 └─► ROM 引导程序 一级引导程序 └─► 主引导程序 二级引导程序 └─► 应用程序恢复引导程序流程当主引导程序缺失或已损坏时ROM 引导程序会回退至恢复引导程序上电 └─► ROM 引导程序 ├─► 主引导程序 —— 失败 └─► 恢复引导程序 └─► 应用程序恢复引导程序的地址保存在芯片的 eFuse 中。eFuse 是芯片上的一次性可编程存储器其比特位一旦从 0 烧录为 1就无法再更改。支持恢复引导程序的芯片恢复引导程序功能仅在 ROM 引导程序支持该特性的芯片上可用。在 ESP-IDF 中该功能通过CONFIG_BOOTLOADER_RECOVERY_ENABLEKconfig 选项开放可在 menuconfig 中的Bootloader config - Recovery Bootloader and Rollback路径下找到。截至本文撰写时支持该选项的芯片列表如下芯片CONFIG_BOOTLOADER_RECOVERY_ENABLE可用版本ESP32-C5ESP-IDF v5.5 及以上ESP32-C61ESP-IDF v5.5 及以上ESP32-P4芯片版本 3.0ESP-IDF v5.5 及以上ESP32-S31ESP-IDF v6.1 及以上恢复引导程序的配置步骤在确认目标芯片支持该功能并开启CONFIG_BOOTLOADER_RECOVERY_ENABLEKconfig 选项之后下一步就是配置恢复引导程序。首先需要检查现有的分区布局并为恢复引导程序预留一块合适的空闲 flash 区域。1. 选择恢复引导程序的存放位置在选择恢复引导程序的位置之前需要先估算当前布局所允许的最大引导程序镜像大小。在 ESP-IDF 中该上限由主引导程序偏移地址与分区表偏移地址之间的区域决定。CONFIG_PARTITION_TABLE_OFFSET本身并不直接定义引导程序镜像大小但它决定了该区域的上边界。恢复引导程序镜像的大小与主引导程序镜像相同。最大引导程序大小的计算方式为CONFIG_PARTITION_TABLE_OFFSET - CONFIG_BOOTLOADER_OFFSET_IN_FLASH。例如若分区表起始地址为0x8000、主引导程序位于0x1000则最大引导程序大小为0x7000字节。选定恢复引导程序位置后需要将该地址设置到CONFIG_BOOTLOADER_RECOVERY_OFFSETKconfig 选项中。同一地址后续也需要烧录进 eFuse相关步骤将在本文后面介绍。这是 ROM 引导程序查找恢复引导程序时唯一需要知道的地址。此外也可以在分区表中显式添加恢复引导程序条目 (recovery_bloader) 来明确定义该区域这主要是为了保持一致性并记录完整的 flash 布局。2. 分区表布局ESP-IDF 官方指南中记录了分区表的类型 (Type) 与子类型 (SubType)详见 ESP-IDF Partition Tables。推荐的分区表布局如下# ESP-IDF 分区表 # 名称, 类型, 子类型, 偏移, 大小, 标志 bootloader, bootloader, primary, N/A, N/A, partition_table, partition_table, primary, N/A, N/A, ota_data, data, ota, , 0x2000, nvs, data, nvs, , 0x6000, phy_init, data, phy, , 0x1000, ota_0, app, ota_0, , 1M, ota_1, app, ota_1, , 1M, recovery_bloader, bootloader, recovery, N/A, N/A,与 ESP-IDF 默认分区表相比此布局新增了显式的bootloader与partition_table条目便于查看和梳理整体 flash 布局并可完整记录整个 flash 的分区情况。可运行idf.py partition-table查看带有偏移地址与大小信息的分区表。说明如果产品已经部署且现有分区表中不包含bootloader分区条目仍然可以使用引导程序恢复功能——有专门的 API 可以在运行时向分区表中动态添加缺失的分区。N/A是一个特殊取值表示分区工具会根据配置自动为某些分区类型填充相应字段。主引导程序与主分区表之间不应放置任何其他分区因为该区域专用于主引导程序并决定了引导程序的最大允许大小恢复引导程序应放置在单独的区域中。3. 烧录恢复引导程序 eFuse烧录恢复引导程序 eFuse 有两种方式在应用程序运行时烧录调用以下 APIesp_efuse_set_recovery_bootloader_offset(CONFIG_BOOTLOADER_RECOVERY_OFFSET);在主机端烧录使用espefuse.py工具espefuse--port/dev/ttyUSB0 burn_efuse RECOVERY_BOOTLOADER_FLASH_SECTOR 0x3F0恢复引导程序对应的 eFuse 字段以扇区号 (sector number) 的形式表示而非原始字节地址。要将恢复引导程序的偏移地址转换为扇区号需将该偏移地址除以 flash 扇区大小 (0x1000)。例如若恢复引导程序偏移地址为0x3F0000则对应的 eFuse 取值为0x3F0000 / 0x1000 0x3F0。RECOVERY_BOOTLOADER_FLASH_SECTOR CONFIG_BOOTLOADER_RECOVERY_OFFSET / 0x1000引导程序 OTA 更新引导程序 OTA 更新的流程与应用程序 OTA 更新流程类似主要区别在于在开始更新之前需要预先准备好引导程序相关的分区与 eFuse。当 Secure Boot v1 已启用时无法执行引导程序 OTA 更新。1. 获取主引导程序与恢复引导程序的分区句柄如果分区表中已经包含显式的引导程序条目可以使用以下代码分别获取主引导程序与恢复引导程序区域的分区句柄。这两个句柄用于在不同分区之间拷贝引导程序镜像。constesp_partition_t*primary_bootloaderesp_partition_find_first(ESP_PARTITION_TYPE_BOOTLOADER,ESP_PARTITION_SUBTYPE_BOOTLOADER_PRIMARY,NULL);constesp_partition_t*recovery_bootloaderesp_partition_find_first(ESP_PARTITION_TYPE_BOOTLOADER,ESP_PARTITION_SUBTYPE_BOOTLOADER_RECOVERY,NULL);如果分区表中不包含显式的引导程序条目适用于已经部署的产品可以使用以下代码在运行时注册对应区域constesp_partition_t*primary_bootloader;esp_partition_register_external(NULL,ESP_PRIMARY_BOOTLOADER_OFFSET,ESP_BOOTLOADER_SIZE,PrimaryBTLDR,ESP_PARTITION_TYPE_BOOTLOADER,ESP_PARTITION_SUBTYPE_BOOTLOADER_PRIMARY,primary_bootloader);constesp_partition_t*recovery_bootloader;esp_partition_register_external(NULL,CONFIG_BOOTLOADER_RECOVERY_OFFSET,ESP_BOOTLOADER_SIZE,RecoveryBTLDR,ESP_PARTITION_TYPE_BOOTLOADER,ESP_PARTITION_SUBTYPE_BOOTLOADER_RECOVERY,recovery_bootloader);2. 在 eFuse 中烧录恢复引导程序偏移地址将恢复引导程序的偏移地址烧录进 eFuse。如果该 eFuse 字段尚未设置此 API 会写入该数值如果该 eFuse 已设置且取值相同此 API 不会做任何操作但如果取值不同此 API 将返回错误。esp_efuse_set_recovery_bootloader_offset(CONFIG_BOOTLOADER_RECOVERY_OFFSET);3. 创建已知可用的恢复引导程序备份在开始 OTA 更新之前需要先将主引导程序备份到恢复分区中。由于设备刚刚正是从该主引导程序启动的可以确认该镜像是可用的。如果设备本次是通过恢复引导程序启动的说明恢复引导程序中已经保存了有效镜像此时可跳过备份步骤直接进入下载环节。if(esp_rom_get_bootloader_offset()ESP_PRIMARY_BOOTLOADER_OFFSET){// 本次是通过主引导程序启动的。ESP_LOGI(TAG,Backing up primary bootloader to recovery partition.);esp_partition_copy(recovery_bootloader,0,primary_bootloader,0,primary_bootloader-size);}else{// 恢复引导程序中已保存有效镜像。ESP_LOGW(TAG,Recovery bootloader was used to boot the device);ESP_LOGI(TAG,Skipping backup, recovery bootloader already holds a valid image.);}4. 将新引导程序镜像下载到空闲 OTA 分区使用空闲的 OTA 分区ota_0或ota_1来下载新的引导程序镜像该流程与应用程序 OTA 更新流程相同。新的引导程序镜像会先下载到这一暂存分区中只有在校验通过之后才会被拷贝到主引导程序分区。此处所用的高层 OTA 辅助接口详见 ESP HTTPS OTA 相关文档。esp_https_ota_config_t*ota_config;// free app ota partition will be used to store the new bootloader imageconstesp_partition_t*free_ota_slotesp_ota_get_next_update_partition(NULL);ota_config-partition.stagingfree_ota_slot;// The target for the OTA update.ota_config-partition.finalprimary_bootloader;esp_https_ota(ota_config);esp_partition_copy(primary_bootloader,0,free_ota_slot,0,primary_bootloader-size);新引导程序镜像通过校验后将其拷贝到主引导程序分区。这是整个更新流程中最关键的一步因为此时主引导程序会先被擦除、再重新写入。如果这一步骤中发生掉电或其他故障主引导程序将处于损坏状态此时下一次启动会转而使用恢复引导程序。5. ROM 引导程序的行为设备重启后ROM 引导程序会先尝试加载并校验主引导程序如果成功则立即跳转执行如果失败ROM 会读取RECOVERY_BOOTLOADER_FLASH_SECTOR这一 eFuse 字段——若该字段为0表示尚未配置或0xFFF表示已被永久禁用ROM 会跳过恢复流程反复重试主引导程序进入无限循环若该字段为其他任意数值则视为一个有效的扇区偏移地址ROM 会从该地址加载恢复引导程序加载成功后即跳转执行。如果恢复引导程序的加载同样失败ROM 会再次重试主引导程序同样陷入无限循环。这也是为什么必须在擦除主引导程序之前预先烧录好一份可用的恢复镜像如果两者同时损坏设备将无法从该循环中恢复。当触发恢复路径时ROM 日志大致如下ESP-ROM:esp32c5-eco2-20250121 Build:Jan 21 2025 rst:0x1 (POWERON),boot:0x18 (SPI_FAST_FLASH_BOOT) invalid header: 0xffffffff invalid header: 0xffffffff invalid header: 0xffffffff PRIMARY - FAIL Loading RECOVERY Bootloader... SPI mode:DIO, clock div:1 load:0x408556b0,len:0x17cc load:0x4084bba0,len:0xdac load:0x4084e5a0,len:0x3140 entry 0x4084bbaa I (46) boot: ESP-IDF ... 2nd stage bootloaderROM 引导程序对恢复引导程序执行的安全启动校验与对主引导程序执行的校验是完全相同的。6. 引导程序 OTA 更新后应用程序的行为设备每次启动时都应检查本次是通过哪个引导程序启动的主引导程序处于激活状态—— 说明新的引导程序已成功加载。恢复引导程序处于激活状态—— 说明主引导程序启动失败设备已优雅地完成了恢复。此时应用程序需要用已知可用的恢复镜像重新恢复主引导程序以便下一次 OTA 周期能够从一个干净的状态重新开始。if(esp_rom_get_bootloader_offset()ESP_PRIMARY_BOOTLOADER_OFFSET){ESP_LOGI(TAG,Primary bootloader is active — OTA succeeded.);}else{ESP_LOGW(TAG,Recovery bootloader is active — primary OTA failed, restoring primary from recovery.);esp_partition_copy(primary_bootloader,0,recovery_bootloader,0,primary_bootloader-size);}这种先检查、再处理的模式是闭环完成引导程序 OTA 更新、并为下一次 OTA 周期做好准备的推荐做法。防回滚 (Anti-rollback)ESP-IDF 为引导程序提供了与应用程序相同的防回滚机制。每个镜像的头部都包含一个secure_version字段芯片上有对应的 eFuse 字段用于保存所接受的最低版本号。引导程序只会加载secure_version大于或等于该 eFuse 存储值的镜像。一旦新镜像被确认可正常工作就可以将该 eFuse 值提升到该镜像的secure_version——此后任何版本号更低的镜像都将被无条件拒绝即便该镜像本身签名有效、内容完整也是如此。这一机制可以防止攻击者将引导程序回滚到已知存在漏洞的旧版本。应用程序与引导程序的防回滚各自使用独立的secure_versioneFuse 字段并在不同阶段进行校验一级引导程序负责校验二级引导程序的版本二级引导程序负责校验应用程序的版本。引导程序的secure_version字段属于 Bootloader Image Format引导程序镜像格式的一部分。引导程序防回滚功能仅在部分支持恢复引导程序的芯片上可用请确认目标芯片是否支持CONFIG_BOOTLOADER_ANTI_ROLLBACK_ENABLE选项。引导程序的防回滚功能是可选的由CONFIG_BOOTLOADER_ANTI_ROLLBACK_ENABLEKconfig 选项控制。启用后CONFIG_BOOTLOADER_SECURE_VERSION用于设置构建时写入引导程序镜像头部的secure_version数值。eFuse 数值的推进方式取决于BOOTLOADER_ANTI_ROLLBACK_UPDATE_IN_ROM选项若启用该选项ROM 引导程序会在校验成功后自动烧录更高的安全版本号若未启用则需要由应用程序显式推进该数值。下面展示的是由应用程序管理的方式。esp_bootloader_desc_tprimary_bootloader_desc;size_tefuse_secure_version0;if(esp_rom_get_bootloader_offset()ESP_PRIMARY_BOOTLOADER_OFFSET){// Get the primary bootloader description, which includes the secure_version field.esp_ota_get_bootloader_description(NULL,primary_bootloader_desc);// Read the eFuse secure_versionesp_efuse_read_field_cnt(ESP_EFUSE_BOOTLOADER_ANTI_ROLLBACK_SECURE_VERSION,efuse_secure_version);// Advance the secure_version in eFuse. It prevents loading older bootloaders.if(primary_bootloader_desc.secure_versionefuse_secure_version){size_tdiffprimary_bootloader_desc.secure_version-efuse_secure_version;esp_efuse_write_field_cnt(ESP_EFUSE_BOOTLOADER_ANTI_ROLLBACK_SECURE_VERSION,diff);}}BOOTLOADER_ANTI_ROLLBACK_SECURE_VERSION字段采用单调递增的比特计数编码方式每递增一次就会多烧录一个比特位为 1。大多数芯片为该字段分配了 4 个比特位最多支持 4 次递增0x0 → 0x1 → 0x3 → 0x7 → 0xF。在产品出货前应确认目标芯片该字段的具体位宽并据此规划版本管理方案确保不超出该预算。下表展示了引导程序版本管理机制在实践中的运作方式。其中Ver表示esp_bootloader_desc_t.versionsec_ver表示镜像的secure_versioneFuse min表示BOOTLOADER_ANTI_ROLLBACK_SECURE_VERSION。Versec_vereFuse min说明100初始生产镜像尚未启用防回滚机制。200新的引导程序构建版本但不涉及安全修复仍接受旧镜像。310包含安全修复的构建版本因 1 0 而被接受。311同一镜像eFuse 已推进此后任何 secure_version 0 的镜像都将被拒绝。421后续的安全修复构建版本因 2 1 而被接受。422eFuse 再次推进此后任何 secure_version 2 的镜像都将被拒绝。不使用恢复引导程序的引导程序 OTA在没有恢复引导程序的情况下覆写主引导程序不具备任何自动的安全保障。在烧录过程中一旦发生掉电或镜像无效设备将永久无法启动——此时的恢复只能依靠 JTAG 或串口编程器进行物理接入。因此只有在具备其他救援手段并能够接受相应风险的前提下才建议采用此方式。其更新流程与前文所述的「引导程序 OTA 更新」基本相同只是省略了其中的第 2 步与第 3 步——因为不存在恢复分区也无需在其中放置已知可用的备份镜像。第 4 步末尾的esp_partition_copy()调用是整个流程中最关键的一步一旦该操作失败设备将直接变砖。因此务必在确保电源供应稳定的情况下再执行该操作。完整示例可参考 partitions_ota example。结语本文完整介绍了引导程序 OTA 更新的两条路径在支持 ROM 级恢复功能的芯片上使用恢复引导程序的安全方案以及在不支持该功能的芯片上采用的风险较高的直接烧录方案。乐鑫建议优先采用恢复引导程序方案。完整的工作示例可参考 ESP-IDF 仓库中的 partitions_ota example。相关资源Bootloader guidePartition TablesOTA API referenceESP HTTPS OTAeFuse ManagerBootloader Image Formatpartitions_ota example