4. 固件镜像格式:二进制镜像、ELF与HEX格式、镜像头部结构

说到固件升级,第一个绕不开的话题就是——固件镜像到底长什么样?

我刚开始做嵌入式那会儿,拿到一个.bin文件就直接往Flash里烧,从来没想过里面是什么结构。直到有一次产品量产,发现一批设备升级后变砖了,排查了三天才发现是镜像格式搞错了。嗯,从那以后我再也不敢小看这个环节了。

这一章,我们就来把固件镜像的几种常见格式、它们的内部结构,以及头部设计的关键点讲清楚。你想想看,搞懂了这些,后面做升级方案的时候心里就有底了。

4.1 三种主流镜像格式:Bin、HEX、ELF

嵌入式世界里,最常见的固件镜像格式就这三种。它们各有各的用途,也各有各的坑。

格式 全称 典型后缀 是否包含元数据 是否可执行 典型用途
二进制镜像 Raw Binary .bin 否 否(需加载到指定地址) 量产烧录、OTA升级包
HEX格式 Intel HEX .hex 是(地址、校验) 否 烧录器、调试器传输
ELF格式 Executable and Linkable Format .elf / .out 是(符号表、段信息、调试信息) 是 编译输出、调试、链接

4.1.1 二进制镜像(Raw Binary)

说白了,二进制镜像就是固件最原始的样子——从Flash起始地址开始,一个字节一个字节地排列,没有任何多余信息。你把它读到内存里,PC指针指过去,CPU就开始执行了。

优点:

  • 体积最小,没有冗余数据
  • 解析最简单,直接memcpy就能加载
  • 适合OTA传输,省带宽

缺点:

  • 没有地址信息,你必须知道它该烧到哪个地址
  • 没有校验,传输过程中出错了你也不知道
  • 没有符号表,出问题了很难定位
我的经验:量产固件我几乎只用.bin格式。原因很简单——体积小、解析快、不容易出幺蛾子。但我会在.bin前面加一个自定义头部,把版本号、校验和、长度这些信息塞进去。后面会详细讲。

4.1.2 Intel HEX格式

HEX格式是文本形式的固件,每一行都是一条记录。它包含了地址信息和校验,所以烧录器可以根据地址把数据放到正确的位置。

一个典型的HEX文件长这样:

:10000000C0001E0000000000000000000000000000F0
:100010000000000000000000000000000000000000E0
:100020000000000000000000000000000000000000D0
:00000001FF

每一行的结构:

  • 起始码:冒号(:)
  • 字节长度:1字节,表示本行数据字节数
  • 地址:2字节,数据加载的起始地址
  • 记录类型:1字节(00=数据,01=文件结束,02=扩展段地址,04=扩展线性地址)
  • 数据:N字节
  • 校验和:1字节,前面所有字节之和取补码
注意:HEX格式虽然方便,但体积比二进制大2-3倍。我曾经见过有人直接用HEX做OTA升级包,结果因为传输时间太长导致升级失败。OTA场景下,建议先把HEX转成BIN再传输。

4.1.3 ELF格式

ELF是编译器的最终输出格式,包含了所有段信息、符号表、调试信息。它本身是可以被操作系统加载执行的,但在裸机MCU上,我们通常只取其中的代码段和数据段。

ELF文件的核心结构:

  • ELF头部:魔数、架构、入口点、段表偏移等
  • 程序头表:描述如何加载到内存
  • 节头表:描述各个段(.text, .data, .bss等)
  • 段数据:实际的代码和数据

我建议在开发阶段保留ELF文件,方便调试。但到了发布阶段,一定要转成BIN或者HEX。为什么?因为ELF里包含了大量调试信息,体积可能是BIN的10倍以上,而且直接烧录ELF到Flash可能会把符号表也写进去,浪费空间。

4.2 镜像头部结构:给固件加个身份证

好了,现在我们知道该用BIN格式做升级包了。但裸BIN有个问题——你怎么知道这个固件是给哪个设备用的?版本对不对?有没有被篡改过?

所以我们需要在BIN数据前面加一个头部(Header),就像给固件办了一张身份证。

一个典型的固件头部长这样:

typedef struct {
    uint32_t magic;          // 魔数,用于识别固件格式
    uint32_t version;        // 固件版本号
    uint32_t length;         // 固件数据长度
    uint32_t checksum;       // 校验和
    uint32_t reserved[4];    // 保留字段,用于扩展
    uint8_t  signature[256]; // 数字签名(可选)
    // 后面跟着固件数据
} firmware_header_t;

