3. 固件升级的软件基础:固件镜像格式、版本号管理与签名校验
做固件升级,说白了就是跟“文件”打交道。你想想看,不管你的升级流程设计得多花哨,最终烧进芯片里的,总归是一堆二进制数据。但这堆数据怎么组织、怎么识别、怎么保证它没被人动过手脚,这就是我们今天要聊的核心。
我个人习惯把固件升级的软件基础拆成三块:镜像格式、版本号管理、签名与校验。这三块搞不明白,后面的升级流程全是空中楼阁。
3.1 固件镜像格式:BIN、HEX、ELF
先说说最常见的三种格式。很多新手工程师上来就问:“我该用哪种格式?”其实答案很简单——看你的目标是什么。
3.1.1 BIN 格式
BIN 文件,就是纯粹的二进制数据。没有头、没有尾、没有地址信息。它是什么?它就是芯片 Flash 里内容的“裸拷贝”。
- 优点:体积最小,解析最简单。烧录器直接按地址顺序往里写就行。
- 缺点:没有地址映射。如果你想把一段代码放到 0x08001000,你得自己告诉烧录工具。
- 适用场景:量产烧录、OTA 升级包(因为体积小,传输快)。
3.1.2 HEX 格式(Intel HEX)
HEX 文件是文本格式的二进制数据。每一行都包含地址、数据类型和校验和。说白了,它比 BIN 多了一层“地址标签”。
- 结构:
:LLAAAATTDDDD...CC - 优点:可以指定每段数据存放的地址,支持分段加载。
- 缺点:体积比 BIN 大(因为 ASCII 编码和校验开销)。
- 适用场景:调试阶段、通过串口或 J-Link 下载时。
// 一个典型的 HEX 记录示例
:020000040800F2 // 扩展线性地址记录,设置基地址为 0x0800
:1000000000BE00A0E301F000F800F000F800F000F8A7 // 数据记录
:00000001FF // 文件结束记录
3.1.3 ELF 格式
ELF 是 Linux 和嵌入式高级调试中最常用的格式。它不光包含代码和数据,还包含符号表、调试信息、重定位表。
- 优点:信息最全,支持调试、支持动态链接。
- 缺点:体积巨大,结构复杂。
- 适用场景:开发调试、Linux 内核/驱动加载。
嗯,这里要注意:ELF 几乎不会直接用于固件升级。因为它的结构太复杂了,Bootloader 解析起来成本太高。通常的做法是:开发时用 ELF 调试,发布时用 objcopy 转成 BIN 或 HEX。
3.1.4 三种格式对比
| 特性 | BIN | HEX | ELF |
|---|---|---|---|
| 体积 | 最小 | 中等 | 最大 |
| 地址信息 | 无 | 有 | 有(含符号) |
| 校验 | 无 | 行内校验和 | 无(依赖工具) |
| 解析复杂度 | 极低 | 低 | 高 |
| 升级场景 | OTA、量产 | 串口下载 | 调试阶段 |
3.2 固件版本号管理
版本号这东西,看着简单,但坑特别多。我见过不少产品,因为版本号管理混乱,导致升级后设备变砖。
3.2.1 版本号的组成
我个人习惯用三段式版本号:主版本号.次版本号.修订号。
- 主版本号:重大架构变更,不兼容升级。
- 次版本号:新增功能,向下兼容。
- 修订号:Bug 修复,功能不变。
但光有这三段还不够。在嵌入式里,我建议再加一个内部构建号或Git Commit ID。为什么?因为有时候你改了代码但忘了改版本号,结果两个不同版本的固件版本号一模一样,升级逻辑就懵了。
3.2.2 版本号存储在哪里?
版本号不能只写在代码里。它必须放在固件镜像的固定偏移位置,这样 Bootloader 不需要解析整个固件就能读到版本信息。
// 固件头结构体示例(放在镜像开头)
typedef struct {
uint32_t magic; // 魔数,用于识别固件合法性
uint32_t version_major; // 主版本号
uint32_t version_minor; // 次版本号
uint32_t version_patch; // 修订号
uint32_t build_number; // 构建号
uint32_t firmware_size; // 固件总大小
uint32_t crc32; // 固件数据 CRC32 校验
uint8_t reserved[16]; // 保留字段
} firmware_header_t;
3.2.3 版本号比较逻辑
Bootloader 在决定是否升级时,需要比较当前固件版本和新固件版本。逻辑很简单:
- 先比较主版本号,大的优先。
- 主版本相同,比较次版本号。
- 主次都相同,比较修订号。
- 全部相同,比较构建号(或 Git Commit)。
但这里有个细节:是否允许降级? 我建议默认禁止降级,除非有特殊开关。因为降级可能导致配置不兼容,或者引入已知 Bug。
3.3 固件签名与校验
这是固件升级的安全基石。没有签名校验,你的设备就是裸奔的。
3.3.1 为什么要签名?
你想想看,如果攻击者伪造了一个恶意固件,通过 OTA 推送给了你的设备,会发生什么?轻则设备变砖,重则被植入后门。签名就是为了解决这个问题——确保固件来源可信、内容完整。
3.3.2 签名流程
- 生成密钥对:私钥保存在安全的构建服务器上,公钥烧录在设备中。
- 计算哈希:对固件数据(通常是 BIN 文件)计算 SHA256 哈希。
- 签名:用私钥对哈希值进行 RSA 或 ECDSA 签名。
- 打包:将签名附加到固件头或固件尾部。
3.3.3 校验流程
- Bootloader 读取固件头和签名。
- 计算固件数据的 SHA256 哈希。
- 用公钥解密签名,得到原始哈希。
- 比较两个哈希值。一致则通过,否则拒绝升级。
3.3.4 签名算法选择
| 算法 | 签名长度 | 计算开销 | 安全性 |
|---|---|---|---|
| RSA-2048 | 256 字节 | 较高 | 高 |
| ECDSA-P256 | 64 字节 | 中等 | 高 |
| HMAC-SHA256 | 32 字节 | 低 | 中(对称密钥) |
我个人更推荐 ECDSA。为什么?签名短、计算快,对资源受限的 MCU 更友好。RSA 虽然成熟,但签名太长,传输和存储都有开销。
3.3.5 校验的另一个维度:CRC 或 SHA
签名校验保证了固件的来源可信,但传输过程中可能发生比特翻转。所以,我建议在签名之外,再加一层完整性校验——比如 CRC32 或 SHA256。
- CRC32:速度快,适合校验传输错误。
- SHA256:更安全,但计算慢。
通常的做法是:签名校验只在升级开始时做一次,而 CRC 校验可以在每收到一个数据包时做,也可以在写入 Flash 后做。
3.4 知识体系总览
下面这张图,把今天讲的内容串起来了。你可以看到,从固件构建到最终烧录,每一步都有对应的软件基础支撑。
好了,以上就是固件升级软件基础的三个核心支柱。搞懂了这些,你就能理解为什么有的升级方案稳定可靠,有的却漏洞百出。下一层,我们会深入到具体的升级流程设计中去。