固件镜像格式解析:raw、flat、srec、ihex、elf 的头部结构与校验域

做网络启动固件校验,第一关就是搞清楚你手里拿的到底是什么格式。我见过不少同事,拿到一个 .bin 文件就直接往 Flash 里写,结果板子变砖了才回头查——原来是格式没对上。

说白了,固件镜像的格式决定了三件事:数据怎么组织、地址怎么映射、校验怎么算。今天咱们就把最常见的五种格式——raw、flat、srec、ihex、elf——挨个拆开看看。

核心观点: 没有「最好」的格式,只有「最合适」的场景。网络启动时,你往往需要在多种格式之间做转换,而校验域就是那个「转换后还能保证数据没坏」的保险丝。

固件镜像格式分类与校验域关系图 固件镜像格式分类与校验域 无结构格式 文本/记录格式 可执行链接格式 Raw / Binary Flat (非结构化) SREC (S-Record) Intel HEX (IHEX) ELF (Executable) 校验域位置对比 Raw/Flat:无内建校验 → 需外部 CRC/哈希 SREC/IHEX:每行记录自带校验和 → 行级校验 ELF:Section/Program Header 内嵌校验 → 段级校验

1. Raw / Binary 格式:最纯粹,也最「裸」

Raw 格式,也叫纯二进制格式。它没有头部、没有尾部、没有元数据——就是内存映像的逐字节转储。你拿十六进制编辑器打开,看到的就是 CPU 要执行的原始指令和数据。

头部结构: 没有。真的没有。从偏移 0 开始就是代码。

校验域: 也没有。Raw 格式本身不提供任何校验机制。我早期做 BootROM 开发时,就吃过这个亏——下载了一个 raw 镜像,烧到一半网络断了,结果烧进去的是半截数据,板子直接变砖。从那以后,我坚持在 raw 镜像尾部追加一个 4 字节的 CRC32。

我的习惯: 给 raw 镜像加一个 16 字节的尾部元数据块,包含镜像长度、CRC32、版本号。这样既保留了 raw 的简单性,又有了基本的完整性保障。

2. Flat 格式:比 raw 多一点点「规矩」

Flat 格式,严格来说不是一种标准格式,而是一类「非结构化但带加载地址」的镜像。常见于一些 RTOS 或裸机系统的固件打包工具。

它的典型结构是:

+------------------+
| 加载基地址 (4B)   |  ← 通常是 0x80000000 这样的值
+------------------+
| 镜像长度 (4B)     |
+------------------+
| 数据体           |  ← 和 raw 一样,纯二进制
+------------------+
| 可选校验域 (4B)   |  ← 有些实现会加 CRC
+------------------+

我在一个老项目里见过一种「Flat+」格式,它在头部放了 3 个域:加载地址、入口点、长度。校验域放在尾部,用的是简单的 XOR 累加和。说实话,这种校验强度不够,但胜在计算快——适合资源受限的 MCU。

注意: Flat 格式没有统一标准。不同工具生成的 flat 镜像,头部字段顺序可能不同。跨工具使用时,一定要先确认字节序(Endianness)和字段定义。

3. SREC 格式:每行都带「身份证」

SREC(Motorola S-Record)是文本格式的固件。每行以 'S' 开头,后面跟记录类型、字节数、地址、数据、校验和。

一个典型的 S19 记录长这样:

S1137F0000 102030405060708090A0B0C0D0E0F0 6A
| |   |    |                              |
| |   |    +-- 数据体 (16 字节)            +-- 校验和
| |   +------- 地址 (4 字节, 0x7F0000)
| +----------- 字节数 (0x13 = 19 字节)
+------------- 记录类型 (S1 = 16位地址数据记录)

校验域解析: 每行最后一个字节就是校验和。算法很简单:取该行所有字节(从字节数到数据末尾)的和,取低 8 位,再取反。

我记得有一次调试 Bootloader,发现下载的 SREC 文件总是校验失败。查了半天,原来是上位机工具把校验和算成了「补码」而不是「反码」。嗯,这种细节坑过不少人。

记录类型 地址宽度 典型用途 校验域位置
S0 16 位 文件头(含注释) 行尾 1 字节
S1 16 位 数据记录(8KB 以内) 行尾 1 字节
S2 24 位 数据记录(16MB 以内) 行尾 1 字节
S3 32 位 数据记录(4GB 以内) 行尾 1 字节
S5 16 位 记录计数 行尾 1 字节
S7/S8/S9 32/24/16 位 结束记录(含入口地址) 行尾 1 字节

实战要点: 网络传输 SREC 时,文本行可能被插入换行符或空格。健壮的解析器应该忽略空白字符,只认 'S' 开头的有效行。我曾经见过一个解析器因为行尾多了回车符,把校验和算错了——后来我加了一步「行内非十六进制字符过滤」才搞定。

