4. 分区表条目解析:Type、SubType、Offset、Size 等字段含义
上一章我们把分区表的二进制结构拆开了,知道了每个条目固定 32 字节。但光知道结构还不够,你得明白每个字段到底在干什么。说白了,ESP32 的启动流程就是靠这些字段来“指路”的——哪个分区是 bootloader,哪个是 app,哪个存的是文件系统,全看 Type 和 SubType 怎么配。
我个人习惯把分区表条目看成一张“内存地图”。每个条目就是地图上的一个标注点,告诉你:这里有一块区域,它是什么用途,从哪里开始,有多大。今天我们就来逐个字段拆解,顺便聊聊我在实际项目中踩过的坑。
4.1 字段总览:32 字节里到底塞了什么?
先看一个标准的分区表条目结构。每个条目 32 字节,按小端序排列:
| 偏移 | 大小 | 字段名 | 说明 |
|---|---|---|---|
| 0x00 | 8 字节 | label | 分区名称,ASCII 字符串,不足补 0x00 |
| 0x08 | 4 字节 | type | 分区主类型 |
| 0x0C | 4 字节 | subtype | 分区子类型 |
| 0x10 | 4 字节 | offset | 分区在 Flash 中的起始地址 |
| 0x14 | 4 字节 | size | 分区大小(字节) |
| 0x18 | 4 字节 | flags | 标志位(加密、只读等) |
嗯,这里要注意:label 字段虽然占了 8 字节,但实际有效字符最多 7 个,因为最后一位要留给字符串结束符。我曾经在项目里写了个 8 字符的名字,结果分区表解析直接崩了——查了半天才发现是名字超长。
4.2 Type 字段:分区的“身份证”
Type 字段决定了这个分区属于哪一大类。ESP-IDF 预定义了几种标准类型:
- 0x00 (ESP_PARTITION_TYPE_APP):应用程序分区。存放可执行固件,启动时会从这里加载。
- 0x01 (ESP_PARTITION_TYPE_DATA):数据分区。存放文件系统、参数、日志等非代码数据。
- 0x10 (ESP_PARTITION_TYPE_OTA):OTA 分区。用于空中升级的临时存储区。
你可能会问:为什么 OTA 要单独一个类型?其实在 ESP32 的 OTA 机制里,Type 为 0x10 的分区会被启动加载器特殊对待——它不会直接执行,而是作为升级包的暂存区。我做过一个远程升级项目,一开始把 OTA 分区设成了 Data 类型,结果升级包写进去后启动加载器根本不认,折腾了两天才发现是 Type 配错了。
关键点:Type 字段的值由 ESP-IDF 的 partition_table.h 头文件定义。自定义类型也可以,但必须保证启动加载器能识别。否则,分区表校验会直接失败。
4.3 SubType 字段:同一类型下的“细分角色”
SubType 在 Type 的基础上进一步细化。同一个 Type 下可以有多个 SubType,它们的功能完全不同。
以 Type = 0x00 (APP) 为例:
| SubType 值 | 名称 | 说明 |
|---|---|---|
| 0x00 | factory | 出厂固件,首次启动默认加载 |
| 0x10 | ota_0 | OTA 槽位 0 |
| 0x11 | ota_1 | OTA 槽位 1 |
| 0x12 | ota_2 | OTA 槽位 2 |
| ... | ... | ... |
| 0x1F | ota_15 | OTA 槽位 15 |
再看 Type = 0x01 (DATA):
| SubType 值 | 名称 | 说明 |
|---|---|---|
| 0x00 | phy | PHY 初始化数据 |
| 0x01 | nvs | 非易失性存储(NVS) |
| 0x02 | coredump | 崩溃转储 |
| 0x03 | nvs_keys | NVS 加密密钥 |
| 0x04 | efuse | eFuse 模拟分区 |
| 0x05 | undefined | 未定义(可自定义) |
| 0x06 | esphttpd | HTTP 服务器文件 |
| 0x07 | fat | FAT 文件系统 |
| 0x08 | spiffs | SPIFFS 文件系统 |
这里有个容易踩的坑:同一个 Type 下,SubType 不能重复。比如你不能有两个 SubType 都是 ota_0 的分区。我曾经在调试 OTA 时,为了省事复制了一份分区表配置,结果两个 ota_0 分区冲突,启动加载器直接报错“duplicate partition”。
4.4 Offset 和 Size:分区的“物理坐标”
Offset 和 Size 决定了分区在 Flash 芯片上的实际位置。
- Offset:分区起始地址,必须是 4KB 对齐(即 0x1000 的整数倍)。这是 Flash 擦除的最小单位决定的。
- Size:分区大小,同样必须是 4KB 对齐。
为什么强制对齐?因为 ESP32 的 Flash 控制器在读写时,最小操作单位是一个扇区(4KB)。如果分区起始地址不对齐,写入时可能会破坏前一个分区的数据。我见过一个案例:有人把 offset 设成了 0x9100(不是 4KB 对齐),结果每次写入 NVS 数据,都会把相邻的 app 分区尾部数据给冲掉,导致固件校验失败。
警告:Offset 和 Size 必须满足 offset % 0x1000 == 0 且 size % 0x1000 == 0。否则分区表校验会失败,启动加载器会拒绝加载。
另外,分区之间不能重叠。也就是说,前一个分区的 offset + size 必须小于等于后一个分区的 offset。这个逻辑在分区表生成工具里会自动检查,但如果你手动修改二进制分区表,就很容易出问题。
4.5 Flags 字段:隐藏的“开关”
Flags 字段占 4 字节,目前只用了低 2 位:
- Bit 0 (ENCRYPTED):置 1 表示该分区需要 Flash 加密。启动加载器在读取时会自动解密。
- Bit 1 (READONLY):置 1 表示该分区为只读。运行时写入会返回错误。
其他位保留,必须为 0。我建议你在逆向分析时,优先检查 Flags 字段——如果某个分区标了 ENCRYPTED 但实际没加密,那很可能是固件被篡改过。
4.6 实战:手撕一个分区表条目
光说不练假把式。我们拿一个真实的分区表二进制数据来解析。假设从 Flash 偏移 0x8000 处读到了以下 32 字节:
6E 76 73 00 00 00 00 00 01 00 00 00 01 00 00 00
00 90 00 00 00 20 00 00 00 00 00 00 00 00 00 00
逐字段解析:
- label(0x00-0x07):
6E 76 73 00 00 00 00 00→ ASCII 解码为 "nvs"(后面补 0x00)。 - type(0x08-0x0B):
01 00 00 00→ 小端序 0x00000001,即 DATA 类型。 - subtype(0x0C-0x0F):
01 00 00 00→ 小端序 0x00000001,即 NVS 子类型。 - offset(0x10-0x13):
00 90 00 00→ 小端序 0x00009000,即偏移 0x9000。 - size(0x14-0x17):
00 20 00 00→ 小端序 0x00002000,即 8KB。 - flags(0x18-0x1B):
00 00 00 00→ 无特殊标志。
所以这个分区是:NVS 数据分区,起始于 0x9000,大小 8KB,未加密。
小技巧:在逆向分析时,你可以用 Python 的 struct.unpack('<IIII', data[8:24]) 快速解析 type、subtype、offset、size 四个字段。我写过一个脚本,批量扫描 Flash 镜像里的分区表条目,几分钟就能把整个分区布局还原出来。
4.7 分区表条目的“潜规则”
除了字段本身的含义,还有一些不成文的规则,我在项目里吃过不少亏:
- 第一个条目必须是 bootloader 分区(Type=0x00, SubType=0x00)。否则启动加载器找不到自己。
- 分区表本身必须放在 Flash 偏移 0x8000 处。这是硬件决定的,改不了。
- 所有分区的 offset 必须按升序排列。虽然工具会排序,但手动修改时容易乱。
- 最后一个条目之后必须紧跟结束标记(全 0xFF 的 32 字节)。否则分区表解析会认为条目未结束。
嗯,说到结束标记,我记得有一次在逆向一个第三方固件时,发现分区表最后没有结束标记,结果启动加载器一直往后读,读到了 Flash 末尾的空白区域,然后报了个莫名其妙的校验错误。后来我手动补上 32 个 0xFF,问题就解决了。
4.8 一张图看懂分区表条目结构
下面这张 SVG 图把整个条目结构可视化了一下,方便你对照理解:
4.9 小结
分区表条目说白了就是 32 字节的“元数据”,但每个字段都牵一发动全身。Type 和 SubType 决定了分区的“身份”,Offset 和 Size 决定了它的“位置”,Flags 则控制着“访问权限”。