4、固件镜像格式:BIN、HEX、ELF 格式解析、镜像头结构与校验和
做嵌入式烧录这么多年,我见过太多因为镜像格式搞错导致的「变砖」事故。说白了,你手里的固件文件到底是什么格式,决定了烧录工具怎么解析它、怎么把它塞进芯片里。今天咱们就把 BIN、HEX、ELF 这三兄弟彻底聊透。
4.1 三种格式的本质差异
先给个直观对比,你一看就明白:
| 特性 | BIN | HEX (Intel HEX) | ELF |
|---|---|---|---|
| 本质 | 纯二进制数据流 | ASCII 文本 + 地址 + 校验 | 可执行链接格式(含元数据) |
| 是否包含地址信息 | 否(只有数据) | 是(每行都有地址) | 是(段表 + 符号表) |
| 是否包含校验 | 否 | 是(每行末尾校验和) | 否(但段内可自定义) |
| 烧录前是否需要解析 | 直接按偏移写入 | 需解析地址 + 重组数据 | 需提取代码段 + 数据段 |
| 典型大小 | 最小(无冗余) | 比 BIN 大约 2 倍 | 最大(含调试信息) |
| 适用场景 | 量产烧录、OTA 升级 | 调试阶段、小批量烧录 | 调试器加载、符号级调试 |
我个人习惯:量产用 BIN,调试用 ELF,中间过渡用 HEX。为什么?往下看。
4.2 BIN 格式——最纯粹的二进制
BIN 文件就是芯片 Flash 内容的「内存快照」。它从起始地址开始,一个字节挨着一个字节,没有任何包装。
核心要点:BIN 文件本身不记录它应该烧录到哪个地址。这个地址信息由烧录工具或配置文件指定。所以如果你拿一个 BIN 文件烧到错误地址,芯片必死无疑。
举个例子,一个 32KB 的 BIN 文件,内容就是 32768 个字节。烧录时工具从 Flash 基地址(比如 0x08000000)开始,把这 32768 个字节依次写入。
// BIN 文件结构示意(无头无尾,纯数据)
偏移 0x0000: 0x00 0x01 0x02 0x03 ... 0xFF
偏移 0x1000: 0x10 0x20 0x30 0x40 ...
// 没有任何地址标记,没有任何校验
我曾经踩过的坑:有一次量产,产线工人把 BIN 文件的偏移地址配错了 4 个字节。结果 1000 片板子全部无法启动。从那以后,我强制要求所有 BIN 烧录脚本必须做「地址双重校验」——工具读一次,脚本再读一次。
4.3 HEX 格式——带地址的文本化镜像
Intel HEX 格式,说白了就是把二进制数据转成可读的 ASCII 文本,每行都带着地址和校验。它的结构非常清晰:
:10000000 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F F0
:10001000 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F E0
:00000001 FF
每行格式拆解:
- : 起始标记(冒号)
- 10 本行数据长度(16 字节)
- 0000 数据起始地址
- 00 记录类型(00=数据,01=文件结束,02=扩展段地址,04=扩展线性地址)
- 00 01 02 ... 0F 实际数据(16 字节)
- F0 校验和(前面所有字节累加取反 + 1)
你想想看,为什么 HEX 比 BIN 大两倍?因为每个字节被拆成了两个 ASCII 字符,再加上地址、长度、校验等开销。但好处也很明显:你可以直接看文本就知道数据对不对。
我的经验:调试阶段我特别喜欢用 HEX。有一次芯片烧录后跑飞了,我打开 HEX 文件一看,发现某行的校验和是错的。工具没报错,但芯片读到了错误数据。这就是 HEX 自带的「防呆」机制在起作用。
4.4 ELF 格式——调试器的「母语」
ELF(Executable and Linkable Format)是编译器直接输出的格式。它包含的远不止代码数据:
- ELF 头:魔数、架构、入口点、段表偏移
- 程序头(段表):描述哪些段需要加载到内存,加载到哪个地址
- 节头(节表):.text(代码)、.data(初始化数据)、.bss(未初始化数据)、.rodata(只读数据)
- 符号表:函数名、变量名、行号信息(调试用)
// ELF 文件头关键字段(32位架构)
typedef struct {
unsigned char e_ident[16]; // 魔数: 0x7F 'E' 'L' 'F'
uint16_t e_type; // 类型: 2 = 可执行文件
uint16_t e_machine; // 架构: 40 = ARM
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;
ELF 不能直接烧录到 Flash。为什么?因为它包含大量调试信息、符号表、重定位信息,这些在运行时根本不需要。烧录前必须用 objcopy 工具把 ELF 转成 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
# 查看 ELF 段信息
arm-none-eabi-readelf -S firmware.elf
4.5 镜像头结构与校验和
很多芯片厂商(比如 STM32、NXP、TI)会在 BIN 文件前面加一个自定义的镜像头。这个头里记录了:
- 镜像长度
- CRC32 或 SHA256 校验值
- 固件版本号
- 芯片型号 ID
- 签名(如果启用安全启动)
一个典型的镜像头结构:
typedef struct {
uint32_t magic; // 魔数: 0xDEADBEEF
uint32_t image_size; // 固件总大小(含头)
uint32_t header_crc; // 镜像头自身的 CRC32
uint32_t data_crc; // 固件数据的 CRC32
uint32_t version; // 固件版本号
uint32_t chip_id; // 目标芯片 ID
uint32_t reserved[2]; // 保留
uint8_t signature[32]; // RSA/ECDSA 签名
} image_header_t;
校验和的计算方式,我推荐两种:
- 简单累加和:所有字节相加,取低 16 位。速度快,但碰撞率高。适合 bootloader 内部快速校验。
- CRC32:用多项式 0xEDB88320 计算。碰撞率极低,适合 OTA 升级校验。
我曾经踩过的坑:有一次做 OTA 升级,bootloader 里用的累加和校验,结果网络传输中数据被篡改了 3 个字节,累加和竟然没变!从那以后,所有 OTA 项目我强制要求用 CRC32 甚至 SHA256。
4.6 三种格式的转换关系图
下面这张图帮你理清编译器输出到最终烧录文件的完整链路:
4.7 实战建议:如何选择格式
根据我的项目经验,给你几条硬建议:
- 量产烧录:用 BIN + 自定义镜像头(含 CRC32)。速度快,体积小,校验可靠。
- 调试阶段:用 ELF 配合调试器,符号表能帮你快速定位问题。烧录时转成 HEX 做验证。
- OTA 升级:必须用 BIN + 镜像头 + 签名。HEX 的文本格式会增大传输量,ELF 的调试信息是累赘。
- 跨平台兼容:HEX 是「通用语言」,几乎所有烧录工具都支持。如果你不确定对方用什么工具,给 HEX 最保险。
我的习惯:每次发布固件,我都会同时生成 BIN 和 HEX 两个版本。BIN 给产线,HEX 给售后。这样不管对方用什么工具,都能直接烧录。
嗯,格式这块就聊到这儿。记住一句话:格式是工具,校验是底线。不管你用哪种格式,校验和一定要做,镜像头一定要加。这是我对所有嵌入式工程师的忠告。