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

逐字段解析:

  1. label(0x00-0x07):6E 76 73 00 00 00 00 00 → ASCII 解码为 "nvs"(后面补 0x00)。
  2. type(0x08-0x0B):01 00 00 00 → 小端序 0x00000001,即 DATA 类型。
  3. subtype(0x0C-0x0F):01 00 00 00 → 小端序 0x00000001,即 NVS 子类型。
  4. offset(0x10-0x13):00 90 00 00 → 小端序 0x00009000,即偏移 0x9000。
  5. size(0x14-0x17):00 20 00 00 → 小端序 0x00002000,即 8KB。
  6. 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 图把整个条目结构可视化了一下,方便你对照理解:

分区表条目结构(32 字节) 32 字节分区表条目 label(8 字节) type(4 字节) subtype(4 字节) offset(4 字节) size(4 字节) flags(4 字节) 0x00 0x08 0x0C 0x10 0x14 0x18 0x1C 字段说明: • label:分区名称,ASCII 字符串,不足 8 字节补 0x00 • type:主类型(0x00=APP, 0x01=DATA, 0x10=OTA) • subtype:子类型(如 factory、ota_0、nvs、spiffs 等) • offset:分区起始地址(必须 4KB 对齐) • size:分区大小(必须 4KB 对齐) • flags:标志位(bit0=加密, bit1=只读)

4.9 小结

分区表条目说白了就是 32 字节的“元数据”,但每个字段都牵一发动全身。Type 和 SubType 决定了分区的“身份”,Offset 和 Size 决定了它的“位置”,Flags 则控制着“访问权限”。