4.2.1 Magic Number(魔数)

魔数就是一个固定的标识符,用来告诉Bootloader:嘿,这是个合法的固件镜像!

常见的做法是用4个字节的ASCII字符,比如:

  • 0x4649524D → "FIRM"
  • 0x554E494F → "UNIO"
  • 0xDEADBEEF → 经典调试用魔数

我在项目中习惯用公司缩写+产品代号,比如0x544C4D31("TLM1"),这样一眼就能看出是哪个产品的固件。

为什么要用魔数? 防止Bootloader把普通数据当成固件执行。如果Flash里全是0xFF或者随机数据,魔数不匹配,Bootloader就直接跳过,不会尝试启动。

4.2.2 版本号

版本号的设计看似简单,但坑不少。我见过有人用一个uint32_t表示版本号,结果升级时发现新版本号比旧版本号小,导致降级判断出错。

推荐的做法:

  • 主版本号:8位,重大架构变更
  • 次版本号:8位,功能新增
  • 修订号:16位,Bug修复

或者直接用时间戳:

uint32_t version = (year << 16) | (month << 8) | day;

我个人更倾向于用递增的版本号,因为比较大小方便。时间戳版本号有个问题——如果同一天发布了两个版本,版本号就一样了。

4.2.3 校验和(Checksum)

校验和用来检测固件在传输或存储过程中有没有损坏。常用的算法有:

算法 输出长度 速度 安全性 推荐场景
CRC32 4字节 快 低(可碰撞) 一般固件校验
MD5 16字节 中等 中等(已不推荐安全场景) OTA完整性校验
SHA256 32字节 慢 高 安全升级、签名验证
简单累加和 1-4字节 极快 极低 快速检测(不推荐)
我的建议: 普通产品用CRC32就够了,速度快,硬件支持也好。如果涉及安全,至少用SHA256。千万别用简单的累加和——我曾经在一个项目里用累加和,结果数据错了两处,累加和居然没变,排查了一整天。

4.3 镜像签名:给固件加个防伪标签

校验和只能检测数据是否损坏,但无法防止恶意篡改。如果有人截获了你的升级包,改了里面的代码,重新计算CRC,你的设备就会执行恶意固件。

这就是签名的作用。

签名的基本流程:

  1. 生成密钥对:私钥保存在你的构建服务器上,公钥烧录在设备里
  2. 计算哈希:对固件数据计算SHA256哈希
  3. 签名:用私钥对哈希值进行RSA或ECDSA签名
  4. 打包:将签名附加到固件头部
  5. 验证:设备收到固件后,用公钥验证签名

一个带签名的固件头部结构:

typedef struct {
    uint32_t magic;          // 魔数
    uint32_t version;        // 版本号
    uint32_t length;         // 固件长度
    uint32_t checksum;       // CRC32校验
    uint8_t  hash[32];       // SHA256哈希
    uint8_t  signature[256]; // RSA-2048签名
    uint32_t sig_len;        // 签名长度
    uint8_t  reserved[8];    // 保留
} secure_firmware_header_t;
重要提醒: 签名验证必须在Bootloader里做,不能在应用程序里做。为什么?因为如果应用程序被篡改了,它可能直接跳过验证。我曾经见过一个产品,签名验证放在应用层,结果黑客直接修改了验证函数,让它永远返回成功。

4.4 知识体系总览

下面这张图把本章的核心内容串起来了,你可以对照着回顾一下:

固件镜像格式知识体系 二进制镜像 (.bin) Intel HEX (.hex) ELF (.elf) 固件头部结构 (Firmware Header) Magic Number 版本号 校验和 长度 签名 镜像签名 (Firmware Signature) 私钥签名 → 公钥验证 → 防篡改 💡 核心要点: • 量产/OTA用BIN,开发调试用ELF,烧录器传输用HEX • 头部结构是固件的身份证,魔数、版本、校验、签名缺一不可

4.5 实战建议:如何选择镜像格式

讲了这么多,最后给几个实际项目中的建议:

  • 开发阶段:用ELF,方便调试和符号定位
  • 本地烧录:用HEX,烧录器自动处理地址映射
  • OTA升级:用BIN + 自定义头部,体积小、传输快
  • 量产烧录:用BIN,配合烧录工具直接写入Flash
  • 安全要求高:BIN + 签名,头部里放SHA256和RSA签名
一句话总结: 搞懂镜像格式,就是搞懂固件从编译到烧录的整个链路。头部结构设计得好,后面做升级、做安全、做兼容都会省很多事。别像我当年那样,等到变砖了才回头补课。