4. Intel HEX 格式:和 SREC 是「表兄弟」

Intel HEX(IHEX)也是文本格式,但记录以冒号 ':' 开头。结构上和 SREC 类似,但字段顺序和校验算法略有不同。

一个典型的 IHEX 记录:

:10 7F00 00 102030405060708090A0B0C0D0E0F0 6A
 |  |    |  |                              |
 |  |    |  +-- 数据体 (16 字节)            +-- 校验和
 |  |    +----- 记录类型 (00 = 数据)
 |  +---------- 地址 (16 位, 0x7F00)
 +------------- 字节数 (0x10 = 16 字节)

校验域算法: 和 SREC 类似,但取的是 补码。具体来说:计算从字节数到数据末尾所有字节的和,取低 8 位,然后用 0x100 减去这个值。

你想想看,为什么 IHEX 用补码而 SREC 用反码?其实没特别深的原因——就是两家公司(Intel 和 Motorola)各自定的标准。我在做跨平台工具链时,经常要同时处理这两种格式,校验算法写错了就会出大问题。

避坑指南: 我曾经在解析 IHEX 时,遇到过大文件(超过 64KB)需要用扩展线性地址记录(类型 04)来切换高 16 位地址。如果解析器不处理这个记录,数据就会写到错误的地址上。校验和算对了,但数据写错了——这种 bug 最难查。

5. ELF 格式:最复杂,也最强大

ELF(Executable and Linkable Format)是 Linux 和多数嵌入式系统的主力格式。它不像 SREC/IHEX 那样逐行校验,而是在文件头和段头里内嵌校验信息。

头部结构(ELF Header):

typedef struct {
    unsigned char e_ident[16];  // 魔数 + 类别 + 字节序 + 版本
    uint16_t      e_type;       // 文件类型 (REL/EXEC/DYN)
    uint16_t      e_machine;    // 架构 (ARM/RISC-V/x86)
    uint32_t      e_version;    // 版本
    uint64_t      e_entry;      // 入口地址
    uint64_t      e_phoff;      // Program Header 偏移
    uint64_t      e_shoff;      // Section Header 偏移
    uint32_t      e_flags;      // 处理器特定标志
    uint16_t      e_ehsize;     // ELF Header 大小
    uint16_t      e_phentsize;  // Program Header 条目大小
    uint16_t      e_phnum;      // Program Header 条目数
    uint16_t      e_shentsize;  // Section Header 条目大小
    uint16_t      e_shnum;      // Section Header 条目数
    uint16_t      e_shstrndx;   // 字符串表索引
} Elf64_Ehdr;

校验域在哪里? ELF 本身没有全局校验和。但它的 Section Header Table 里,每个 Section 可以有一个 sh_flags 字段,其中 SHF_ALLOC 标志表示该段需要加载到内存。网络启动时,我们通常只校验那些 SHF_ALLOC 的段。

我个人的做法是:在 ELF 文件的末尾附加一个自定义的校验段(.note 段或 .shstrtab 之后的填充区),存放整个镜像的 SHA256 哈希。这样既保留了 ELF 的原始结构,又加上了校验能力。

格式 校验域位置 校验算法 校验粒度 网络启动适用性
Raw 无(需外部追加) CRC32 / SHA256 整个文件 需配合外部校验协议
Flat 尾部(可选) XOR / CRC16 整个文件 适合简单 MCU
SREC 每行末尾 反码和 行级 适合串口/低速网络
IHEX 每行末尾 补码和 行级 适合串口/低速网络
ELF Section/Program Header 自定义(无标准) 段级 适合高性能网络启动

实战中的格式选择建议

说了这么多,到底该用哪种?我根据项目经验给几个建议:

  • MCU 裸机/RTOS 系统: 用 SREC 或 IHEX。行级校验能及时发现传输错误,而且文本格式方便调试。我习惯用 SREC,因为它的地址范围更灵活。
  • Linux 内核/应用处理器: 用 ELF。虽然解析复杂,但段级加载和符号信息对调试太重要了。网络启动时,TFTP 传 ELF,Bootloader 解析后直接跳转。
  • 资源极度受限的场景: 用 Raw + 外部 CRC。少了解析开销,但一定要在传输协议层加校验——比如 TFTP 的确认重传机制。

最后提醒一句: 不管你选哪种格式,一定要在固件烧录前做格式验证。我曾经在产线上遇到过一批固件,工具链升级后生成的 ELF 文件 e_machine 字段变成了 0(未定义),Bootloader 直接拒绝加载。幸亏有格式校验,不然几千块板子全得返工。