固件镜像格式:BIN、HEX、ELF 深度解析

做 Bootloader 开发,绕不开这三种文件格式。我见过不少新手,拿着 .bin 文件直接往 Flash 里写,结果跑不起来——其实问题出在文件本身。今天咱们就把 BIN、HEX、ELF 这三兄弟彻底聊透。

为什么 Bootloader 要关心镜像格式?

说白了,Bootloader 的职责就是把固件从 A 点搬到 B 点,然后跳过去执行。但不同的格式,搬运方式完全不同。

  • BIN:纯裸数据,没有地址信息,没有校验,连长度都得你自己算。
  • HEX:带地址、带校验,但解析起来比 BIN 麻烦一点。
  • ELF:最完整,但体积也最大,Bootloader 里几乎不用它做传输格式。

我个人习惯是:开发调试用 ELF,烧录用 HEX,量产用 BIN。这个原则帮我省了不少事。

核心观点: 你的 Bootloader 必须能正确解析至少一种格式。选错了,升级就失败。

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 没有校验,传输错误无法发现
适合量产烧录 无法表示非连续地址空间
我曾经踩过的坑: 有一次量产,产线工人把 BIN 文件烧到了 0x08010000,但实际代码应该放在 0x08000000。结果 100 片板子全部变砖。从那以后,我坚持在 BIN 文件头部加一个 4 字节的 CRC 校验,并在 Bootloader 里做地址校验。

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 解析器时,最头疼的就是这个地址拼接。你想想看,如果解析器忘了处理扩展地址记录,那所有数据都会跑到错误的地址上。

我的建议: 写 HEX 解析器时,先解析所有记录,构建一个地址-数据映射表,然后再统一烧写。不要边解析边烧写,否则遇到乱序记录会出问题。

ELF 文件格式:调试的瑞士军刀

ELF(Executable and Linkable Format)是 Linux 和大多数嵌入式工具链的标准格式。它包含了你能想到的所有信息:代码、数据、符号表、调试信息、重定位表……

ELF 的结构

ELF 文件由三部分组成:

  1. ELF 头部:魔数(0x7F 'E' 'L' 'F')、架构、入口地址、段表偏移等。
  2. 程序头(Program Headers):描述如何加载到内存,Bootloader 最关心的部分。
  3. 节头(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 文件,直接定位到出问题的源码行——原来是中断服务函数里用了一个未初始化的全局变量。

实践建议: 开发阶段用 ELF 调试,发布时用 objcopy 转成 HEX 或 BIN。命令很简单:
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin

三种格式的对比与选择

特性 BIN HEX ELF
文件大小 最小 中等(文本格式,约 2x) 最大(含调试信息)
地址信息 无 有(每行都带) 有(程序头描述)
校验 无 每行校验和 无(但可加 CRC)
解析复杂度 极低 中等 高
适用场景 量产烧录、OTA 升级 烧录器、Bootloader 传输 调试、链接、分析

三种格式的转换关系

下面这张图展示了三种格式在开发流程中的位置和转换关系:

固件镜像格式转换与使用流程 C/ASM 源码 编译 ELF 文件 GDB 调试 调试器/JTAG objcopy HEX 文件 BIN 文件 烧录器 OTA 升级 Flash 烧录 Bootloader 升级 开发调试 → ELF | 烧录调试 → HEX | 量产/OTA → BIN Bootloader 必须支持 HEX 或 BIN 解析,ELF 仅用于主机端调试

实战:Bootloader 如何选择解析格式?

根据我的经验,这里有一个简单的决策树:

  • 如果 Bootloader 空间充足(> 64KB):支持 HEX 解析。HEX 的校验机制能帮你发现传输错误。
  • 如果 Bootloader 空间紧张(< 32KB):只支持 BIN,但必须在 BIN 文件头部加 CRC 校验。
  • 如果使用 OTA 升级:强烈建议用 BIN + 外部 CRC。HEX 的文本格式会让传输量翻倍,对无线传输不友好。
一个小技巧: 我习惯在 BIN 文件的前 16 字节放一个自定义头部:
0x00-0x03: 魔数 (0xDEADBEEF)
0x04-0x07: 固件长度
0x08-0x0B: CRC32 校验
0x0C-0x0F: 固件版本号
这样 Bootloader 在烧写前就能做完整性检查,避免变砖。

总结

三种格式各有各的用处。BIN 是“裸奔”选手,快但危险;HEX 是“带地图”的选手,安全但啰嗦;ELF 是“全副武装”的选手,调试必备但不适合传输。

你想想看,如果你的 Bootloader 连 HEX 都解析不了,那市面上大部分烧录器生成的固件你都用不了。反过来,如果你只支持 BIN,那你就得自己搞定地址映射和校验——也不是不行,但要多写不少代码。

我个人建议:至少支持 HEX 和 BIN 两种格式。HEX 用于开发调试阶段的烧录,BIN 用于量产和 OTA。至于 ELF,那是主机端的事,Bootloader 不需要管它。