固件镜像结构解析:Bin/Hex文件格式、Flash布局与向量表
做固件逆向分析,第一件事就是搞清楚你手里拿到的文件到底是什么。我见过不少新手,拿到一个.bin文件就开始反汇编,结果发现地址全对不上——说白了,连文件格式都没搞明白,后面全是白费功夫。
这一章,我们就来聊聊固件镜像的底层结构。Bin和Hex有什么区别?固件在Flash里到底怎么放的?向量表又是什么?这些是后续所有校验机制分析和补丁操作的基础。
2.1 Bin与Hex文件格式
嵌入式开发中,最常见的两种固件分发格式就是Bin和Hex。它们承载的内容其实是一样的——都是MCU要执行的机器码——但组织形式完全不同。
| 特性 | Bin文件 | Hex文件 |
|---|---|---|
| 存储内容 | 纯二进制数据,无元信息 | ASCII文本,包含地址、校验等信息 |
| 地址信息 | 无,需烧录器指定基地址 | 每条记录自带地址偏移 |
| 文件大小 | 等于实际数据大小 | 约为实际数据的2-3倍 |
| 校验机制 | 无内建校验 | 每行有校验和,末尾有文件校验 |
| 适用场景 | 量产烧录、OTA升级包 | 调试、分段烧录、非连续地址 |
Bin文件,说白了就是内存的“裸拷贝”。从起始地址开始,一个字节挨着一个字节,没有任何额外信息。我早期做STM32开发时,经常用fromelf --bin命令把编译产物转成bin,然后直接烧到0x08000000。简单粗暴,但有个坑——如果你不知道它应该烧到哪个地址,那就麻烦了。
Hex文件(Intel HEX格式)就友好得多。它每行都以冒号开头,结构如下:
:10002000A0E10000A0E10000A0E10000A0E1000078
│││ │ │ │
│││ │ │ └── 校验和
│││ │ └── 数据(16字节)
│││ └── 地址(0x0020)
││└── 记录类型(00=数据, 01=结束, 02/04=扩展地址)
│└── 本行数据长度(0x10 = 16字节)
└── 起始标记
2.2 固件在Flash中的布局
MCU上电后,Flash里的内容就是它的整个世界。不同MCU厂商的Flash布局略有差异,但大体上都遵循一个通用模式。我以最常见的ARM Cortex-M系列为例,给你画一张典型的Flash布局图:
这张图你一定要记住。每次拿到一个固件,我第一件事就是定位向量表,然后顺着它找到复位向量,再找到main函数。这是逆向分析的“破冰”步骤。
2.3 向量表与入口地址
向量表,是MCU启动的“第一张地图”。Cortex-M系列上电后,硬件会自动从Flash起始地址读取两个关键值:
- 地址0x00000000(或0x08000000): 读取主栈指针(MSP)的初始值
- 地址0x00000004(或0x08000004): 读取复位向量,即程序入口地址
然后CPU直接跳转到这个地址开始执行。就这么简单,没有其他初始化过程。
一个标准的向量表结构如下(以STM32F103为例):
; 向量表起始地址:0x08000000
0x08000000: 0x20001000 ; MSP初始值(栈顶地址)
0x08000004: 0x08000145 ; 复位向量(注意:最低位为1表示Thumb模式)
0x08000008: 0x08000201 ; NMI异常
0x0800000C: 0x08000205 ; HardFault异常
0x08000010: 0x08000209 ; MemManage异常
0x08000014: 0x0800020D ; BusFault异常
0x08000018: 0x08000211 ; UsageFault异常
...
0x08000040: 0x08000301 ; SysTick中断
0x08000044: 0x08000305 ; 外部中断0
0x08000048: 0x08000309 ; 外部中断1
...
关键点:向量表中的地址值,最低位必须是1。这是ARM Thumb指令集的要求——表示处理器处于Thumb状态。如果你在逆向时看到某个向量地址的最低位是0,那要么是数据被破坏了,要么是固件做了某种混淆。
我在一次分析某款IoT设备固件时,就遇到过这种情况。向量表里复位向量的值是0x08000144,最低位是0。我当时愣了一下,心想这不可能啊。后来仔细一看,原来是厂商在固件打包时做了地址对齐处理,把最低位给抹掉了。烧录器在写入时会自动补上。嗯,这种“坑”在量产固件里并不少见。
2.4 如何从固件中提取向量表
拿到一个陌生的固件,我通常会按以下步骤快速定位向量表:
- 确定文件格式:如果是Hex,直接看地址;如果是Bin,需要知道加载基址
- 读取前8个字节:前4字节是MSP,后4字节是复位向量
- 验证合理性:MSP值应该在RAM地址范围内,复位向量应该在Flash地址范围内且最低位为1
- 提取完整向量表:根据MCU型号,确定向量数量(通常为16个系统异常 + 外设中断数量)
举个例子,假设你有一个STM32F103的固件,基址0x08000000,前16个字节如下:
00 10 00 20 45 01 00 08 01 02 00 08 05 02 00 08
解析一下:
00 10 00 20→ 小端序 → 0x20001000(MSP,在SRAM范围内)45 01 00 08→ 小端序 → 0x08000145(复位向量,最低位为1,合法)01 02 00 08→ 0x08000201(NMI)05 02 00 08→ 0x08000205(HardFault)
2.5 常见MCU的Flash布局差异
不同厂商的MCU,Flash布局还是有些区别的。我整理了一个对比表,方便你快速查阅:
| MCU系列 | Flash起始地址 | 向量表大小 | 特殊说明 |
|---|---|---|---|
| STM32F0/F1/F3/F4 | 0x08000000 | 通常256字节(64向量) | 支持VTOR重定位 |
| NXP LPC17xx | 0x00000000 | 256字节 | 启动时从BootROM映射 |
| GD32系列 | 0x08000000 | 与STM32兼容 | 注意Flash分页大小不同 |
| AT32系列 | 0x08000000 | 256字节 | 向量表偏移需配置 |
| ESP32 | 0x400D0000(IRAM) | 不适用 | Xtensa架构,启动流程不同 |
你想想看,如果不知道这些差异,拿着STM32的经验去分析ESP32的固件,那肯定一头雾水。我刚开始接触ESP32时就犯过这个错——拿着Bin文件按Cortex-M的向量表去解析,结果全是乱码。后来才意识到,Xtensa架构的启动流程完全不一样。