固件镜像格式:BIN、HEX、ELF 深度解析
做 Bootloader 开发,绕不开这三种文件格式。我见过不少新手,拿着 .bin 文件直接往 Flash 里写,结果跑不起来——其实问题出在文件本身。今天咱们就把 BIN、HEX、ELF 这三兄弟彻底聊透。
为什么 Bootloader 要关心镜像格式?
说白了,Bootloader 的职责就是把固件从 A 点搬到 B 点,然后跳过去执行。但不同的格式,搬运方式完全不同。
- BIN:纯裸数据,没有地址信息,没有校验,连长度都得你自己算。
- HEX:带地址、带校验,但解析起来比 BIN 麻烦一点。
- ELF:最完整,但体积也最大,Bootloader 里几乎不用它做传输格式。
我个人习惯是:开发调试用 ELF,烧录用 HEX,量产用 BIN。这个原则帮我省了不少事。
BIN 文件格式:最简单,也最容易出错
BIN 文件就是二进制数据的直接转储。没有头部,没有尾部,没有地址信息。你给它什么地址,它就放在什么地址。
BIN 的结构
// 一个典型的 BIN 文件内容(十六进制视图)
地址: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
------------------------------------------------------
0000: 00 80 00 20 71 01 00 00 75 01 00 00 00 00 00 00
0010: 00 00 00 00 00 00 00 00 00 00 00 00 79 01 00 00
...
你看,没有任何标记告诉你这段数据应该放在 0x08000000 还是 0x20000000。这就是 BIN 的“裸”本质。
BIN 的优缺点
| 优点 | 缺点 |
|---|---|
| 体积最小,传输快 | 没有地址信息,容易烧错位置 |
| 解析最简单,直接 memcpy | 没有校验,传输错误无法发现 |
| 适合量产烧录 | 无法表示非连续地址空间 |
HEX 文件格式:带地址的“快递单”
HEX 文件(Intel HEX 格式)是文本文件,每一行都是一条记录。它告诉你:这段数据应该放在哪个地址,以及数据是否正确。
HEX 记录格式
:10000000008000207101000075010000000000000000F0
:1000100000000000000000000000000079010000000000E8
:0400200079010000A7
:00000001FF
每一行的结构是这样的:
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始码 | 1 字节 | 固定为 ':' |
| 数据长度 | 1 字节 | 本行数据的字节数(如 0x10 = 16 字节) |
| 地址 | 2 字节 | 数据存放的起始地址 |
| 记录类型 | 1 字节 | 00=数据, 01=文件结束, 02=扩展段地址, 04=扩展线性地址 |
| 数据 | N 字节 | 实际固件数据 |
| 校验和 | 1 字节 | 前面所有字节的和取补码 |
HEX 解析的关键点
嗯,这里要注意:HEX 文件支持 32 位地址空间。它通过记录类型 04(扩展线性地址)来指定高 16 位地址。比如:
:020000040800F2 // 设置基地址为 0x0800
:1000000000800020... // 数据放在 0x08000000 + 0x0000 = 0x08000000
我在写 Bootloader 的 HEX 解析器时,最头疼的就是这个地址拼接。你想想看,如果解析器忘了处理扩展地址记录,那所有数据都会跑到错误的地址上。
ELF 文件格式:调试的瑞士军刀
ELF(Executable and Linkable Format)是 Linux 和大多数嵌入式工具链的标准格式。它包含了你能想到的所有信息:代码、数据、符号表、调试信息、重定位表……
ELF 的结构
ELF 文件由三部分组成:
- ELF 头部:魔数(0x7F 'E' 'L' 'F')、架构、入口地址、段表偏移等。
- 程序头(Program Headers):描述如何加载到内存,Bootloader 最关心的部分。
- 节头(Section Headers):描述代码段、数据段、符号表等,调试器最关心的部分。
// ELF 头部结构(简化版)
typedef struct {
uint8_t e_ident[16]; // 魔数 + 类别 + 字节序 + 版本
uint16_t e_type; // 文件类型(可重定位/可执行/共享库)
uint16_t e_machine; // 架构(ARM/RISC-V/x86)
uint32_t e_version; // 版本
uint32_t e_entry; // 入口地址
uint32_t e_phoff; // 程序头偏移
uint32_t e_shoff; // 节头偏移
uint32_t e_flags; // 标志
uint16_t e_ehsize; // ELF 头部大小
uint16_t e_phentsize; // 每个程序头大小
uint16_t e_phnum; // 程序头数量
uint16_t e_shentsize; // 每个节头大小
uint16_t e_shnum; // 节头数量
uint16_t e_shstrndx; // 节名字符串表索引
} Elf32_Ehdr;
为什么 Bootloader 不用 ELF?
说白了,ELF 太“胖”了。一个 ELF 文件可能包含几十 KB 的调试信息,这些信息在运行时完全没用。而且 ELF 的解析比 HEX 复杂得多,Bootloader 的 ROM 空间本来就紧张,塞一个 ELF 解析器不划算。
但调试时 ELF 是神器。我记得有一次,客户报告说固件在某个特定条件下死机。我用 GDB 加载 ELF 文件,直接定位到出问题的源码行——原来是中断服务函数里用了一个未初始化的全局变量。
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hexarm-none-eabi-objcopy -O binary firmware.elf firmware.bin
三种格式的对比与选择
| 特性 | BIN | HEX | ELF |
|---|---|---|---|
| 文件大小 | 最小 | 中等(文本格式,约 2x) | 最大(含调试信息) |
| 地址信息 | 无 | 有(每行都带) | 有(程序头描述) |
| 校验 | 无 | 每行校验和 | 无(但可加 CRC) |
| 解析复杂度 | 极低 | 中等 | 高 |
| 适用场景 | 量产烧录、OTA 升级 | 烧录器、Bootloader 传输 | 调试、链接、分析 |
三种格式的转换关系
下面这张图展示了三种格式在开发流程中的位置和转换关系:
实战:Bootloader 如何选择解析格式?
根据我的经验,这里有一个简单的决策树:
- 如果 Bootloader 空间充足(> 64KB):支持 HEX 解析。HEX 的校验机制能帮你发现传输错误。
- 如果 Bootloader 空间紧张(< 32KB):只支持 BIN,但必须在 BIN 文件头部加 CRC 校验。
- 如果使用 OTA 升级:强烈建议用 BIN + 外部 CRC。HEX 的文本格式会让传输量翻倍,对无线传输不友好。
0x00-0x03: 魔数 (0xDEADBEEF)0x04-0x07: 固件长度0x08-0x0B: CRC32 校验0x0C-0x0F: 固件版本号这样 Bootloader 在烧写前就能做完整性检查,避免变砖。
总结
三种格式各有各的用处。BIN 是“裸奔”选手,快但危险;HEX 是“带地图”的选手,安全但啰嗦;ELF 是“全副武装”的选手,调试必备但不适合传输。
你想想看,如果你的 Bootloader 连 HEX 都解析不了,那市面上大部分烧录器生成的固件你都用不了。反过来,如果你只支持 BIN,那你就得自己搞定地址映射和校验——也不是不行,但要多写不少代码。
我个人建议:至少支持 HEX 和 BIN 两种格式。HEX 用于开发调试阶段的烧录,BIN 用于量产和 OTA。至于 ELF,那是主机端的事,Bootloader 不需要管它。