固件镜像结构解析: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字节)
└── 起始标记
我的习惯:做逆向分析时,我更喜欢用Hex文件。因为它自带地址信息,能直接告诉我代码段、数据段分别位于Flash的什么位置。Bin文件虽然小,但你必须额外知道它的加载地址,否则反汇编出来的地址全是错的。

2.2 固件在Flash中的布局

MCU上电后,Flash里的内容就是它的整个世界。不同MCU厂商的Flash布局略有差异,但大体上都遵循一个通用模式。我以最常见的ARM Cortex-M系列为例,给你画一张典型的Flash布局图:

典型ARM Cortex-M Flash布局 向量表(Vector Table) 0x08000000 - 0x0800013F(256字节,64个向量) 主栈指针(MSP) | 复位向量 | NMI | HardFault | ... | 外部中断 启动代码(Startup) SystemInit | 时钟配置 | 数据段搬运 | BSS清零 | 跳转main 代码段(.text) 应用程序指令 | 中断服务函数 | 库函数 只读数据(.rodata) 数据段(.data) 已初始化全局变量(初始值存储在Flash,运行时拷贝到RAM) ⚠ 不同MCU的Flash起始地址不同 STM32: 0x08000000 | NXP: 0x00000000 | GD32: 0x08000000 🔑 逆向分析关键 找到向量表 = 找到程序入口

这张图你一定要记住。每次拿到一个固件,我第一件事就是定位向量表,然后顺着它找到复位向量,再找到main函数。这是逆向分析的“破冰”步骤。

注意:有些MCU支持“向量表重定位”,比如STM32可以通过设置VTOR寄存器把向量表搬到RAM或其他Flash区域。如果你发现固件开头不是标准的向量表内容,别慌——先检查一下有没有VTOR配置代码。

2.3 向量表与入口地址

向量表,是MCU启动的“第一张地图”。Cortex-M系列上电后,硬件会自动从Flash起始地址读取两个关键值:

  1. 地址0x00000000(或0x08000000): 读取主栈指针(MSP)的初始值
  2. 地址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 如何从固件中提取向量表

拿到一个陌生的固件,我通常会按以下步骤快速定位向量表:

  1. 确定文件格式:如果是Hex,直接看地址;如果是Bin,需要知道加载基址
  2. 读取前8个字节:前4字节是MSP,后4字节是复位向量
  3. 验证合理性:MSP值应该在RAM地址范围内,复位向量应该在Flash地址范围内且最低位为1
  4. 提取完整向量表:根据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)
我的小工具:我习惯用Python写一个简单的向量表解析脚本,几行代码就能把整个向量表dump出来。配合Capstone反汇编引擎,可以直接从复位向量开始反汇编,快速定位到main函数。这个脚本我后面会分享到课程配套资源里。

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架构的启动流程完全不一样。

避坑指南:我曾经在一个项目中,客户给了一个固件说是STM32F4的,但我怎么都找不到正确的向量表。折腾了半天,才发现那个固件其实是GD32F4的——两者虽然Pin-to-Pin兼容,但Flash的分页大小不同,导致固件在Flash里的布局有细微差异。所以,拿到固件后,先确认MCU型号,再动手分析。