4、固件镜像格式:Bin、Hex、ELF 与镜像头设计

做 Bootloader 这么多年,我接触过各种各样的固件格式。说实话,很多刚入行的工程师容易在这上面栽跟头。你想想看,如果连固件长什么样都不清楚,怎么保证升级不出问题?

这一节,我们就来聊聊 Bin、Hex、ELF 这三种最常见的固件格式,以及镜像头里那些不得不说的门道。

4.1 三种固件格式的本质区别

先给个直观的对比,你一看就明白:

特性 Bin 文件 Hex 文件 ELF 文件
本质 纯二进制数据 ASCII 编码的二进制 可执行与链接格式
地址信息 无(裸数据) 每行包含地址 包含完整段信息
文件大小 最小 约为 Bin 的 2 倍 最大(含调试信息)
烧录/升级用途 最常用 常用于烧录器 调试阶段使用
是否可直接运行 是(需加载到正确地址) 否(需解析) 否(需加载器)

4.2 Bin 文件——最纯粹的固件形态

Bin 文件,说白了就是内存的「快照」。它从起始地址开始,一个字节挨着一个字节,没有任何多余信息。

特点:

  • 没有地址信息,你必须知道它该烧到哪个位置
  • 没有校验,数据对错全靠你自己
  • 文件大小等于实际代码大小(对齐后)
我的习惯: 在 Bootloader 升级中,我几乎只用 Bin 格式。为什么?因为它简单、直接、解析快。嵌入式设备的 Flash 空间本来就紧张,Bin 文件体积最小,传输和存储都划算。

4.3 Hex 文件——带地址的「快递单」

Hex 文件(Intel HEX 格式)每行都包含地址和校验。它像是一张张快递单,告诉你每个包裹该送到哪个地址。

一个典型的 Hex 行长这样:

:10000000FF0040E3390000000000000000F0
:1000100000000000000000000000000000000000
:0400000500000000F5
:00000001FF

格式解析:

  • : 起始标记
  • 10 本行数据长度(16 字节)
  • 0000 起始地址
  • 00 记录类型(00=数据,01=结束)
  • 后面是数据,最后两位是校验和
我曾经踩过的坑: 有一次用 Hex 文件做升级,没注意地址是分段不连续的。结果 Bootloader 解析时,把中间的空洞区域也当成有效数据写进了 Flash,导致程序跑飞。后来我加了个地址连续性检查,才彻底解决。

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 的第一个字,先检查它是不是预期的魔数。不是?直接跳过,不浪费时间。

避坑指南: 魔数不要选太常见的值,比如 0xFFFFFFFF(擦除后的状态)或 0x00000000。我习惯用 0xFEEDBEEF,既好记又不容易撞车。

4.5.2 版本号——升级决策的依据

版本号我一般用 4 字节表示:

  • 高 2 字节:主版本号
  • 次高 1 字节:次版本号
  • 最低 1 字节:修订号

比如 0x00020001 表示 v2.1。Bootloader 比较版本号时,必须做数值比较,而不是字符串比较。我见过有人用字符串存版本号,结果 "v2.10" 比 "v2.9" 小,闹了笑话。

4.5.3 校验和——数据完整性的「守门员」

校验和有很多种算法,我按可靠性排个序:

  1. 简单累加和: 最快,但容易漏错。适合对安全性要求不高的场景。
  2. CRC32: 速度和可靠性均衡。我个人最常用。
  3. 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)对镜像头 + 固件数据做签名。

流程是这样的:

  1. 生产端用私钥对固件签名,生成 256 字节签名数据
  2. Bootloader 用公钥验证签名
  3. 验证通过才允许升级
我曾经犯过的错: 早期做签名验证时,我只验证了镜像头,没验证固件数据。结果攻击者篡改了固件内容,但保留了原签名,Bootloader 照样放行。后来我把签名范围改成「镜像头 + 整个固件数据」,才堵住这个漏洞。

4.7 知识体系总览

下面这张图,把固件格式和镜像头的核心关系串起来了:

固件镜像格式与镜像头结构总览 Bin 文件(裸数据) Hex 文件(带地址) ELF 文件(含调试) 最终烧录格式:Bin + 自定义镜像头 Magic Number 版本号 固件长度 校验和 签名区域 固件数据(实际代码) 关键说明: • 开发阶段:使用 ELF 调试 → 编译生成 Hex/Bin • 发布阶段:Bin + 自定义镜像头(含签名)→ 最终固件 • Bootloader 启动流程:检查 Magic → 校验版本 → 验证 CRC → 验证签名 → 加载固件

4.8 小结

嗯,这一节内容不少。总结下来就三句话:

  • Bin 文件是升级的主力,简单高效
  • 镜像头是固件的身份证,魔数、版本、校验、签名缺一不可
  • 签名验证是安全底线,千万别偷懒

我个人建议,在设计 Bootloader 时,先把镜像头结构定死,后续所有工作都围绕这个结构展开。格式定了,后面的事情就顺了。