4、固件镜像格式:Bin、Hex、ELF 与镜像头设计
做 Bootloader 这么多年,我接触过各种各样的固件格式。说实话,很多刚入行的工程师容易在这上面栽跟头。你想想看,如果连固件长什么样都不清楚,怎么保证升级不出问题?
这一节,我们就来聊聊 Bin、Hex、ELF 这三种最常见的固件格式,以及镜像头里那些不得不说的门道。
4.1 三种固件格式的本质区别
先给个直观的对比,你一看就明白:
| 特性 | Bin 文件 | Hex 文件 | ELF 文件 |
|---|---|---|---|
| 本质 | 纯二进制数据 | ASCII 编码的二进制 | 可执行与链接格式 |
| 地址信息 | 无(裸数据) | 每行包含地址 | 包含完整段信息 |
| 文件大小 | 最小 | 约为 Bin 的 2 倍 | 最大(含调试信息) |
| 烧录/升级用途 | 最常用 | 常用于烧录器 | 调试阶段使用 |
| 是否可直接运行 | 是(需加载到正确地址) | 否(需解析) | 否(需加载器) |
4.2 Bin 文件——最纯粹的固件形态
Bin 文件,说白了就是内存的「快照」。它从起始地址开始,一个字节挨着一个字节,没有任何多余信息。
特点:
- 没有地址信息,你必须知道它该烧到哪个位置
- 没有校验,数据对错全靠你自己
- 文件大小等于实际代码大小(对齐后)
4.3 Hex 文件——带地址的「快递单」
Hex 文件(Intel HEX 格式)每行都包含地址和校验。它像是一张张快递单,告诉你每个包裹该送到哪个地址。
一个典型的 Hex 行长这样:
:10000000FF0040E3390000000000000000F0
:1000100000000000000000000000000000000000
:0400000500000000F5
:00000001FF
格式解析:
:起始标记10本行数据长度(16 字节)0000起始地址00记录类型(00=数据,01=结束)- 后面是数据,最后两位是校验和
4.4 ELF 文件——调试阶段的「瑞士军刀」
ELF 文件包含的信息最丰富:代码段、数据段、BSS 段、符号表、调试信息……应有尽有。但它不适合直接用于升级,原因很简单——太臃肿了。
我一般在开发阶段用 ELF 调试,发布时再通过 objcopy 转成 Bin 或 Hex:
# ELF 转 Bin
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
# ELF 转 Hex
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
4.5 镜像头结构——Bootloader 的「身份证」
不管用哪种格式,最终烧到 Flash 里的固件,我都会给它加一个「镜像头」。这就像给固件办了一张身份证,Bootloader 拿到后先验明正身,再决定要不要升级。
一个典型的镜像头结构如下:
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0x00 | Magic Number | 4 字节 | 魔数,用于快速识别 |
| 0x04 | 版本号 | 4 字节 | 主版本.次版本.修订号 |
| 0x08 | 固件长度 | 4 字节 | 有效数据长度 |
| 0x0C | 校验和 | 4 字节 | CRC32 或 SHA256 |
| 0x10 | 签名区域 | 256 字节 | RSA/ECDSA 签名 |
| 0x110 | 固件数据 | 可变 | 实际代码 |
4.5.1 Magic Number——快速识别的「暗号」
Magic Number 就是一个固定值,比如 0xDEADBEEF 或 0xA5A5A5A5。Bootloader 读到 Flash 的第一个字,先检查它是不是预期的魔数。不是?直接跳过,不浪费时间。
0xFEEDBEEF,既好记又不容易撞车。
4.5.2 版本号——升级决策的依据
版本号我一般用 4 字节表示:
- 高 2 字节:主版本号
- 次高 1 字节:次版本号
- 最低 1 字节:修订号
比如 0x00020001 表示 v2.1。Bootloader 比较版本号时,必须做数值比较,而不是字符串比较。我见过有人用字符串存版本号,结果 "v2.10" 比 "v2.9" 小,闹了笑话。
4.5.3 校验和——数据完整性的「守门员」
校验和有很多种算法,我按可靠性排个序:
- 简单累加和: 最快,但容易漏错。适合对安全性要求不高的场景。
- CRC32: 速度和可靠性均衡。我个人最常用。
- SHA256: 最安全,但计算量大。适合安全升级场景。
代码示例(CRC32 校验):
uint32_t calculate_crc32(uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc >>= 1;
}
}
return ~crc;
}
4.6 镜像签名区域——防篡改的「封条」
签名区域是安全升级的最后一道防线。它用非对称加密算法(如 RSA、ECDSA)对镜像头 + 固件数据做签名。
流程是这样的:
- 生产端用私钥对固件签名,生成 256 字节签名数据
- Bootloader 用公钥验证签名
- 验证通过才允许升级
4.7 知识体系总览
下面这张图,把固件格式和镜像头的核心关系串起来了:
4.8 小结
嗯,这一节内容不少。总结下来就三句话:
- Bin 文件是升级的主力,简单高效
- 镜像头是固件的身份证,魔数、版本、校验、签名缺一不可
- 签名验证是安全底线,千万别偷懒
我个人建议,在设计 Bootloader 时,先把镜像头结构定死,后续所有工作都围绕这个结构展开。格式定了,后面的事情就顺了。