← 返回目录课程:《IDA Pro分析STM32单片机固件实战课程》
02

02 STM32固件结构解析:启动文件、向量表与链接脚本逆向

形成固件二进制布局,识别中断向量表与复位处理入口
章节导航 难度:入门类型:排障定位
本页抓手

把STM32固件结构解析落到Bootloader升级链路的触发条件、可观察状态和复查材料上,先判断问题从哪里收窄。

判断线索
接口配置流程状态日志
复用位置
代码片段排障表检查清单架构图

查看现场信号与复查材料

  1. 核对Bootloader升级链路的输入条件、变化信号和结果材料
  2. 再把命令、配置、日志和状态结果对应到同一个操作结果
  3. 最后用启动成功率验证结果是否可复现

代码阅读线索

STM32固件结构解析涉及源码、函数、脚本或 API 时,应先确认代码对象、调用条件和结果信号是否对应。

本节用Bootloader升级链路的真实调用点说明代码变化会影响哪一个操作结果。

固件二进制布局:从启动文件到向量表定位

STM32固件结构解析把知识点接成体系01🎯固件二进制布局:从明确检查要点02⭐链接脚本逆向:从分明确检查要点03⭐STM32固件结构明确检查要点04Bootloader明确检查要点05对象边界先定义适用范围06触发条件确认起点与约束

STM32固件结构解析架构示意

Boot ROM
→
Bootloader
→
App分区
→
回滚记录

STM32固件结构解析要确认的对象和结果

在《IDA Pro分析STM32单片机固件实战课程》中,逆向分析的第一步并非直接反汇编,而是理解固件在 Flash 中的物理布局。STM32 的固件通常从 0x08000000 地址开始,其最前端的 4 字节存放的是栈顶指针(MSP),紧接着的 4 字节才是复位向量(Reset_Handler)。这 8 字节构成了中断向量表(Interrupt Vector Table, IVT)的头部,是整个固件执行的起点。若无法准确识别向量表,后续的代码定位将失去基准

实操步骤

在 IDA Pro 中加载固件后,首先将加载基址设为 0x08000000,并手动检查前 8 字节:0x08000000 处应为栈顶地址(通常指向 SRAM 末尾),0x08000004 处应为复位处理函数地址。若这两个值明显异常(如全 0 或指向非代码区),则基址设置或固件加载方式有误

向量表不仅包含复位向量,还按优先级顺序排列了所有异常和中断入口。STM32 的向量表偏移寄存器(VTOR)允许运行时重定位向量表,但在固件镜像中,它始终固定在起始位置。实战中,我们常通过 IDA 的“Create Vector Table”插件或手动解析前 16 个异常向量来验证布局是否正确。若向量表偏移被修改(例如在 Bootloader 中),则需通过交叉引用定位 VTOR 的赋值指令

链接脚本逆向:从分散加载到内存映射还原

固件的内存布局由链接脚本(Linker Script)控制,它定义了代码段(.text)、数据段(.data)、BSS 段(.bss)以及堆栈的起始地址和大小。逆向工程师通过分析固件中的段符号和初始化代码,可以反推出链接脚本的配置。例如,__etext 符号标记了只读数据的结束位置,而 _sdata 和 _edata 则定义了已初始化数据的搬运范围

在 IDA Pro 中,我们通过定位 Reset_Handler 中的循环搬运代码来还原这些段。典型的搬运代码会从 __etext 复制数据到 _sdata,直到 _edata。若固件包含多个分区(如 Bootloader 和 App),则链接脚本会定义不同的加载域(LMA)和运行域(VMA)。例如,Bootloader 的 VMA 可能位于 0x08000000,而 App 的 VMA 则从 0x08020000 开始

真实项目场景

某工业控制设备固件升级后无法启动。通过 IDA Pro 加载固件,发现 0x08000004 处的复位向量指向了 0x0801FFFC,这是一个非代码区域。进一步分析链接脚本发现,App 分区的起始地址被错误配置为 0x08020000,但向量表仍停留在 0x08000000,导致跳转后执行了无效指令。修正链接脚本中的 FLASH_ORIGIN 定义后,问题解决

链接脚本逆向的核心产出是一张内存映射表,它标明了每个段的起始地址、大小和属性。这张表是后续分析代码流、定位全局变量和识别堆栈溢出的基础

STM32固件结构解析的技术资产与验证方式

本节的核心技术资产是一套可复用的向量表解析样例和分区校验脚本。以下是一个基于 Python 的向量表解析片段,它从固件二进制中提取前 16 个异常向量,并与标准 STM32 向量表进行比对:

import struct
def parse_vector_table(路径, base_addr=0x08000000):
    with open(路径, 'rb') as f:
        data = f.read()
    # 读取前 16 个向量(每个 4 字节)
    vectors = []
    for i in range(16):
        offset = i * 4
        addr = struct.unpack('<I', data[offset:offset+4])[0]
        vectors.append((i, hex(addr)))
    # 验证复位向量是否在 Flash 范围内
    reset_addr = vectors[1][1]
    if not (0x08000000 <= int(reset_addr, 16) <= 0x081FFFFF):
        print(f"[WARNING] 复位向量 {reset_addr} 超出 Flash 范围")
    return vectors
# 输出示例
vectors = parse_vector_table("firmware.bin")
for idx, addr in vectors:
    print(f"Vector {idx}: {addr}")

这段配置属于“问题代码/原始配置”,核对键名、默认值、取值范围和修改后影响的行为。

该脚本验证了向量表头部是否合理,是固件加载后的第一道检查点。若复位向量指向 SRAM 或外设地址空间,则说明固件可能被加密、压缩或加载地址错误

以下是一张分区校验参数表,用于判断固件是否包含 Bootloader 和 App 双分区:

STM32固件结构解析 相关检查
变量对照组观察结果结论
向量表位置0x08000000 和 0x08020000 各有一…仅 0x08000000 有向量表Bootloader 向量表在 0x08000000,…
复位向量目标指向 0x0800xxxx 或 0x0802xxxx指向 0x0800xxxx 且后续无跳转复位向量 0x08000123 在 Bootloade…
Flash 大小固件大小超过 128KB 且存在明显分区边界固件大小小于 64KB固件 256KB,前 64KB 为 Bootloader
关键判断

若固件中仅有一个向量表且复位向量直接指向用户代码,则通常为单分区设计。若存在两个向量表且复位向量指向 Bootloader,则必须检查跳转指令(如 BX R0 或 LDR PC, [R0, #4])是否将控制权移交给了 App 分区

项目下一步

项目收束要把STM32固件结构解析落到下一步可执行材料上:入口在哪里、输出是什么、STM32固件结构解析能否证明当前实现已经达到目标。如果继续扩展,应先判断固件结构解析和启动文件哪一个会影响结果,再决定是补配置、补测试,还是补一份更清晰的启动文件。这样的尾部不重复资产清单,而是让后续协作能从具体材料判断项目能否继续推进。保留一条最小证据线:STM32固件结构解析说明对象,项目成果说明约束,STM32固件结构解析说明状态,最后用启动文件判断能否继续推进。第02节留下的风险提示是源码结构不要脱离项目成果单独判断,否则容易把现象误当成原因。