Android 动态分区 (super) 深度解析:原理、结构与实战

Android 动态分区 (super) 深度解析:原理、结构与实战

一、引言:从传统分区到动态分区

1.1 传统分区方案的困境

在 Android 早期版本中,系统固件采用静态分区布局。以 Android 8/9 为例,system、vendor、product、odm 等分区在编译时就被固定了大小:

bash 复制代码
# 典型的 Android 9 分区表
Number  Start       End         Size        Name
 1      2048        6143        4096        boot_a
 2      6144        10239       4096        dtbo_a
 3      10240       ...         ...         system_a
 4      ...         ...         ...         vendor_a
 5      ...         ...         ...         product_a

这种方案的痛点非常明显:

  • 空间浪费:每个分区必须预留足够的空间以应对未来的 OTA 升级,但实际使用率可能只有 60%-70%
  • 扩展困难:如果需要增大某个分区,必须重新调整整个 GPT 分区表,这对已出货的设备几乎不可能
  • 灵活性差:不同厂商、不同机型的空间分配需求千差万别,静态方案无法适配

比如,某款设备 product 分区利用率仅 40%,而 vendor 分区却因为合入了大量驱动而空间不足------静态分区对此无能为力。

2.2 动态分区方案的诞生

Android 10(API 29)引入了动态分区(Dynamic Partitions)机制,核心思路是在一个物理 super 分区内部管理多个逻辑分区。这套方案带来了以下革命性变化:

特性 传统分区 动态分区
分区大小调整 需修改 GPT 运行时调整
OTA 升级 需预留大量空间 按需分配
分区数量 受 GPT 限制 灵活创建
空间利用率 低 高

动态分区的核心理念是:将物理存储空间池化,逻辑分区按需从中分配 block。这类似于文件系统的 extent 机制,或者虚拟内存的分页机制。


二、Super 分区整体架构

2.1 物理视角 vs 逻辑视角

在动态分区方案中,我们需要区分两个层级:
#mermaid-svg-iDb0WRIW09TTuj98{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iDb0WRIW09TTuj98 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iDb0WRIW09TTuj98 .error-icon{fill:#552222;}#mermaid-svg-iDb0WRIW09TTuj98 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iDb0WRIW09TTuj98 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 .marker.cross{stroke:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iDb0WRIW09TTuj98 p{margin:0;}#mermaid-svg-iDb0WRIW09TTuj98 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster-label text{fill:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster-label span{color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster-label span p{background-color:transparent;}#mermaid-svg-iDb0WRIW09TTuj98 .label text,#mermaid-svg-iDb0WRIW09TTuj98 span{fill:#333;color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .node rect,#mermaid-svg-iDb0WRIW09TTuj98 .node circle,#mermaid-svg-iDb0WRIW09TTuj98 .node ellipse,#mermaid-svg-iDb0WRIW09TTuj98 .node polygon,#mermaid-svg-iDb0WRIW09TTuj98 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .rough-node .label text,#mermaid-svg-iDb0WRIW09TTuj98 .node .label text,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape .label,#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape .label{text-anchor:middle;}#mermaid-svg-iDb0WRIW09TTuj98 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .rough-node .label,#mermaid-svg-iDb0WRIW09TTuj98 .node .label,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape .label,#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape .label{text-align:center;}#mermaid-svg-iDb0WRIW09TTuj98 .node.clickable{cursor:pointer;}#mermaid-svg-iDb0WRIW09TTuj98 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 .arrowheadPath{fill:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iDb0WRIW09TTuj98 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iDb0WRIW09TTuj98 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iDb0WRIW09TTuj98 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iDb0WRIW09TTuj98 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iDb0WRIW09TTuj98 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iDb0WRIW09TTuj98 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster text{fill:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster span{color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iDb0WRIW09TTuj98 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iDb0WRIW09TTuj98 rect.text{fill:none;stroke-width:0;}#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape p,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape .label rect,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iDb0WRIW09TTuj98 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iDb0WRIW09TTuj98 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iDb0WRIW09TTuj98 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GPT Partition Table
super 物理分区
逻辑分区层
system
vendor
product
引导分区
boot_a
boot_b

关键点 :从 Linux 内核视角,super 是一个物理分区(通过 GPT 描述);而 system、vendor 等逻辑分区通过 device-mapper 映射为虚拟块设备。用户空间的代码几乎无感知。

2.2 整体数据布局

super 分区的二进制布局如下:

复制代码
Offset                  Contents
───────────────────────────────────────────────────────
0x0000                  GPT Header + GPT Partition Table
0x1000                  super partition header (LP_META_HEADER)
0x1000 + 4096           LP Table #1 (主分区表)
                         ├── LP Table Header
                         ├── Partition Entries (N个)
                         ├── Group Descriptors (M个)
                         └── Block Mapping Table (K个extent)
0x1000 + 4096 * 2       LP Table #2 (备用分区表,结构同上)
0x1000 + 4096 * N       LP Table #3 (仅 OTA 使用)
                        ...
                        [逻辑分区数据区域]
                        ├── system 分区数据
                        ├── vendor 分区数据
                        ├── product 分区数据
                        └── ...

说明:LP Table 默认每 4096 字节(一个 sector 对齐)放置一份,最多支持 4 份拷贝(包括主/备和 OTA 镜像),通过 metadata_slot 来管理多副本。


三、GPT 与 Super 分区的底层关系

3.1 GPT 分区表中的 super 条目

在理解动态分区之前,有必要先回顾一下 GPT(GUID Partition Table)的结构,因为 super 分区本身就是一个 GPT 分区。

GPT 的布局如下:

复制代码
LBA 0:  Protective MBR (防止旧系统误识别)
LBA 1:  GPT Header
  - Signature: "EFI PART"
  - MyLBA: 1
  - Partition Entry Start: LBA 2
  - Number of Partition Entries: 128
  - Size of Partition Entry: 128 bytes
LBA 2~33: GPT Partition Entries (128个, 每个128字节)
  Entry 0: boot_a
  Entry 1: boot_b
  Entry 2: super     ← 我们的主角
  Entry 3: cache
  ...
LBA 34~:  数据区域

#mermaid-svg-cu1lxqFCx09IEQi8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cu1lxqFCx09IEQi8 .error-icon{fill:#552222;}#mermaid-svg-cu1lxqFCx09IEQi8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cu1lxqFCx09IEQi8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 .marker.cross{stroke:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cu1lxqFCx09IEQi8 p{margin:0;}#mermaid-svg-cu1lxqFCx09IEQi8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster-label text{fill:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster-label span{color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster-label span p{background-color:transparent;}#mermaid-svg-cu1lxqFCx09IEQi8 .label text,#mermaid-svg-cu1lxqFCx09IEQi8 span{fill:#333;color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .node rect,#mermaid-svg-cu1lxqFCx09IEQi8 .node circle,#mermaid-svg-cu1lxqFCx09IEQi8 .node ellipse,#mermaid-svg-cu1lxqFCx09IEQi8 .node polygon,#mermaid-svg-cu1lxqFCx09IEQi8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .rough-node .label text,#mermaid-svg-cu1lxqFCx09IEQi8 .node .label text,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape .label,#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cu1lxqFCx09IEQi8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .rough-node .label,#mermaid-svg-cu1lxqFCx09IEQi8 .node .label,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape .label,#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape .label{text-align:center;}#mermaid-svg-cu1lxqFCx09IEQi8 .node.clickable{cursor:pointer;}#mermaid-svg-cu1lxqFCx09IEQi8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 .arrowheadPath{fill:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cu1lxqFCx09IEQi8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cu1lxqFCx09IEQi8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cu1lxqFCx09IEQi8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cu1lxqFCx09IEQi8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cu1lxqFCx09IEQi8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster text{fill:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster span{color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cu1lxqFCx09IEQi8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape p,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape .label rect,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cu1lxqFCx09IEQi8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cu1lxqFCx09IEQi8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cu1lxqFCx09IEQi8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LBA 0

Protective MBR
LBA 1

GPT Header
LBA 2~33

GPT Partition Entries
boot_a
boot_b
super ← 动态分区
cache
LBA 34~ 数据区域
super 分区数据

super 分区的 GPT 分区表项中,其分区类型 GUID 为:

复制代码
GUID: A19B880C-3C6D-48BC-8B1B-E9EBB02D8A51

这个 GUID 告诉引导加载程序(bootloader)和内核:这是一个动态 super 分区,内部包含了需要进一步解析的逻辑分区元数据。

GPT 条目中 super 分区的起始 LBA 和大小决定了整个动态分区体系的物理边界:

c 复制代码
// GPT Partition Entry for super (伪代码)
struct GptEntry {
    uint8_t  type_guid[16];   // = A19B880C-... (动态分区专用 GUID)
    uint8_t  unique_guid[16]; // 设备唯一标识
    uint64_t starting_lba;    // super 分区的起始位置
    uint64_t ending_lba;      // super 分区的结束位置
    uint64_t attributes;      // 属性位
    uint16_t name[36];        // "super"
};

值得注意的是,GPT 中的 super 分区大小通常不等于所有逻辑分区大小之和。这是因为:

  1. 元数据区域:LP table 本身占用几 KB 到几十 KB
  2. 预留空间:为了支持 OTA 升级时的空间调整,通常会预留 10%-20% 的空白空间
  3. 对齐要求:逻辑分区以 1MB 对齐,导致不可避免的碎片

3.2 Bootloader 如何找到 super 分区

Bootloader(如 U-Boot、Little Kernel、ABL)启动时需要加载 system 分区来启动 Android。在动态分区方案中,bootloader 必须通过以下步骤才能访问逻辑分区:

复制代码
Step 1: 扫描 GPT 找到 type_guid 匹配 super 的分区
Step 2: 从 super 分区的起始位置 + 4KB 偏移读取 LP 元数据
Step 3: 解析 LP 元数据,得到 system_a/vendor_a 等逻辑分区的 extent
Step 4: 配置 device-mapper 创建 dm-linear 设备
Step 5: 通过 dm 设备读取 system 分区的文件系统数据

这个流程相比传统方案(直接读取 GPT 分区)增加了复杂度,但换来了极大的灵活性。


四、数据结构深度剖析

动态分区的所有元数据定义在 AOSP 源码中的 system/liblp/ 目录下。核心头文件是 include/liblp/partition_opts.h 和 include/liblp/metadata_format.h。以下逐一分析。

4.1 LP_META_HEADER (元数据头部)

每份 LP Table 以 LP_META_HEADER 开头:

c 复制代码
// system/liblp/include/liblp/metadata_format.h

#define LP_META_HEADER_MAGIC       0x414C5030  // "LP0" in ASCII
#define LP_META_HEADER_MAJOR_VER   10
#define LP_META_HEADER_MINOR_VER   0

struct LpMetadataHeaderV1_0 {
    uint32_t magic;              // 魔数: 0x414C5030 ("LP0")
    uint32_t major_version;      // 主版本号: 10
    uint32_t minor_version;      // 次版本号: 0
    uint32_t linear_extent_count;   // 线性 extent 总数
    uint32_t extent_count;          // extent 项数(含非线性的预留)
    uint32_t logical_block_size;    // 逻辑块大小(通常 4096)
    uint32_t partition_count;       // 逻辑分区数量
    uint32_t reserved[7];
};
// 对齐到 128 字节

字段解读:

字段 说明 典型值
magic 魔数,用于标识有效头部 0x414C5030
major_version 主版本号 10+
linear_extent_count 所有分区的线性 extent 总数 N
logical_block_size 逻辑块大小,与存储块对齐 4096
partition_count 该 super 中管理的逻辑分区数量 3~8

版本演进:V10 是 Android 10 引入的第一个版本。后续 Android 11+ 扩展了 header 结构(增加了快照、COW 等信息),但核心字段保持不变。

3.3 分区表项 (LpMetadataPartition)

紧接着 header 的是 partition_count 个分区表项:

c 复制代码
struct LpMetadataPartition {
    uint32_t name_size;          // 分区名称长度(含 '\0' 终止符)
    uint32_t attributes;         // 属性标志位
    uint32_t first_extent_index; // 第一个 extent 在 extent 表中的索引
    uint32_t num_extents;        // 该分区拥有的 extent 数量
    uint32_t group_index;        // 所属 group 索引
    uint32_t reserved[2];
    char name[];                 // 变长字段:分区名称
};
// 每项最小 24 字节 + 名称长度

attributes 标志位:

c 复制代码
// 属性常量定义
enum {
    LP_PARTITION_ATTR_NONE           = 0,
    LP_PARTITION_ATTR_READONLY       = (1 << 0), // 只读分区
    LP_PARTITION_ATTR_SLOT_SUFFIXED  = (1 << 1), // 需要 A/B slot 后缀
    LP_PARTITION_ATTR_UPDATED        = (1 << 2), // OTA 更新标记
    LP_PARTITION_ATTR_DISABLED       = (1 << 3), // 禁用标记
};

典型的分区表示例:

c 复制代码
// 假设 device 分区配置
Partition Entry #1:
  name_size      = 7       // "system" + '\0'
  attributes     = 0x03    // READONLY | SLOT_SUFFIXED
  first_extent_index = 0
  num_extents    = 3
  group_index    = 0       // "default" group
  name           = "system_a"

Partition Entry #2:
  name_size      = 7
  attributes     = 0x03
  first_extent_index = 3
  num_extents    = 2
  group_index    = 0
  name           = "vendor_a"

3.4 线性 Extent (LpMetadataExtent)

Extent 是动态分区最核心的设计------它描述了一段连续的物理 block 范围:

c 复制代码
struct LpMetadataExtent {
    uint64_t num_sectors;      // 该 extent 包含的扇区数(1 sector = 512B)
    uint64_t sector_offset;    // 在 super 分区内的起始扇区偏移
    uint64_t padding[0];
};

Extent 与分区的映射关系:

复制代码
Partition "system_a" (3 extents):
  Extent #0: sector_offset=10000, num_sectors=20000
  Extent #1: sector_offset=50000, num_sectors=15000
  Extent #2: sector_offset=80000, num_sectors=5000

内存中的虚拟块设备:
  [0 ~ 19999]     → super 分区 [sector 10000 ~ 29999]
  [20000 ~ 34999] → super 分区 [sector 50000 ~ 64999]
  [35000 ~ 39999] → super 分区 [sector 80000 ~ 84999]

这种设计的关键优势:

  • 碎片整理:物理上分散的空间可以被拼接成连续的虚拟空间
  • 空间复用:被删除分区的 block 可通过修改 extent 重新分配给其他分区
  • 极小元数据开销:每个 extent 仅 16 字节

4.5 Group Descriptor (组描述符)

分区可以分组管理,组描述符定义了组的属性:

c 复制代码
struct LpMetadataPartitionGroup {
    uint32_t name_size;          // 组名称长度
    uint32_t flags;              // 标志位
    uint64_t maximum_size;       // 该组在 super 中可用的最大字节数
    char name[];                 // 变长字段:组名称
};
// 每项最小 16 字节 + 名称长度

常见的组:

组名 用途 典型最大大小
default A/B 分区组 super 分区大小
google_dynamic_updatable Google 服务可更新分区 2GB
vendor_group 厂商自定义组 1GB

组的实际作用 :在 OTA 升级时,组内的空间可以作为整体进行管理。例如,A/B 切换时,default 组内的所有分区一起切换到另一个 slot。

4.6 元数据的多重备份与 CRC 保护

动态分区的元数据采用了多重备份 + CRC 校验的容错设计。这是理解 super 分区可靠性的关键。

整个 super 分区的起始区域 layout 如下:
#mermaid-svg-DRdJcCDBYTLYPGIV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DRdJcCDBYTLYPGIV .error-icon{fill:#552222;}#mermaid-svg-DRdJcCDBYTLYPGIV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DRdJcCDBYTLYPGIV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV .marker.cross{stroke:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DRdJcCDBYTLYPGIV p{margin:0;}#mermaid-svg-DRdJcCDBYTLYPGIV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster-label text{fill:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster-label span{color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster-label span p{background-color:transparent;}#mermaid-svg-DRdJcCDBYTLYPGIV .label text,#mermaid-svg-DRdJcCDBYTLYPGIV span{fill:#333;color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .node rect,#mermaid-svg-DRdJcCDBYTLYPGIV .node circle,#mermaid-svg-DRdJcCDBYTLYPGIV .node ellipse,#mermaid-svg-DRdJcCDBYTLYPGIV .node polygon,#mermaid-svg-DRdJcCDBYTLYPGIV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .rough-node .label text,#mermaid-svg-DRdJcCDBYTLYPGIV .node .label text,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape .label,#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape .label{text-anchor:middle;}#mermaid-svg-DRdJcCDBYTLYPGIV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .rough-node .label,#mermaid-svg-DRdJcCDBYTLYPGIV .node .label,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape .label,#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape .label{text-align:center;}#mermaid-svg-DRdJcCDBYTLYPGIV .node.clickable{cursor:pointer;}#mermaid-svg-DRdJcCDBYTLYPGIV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV .arrowheadPath{fill:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DRdJcCDBYTLYPGIV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DRdJcCDBYTLYPGIV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DRdJcCDBYTLYPGIV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DRdJcCDBYTLYPGIV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DRdJcCDBYTLYPGIV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster text{fill:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster span{color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DRdJcCDBYTLYPGIV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV rect.text{fill:none;stroke-width:0;}#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape p,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape .label rect,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DRdJcCDBYTLYPGIV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DRdJcCDBYTLYPGIV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DRdJcCDBYTLYPGIV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} super 分区
元数据区域
逻辑分区数据区域
Slot 0

主副本
Slot 1

备用副本
Slot 2

OTA 专用
Slot 3

OTA 备用
system
vendor
product

多重备份的作用:

Slot 用途 说明
Slot 0 主副本 当前系统使用的元数据
Slot 1 备用副本 主副本损坏时自动切换
Slot 2~3 OTA 专用 升级过程中暂存新元数据

CRC 校验机制:

c 复制代码
// 校验过程 (伪代码)
bool VerifyMetadata(LpMetadataHeader* header, uint8_t* data, size_t size) {
    // 1. 验证魔数
    if (header->magic != LP_META_HEADER_MAGIC) return false;

    // 2. 计算 CRC32
    uint32_t expected_crc = header->metadata_crc32;
    uint32_t actual_crc = crc32(data + sizeof(LpMetadataHeader),
                                 size - sizeof(LpMetadataHeader));

    // 3. 比较
    return expected_crc == actual_crc;
}

当 liblp 读取元数据时,它会先尝试 Slot 0,如果 CRC 校验失败则自动回退到 Slot 1:

cpp 复制代码
std::unique_ptr<LpMetadata> ReadMetadataWithRetry(int fd) {
    // 先尝试 slot 0
    for (int slot = 0; slot < kMaxMetadataSlots; slot++) {
        auto metadata = ReadMetadata(fd, slot);
        if (metadata) {
            return metadata;  // 校验通过,直接返回
        }
        // 校验失败,记录日志并尝试下一个 slot
        LWARN("Metadata slot %d is corrupt, trying next", slot);
    }
    // 所有 slot 都损坏
    LERROR("All metadata slots are corrupt!");
    return nullptr;
}

这种设计确保了在意外断电或刷写中断的情况下,设备仍然可以恢复。


五、Device Mapper 机制深度分析

5.1 dm-linear 映射原理

动态分区的核心机制依赖于 Linux 内核的 Device Mapper 框架。每个逻辑分区对应一个 dm-linear 设备,它本质上是一个"虚拟块设备",将 I/O 请求重定向到 super 分区的特定区域。
渲染错误: Mermaid 渲染失败: Lexical error on line 2. Unrecognized text. ...0
dm-linear 虚拟设备] DM --> Remap[I -----------------------^

使用 dmsetup table 可以看到实际的映射关系:

bash 复制代码
# 一个典型的 dm-linear 映射表
adb shell dmsetup table
# 输出格式: <设备名> <起始扇区> <扇区数> linear <源设备> <源偏移>
system_a: 0 2048000 linear 259:3 2048000
vendor_a: 0 1024000 linear 259:3 4096000
product_a: 0 1024000 linear 259:3 5120000

解释:

  • 0 2048000:虚拟设备的起始扇区为 0,长度 2048000 扇区(约 1GB)
  • linear:使用线性映射目标类型
  • 259:3:源设备(super 分区)的设备号
  • 2048000:在源设备中的偏移量

5.2 Device Mapper 在内核中的实现

Device Mapper 是 Linux 内核中的一个框架,通过 dm 模块实现。dm-linear 目标的代码在 drivers/md/dm-linear.c 中:

c 复制代码
// drivers/md/dm-linear.c (内核源码)
static int linear_map(struct dm_target *ti, struct bio *bio) {
    struct linear_c *lc = ti->private;

    // 关键操作:将 bio 的目标扇区重新映射
    // 从虚拟设备的扇区 → 物理设备的扇区
    bio->bi_iter.bi_sector += lc->start;

    // 将 bio 提交到真实设备
    return DM_MAPIO_REMAPPED;
}

当内核收到对 /dev/dm-0 的读写请求时:

  1. VFS 层:文件系统层将 I/O 发送到块设备
  2. dm 核心:根据映射表找到对应的 dm-linear 目标
  3. linear_map:将虚拟扇区号加上偏移量,转换为物理扇区号
  4. 提交 bio:将重新映射后的 bio 提交到 super 分区

这个过程完全在内核中完成,零拷贝、零上下文切换,性能开销极低(仅增加一次加法运算)。

5.3 内核中的元数据解析

在高通平台中,ABL(Apps Bootloader)或 UFS 驱动会在内核启动前完成逻辑分区的创建。但在某些架构中,内核需要自身解析 LP 元数据:

c 复制代码
// 内核中的 LP 元数据解析 (伪代码)
int dm_init_dynamic_partitions(struct block_device *super_bdev) {
    // 1. 读取 LP 元数据
    struct lp_metadata *meta = read_lp_metadata(super_bdev, 0);
    if (!meta) return -EINVAL;

    // 2. 遍历所有分区
    for (int i = 0; i < meta->header.partition_count; i++) {
        struct lp_partition *part = &meta->partitions[i];

        // 3. 为每个分区创建 dm-linear 设备
        struct dm_ioctl *dm = create_dm_device(part->name);
        for (int j = 0; j < part->num_extents; j++) {
            struct lp_extent *ext = &meta->extents[part->first_extent_index + j];
            add_dm_table(dm, ext->num_sectors * 512,
                         super_bdev->bd_dev, ext->sector_offset * 512);
        }
        load_dm_device(dm);
    }

    return 0;
}

六、lpmake:从零构建 Super 镜像

6.1 lpmake 命令详解

lpmake 是构建 super 镜像的核心工具,源码在 system/liblp/lpmake.cpp。编译系统和 OTA 工具链都依赖它来生成最终的 super.img。

bash 复制代码
# lpmake 基本用法
lpmake \
    --device-size 4294967296 \          # super 分区总大小 (4GB)
    --metadata-size 65536 \             # 元数据区域大小 (64KB)
    --metadata-slots 2 \                # 元数据 slot 数量
    --super-name super \                # super 分区名称
    --partition system_a:readonly:1G:default \
    --partition vendor_a:readonly:512M:default \
    --partition product_a:readonly:512M:default \
    --image system_a=out/system.img \
    --image vendor_a=out/vendor.img \
    --image product_a=out/product.img \
    --sparse \                          # 输出稀疏格式
    --output super.img

参数详解:

参数 说明 示例值
--device-size super 物理分区的大小,必须与 GPT 一致 4294967296
--metadata-size 为元数据分配的字节数 65536
--metadata-slots 元数据副本数 2 或 3
--partition 定义逻辑分区 system_a:readonly:1G:default
--image 指定分区镜像文件路径 system_a=out/system.img
--sparse 输出 Android Sparse 格式 默认开启
--alignment 分区对齐大小 4096 (4KB)

6.2 lpmake 内部工作流

复制代码
lpmake 执行流程:
1. 解析命令行参数
   ├── 收集所有分区定义
   ├── 收集所有镜像文件
   └── 验证参数合法性

2. 分配空间 (Space Allocator)
   ├── 按传入顺序排列分区
   ├── 按 alignment 对齐
   ├── 将 extent 分配到 super 的空闲区域
   └── 确保所有分区在 size 约束内

3. 生成 LP 元数据
   ├── 填充 LP_META_HEADER
   ├── 写入所有 Partition Entries
   ├── 写入所有 Extent Entries
   ├── 写入 Group Descriptors
   ├── 计算并写入 CRC32
   └── 将元数据复制到多个 slot

4. 写入镜像数据
   ├── 创建输出文件
   ├── 写入 GPT 头部 (可选)
   ├── 写入 LP 元数据
   └── 按 extent 位置写入各分区镜像数据

6.3 空间分配算法

lpmake 的空间分配是该工具最值得关注的技术细节。它的核心算法如下:

cpp 复制代码
// 简化的空间分配器
class SpaceAllocator {
public:
    // 初始空闲区间为整个 super 分区(除去元数据头部)
    SpaceAllocator(uint64_t super_size, uint64_t metadata_size) {
        free_regions_.push_back({
            .start = metadata_size,    // 元数据之后
            .end   = super_size - 1    // 到 super 末尾
        });
    }

    // 分配一块连续空间
    bool Allocate(uint64_t size, uint32_t alignment,
                  uint64_t *out_start, uint64_t *out_end) {
        for (auto it = free_regions_.begin(); it != free_regions_.end(); ++it) {
            uint64_t aligned_start = AlignUp(it->start, alignment);
            uint64_t end = aligned_start + size - 1;

            if (end <= it->end) {
                // 找到合适的空闲区间
                *out_start = aligned_start;
                *out_end = end;

                // 更新空闲区间
                it->start = AlignUp(end + 1, alignment);
                if (it->start >= it->end) {
                    free_regions_.erase(it);
                }
                return true;
            }
        }
        return false;  // 空间不足
    }

private:
    struct Region { uint64_t start; uint64_t end; };
    std::list<Region> free_regions_;
};

这个算法的特点:

  • 首次适应(First-Fit):按顺序查找第一个能容纳分区的空闲区域
  • 对齐约束:每个 extent 都按 logical_block_size 对齐
  • 无碎片整理:当前实现不会对已分配空间进行碎片整理

6.4 稀疏镜像格式 (Android Sparse)

lpmake 默认输出 Android Sparse 格式。这种格式可以显著减少 super 镜像的大小:
#mermaid-svg-j04ZuvGEAz6voErL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-j04ZuvGEAz6voErL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-j04ZuvGEAz6voErL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-j04ZuvGEAz6voErL .error-icon{fill:#552222;}#mermaid-svg-j04ZuvGEAz6voErL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-j04ZuvGEAz6voErL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-j04ZuvGEAz6voErL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-j04ZuvGEAz6voErL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-j04ZuvGEAz6voErL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-j04ZuvGEAz6voErL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-j04ZuvGEAz6voErL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-j04ZuvGEAz6voErL .marker.cross{stroke:#333333;}#mermaid-svg-j04ZuvGEAz6voErL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-j04ZuvGEAz6voErL p{margin:0;}#mermaid-svg-j04ZuvGEAz6voErL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster-label text{fill:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster-label span{color:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster-label span p{background-color:transparent;}#mermaid-svg-j04ZuvGEAz6voErL .label text,#mermaid-svg-j04ZuvGEAz6voErL span{fill:#333;color:#333;}#mermaid-svg-j04ZuvGEAz6voErL .node rect,#mermaid-svg-j04ZuvGEAz6voErL .node circle,#mermaid-svg-j04ZuvGEAz6voErL .node ellipse,#mermaid-svg-j04ZuvGEAz6voErL .node polygon,#mermaid-svg-j04ZuvGEAz6voErL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .rough-node .label text,#mermaid-svg-j04ZuvGEAz6voErL .node .label text,#mermaid-svg-j04ZuvGEAz6voErL .image-shape .label,#mermaid-svg-j04ZuvGEAz6voErL .icon-shape .label{text-anchor:middle;}#mermaid-svg-j04ZuvGEAz6voErL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .rough-node .label,#mermaid-svg-j04ZuvGEAz6voErL .node .label,#mermaid-svg-j04ZuvGEAz6voErL .image-shape .label,#mermaid-svg-j04ZuvGEAz6voErL .icon-shape .label{text-align:center;}#mermaid-svg-j04ZuvGEAz6voErL .node.clickable{cursor:pointer;}#mermaid-svg-j04ZuvGEAz6voErL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-j04ZuvGEAz6voErL .arrowheadPath{fill:#333333;}#mermaid-svg-j04ZuvGEAz6voErL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-j04ZuvGEAz6voErL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-j04ZuvGEAz6voErL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j04ZuvGEAz6voErL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-j04ZuvGEAz6voErL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j04ZuvGEAz6voErL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-j04ZuvGEAz6voErL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .cluster text{fill:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster span{color:#333;}#mermaid-svg-j04ZuvGEAz6voErL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-j04ZuvGEAz6voErL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-j04ZuvGEAz6voErL rect.text{fill:none;stroke-width:0;}#mermaid-svg-j04ZuvGEAz6voErL .icon-shape,#mermaid-svg-j04ZuvGEAz6voErL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j04ZuvGEAz6voErL .icon-shape p,#mermaid-svg-j04ZuvGEAz6voErL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-j04ZuvGEAz6voErL .icon-shape .label rect,#mermaid-svg-j04ZuvGEAz6voErL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j04ZuvGEAz6voErL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-j04ZuvGEAz6voErL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-j04ZuvGEAz6voErL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Sparse Header

魔数 + 块大小 + 总块数
Raw Chunk CAC1

原始数据
Fill Chunk CAC2

填充数据
Skip Chunk CAC4

空洞跳过
... 更多 Chunk

Chunk 类型:

类型 标识 说明
CAC1 Raw 原始数据块
CAC2 Fill 用固定值填充(如全零)
CAC3 Don't Care 保留但无数据
CAC4 Skip 跳过(空洞,不占用空间)

稀疏格式的实际效果:一个 4GB 的 super 分区,如果实际数据只有 2GB,稀疏镜像可能只有 2.1GB(考虑元数据),节省近 50% 的传输和存储空间。


七、lpdump 工具实战

4.1 使用 lpdump 解析 super 镜像

lpdump 是 AOSP 自带的元数据解析工具,源码在 system/liblp/lpdump.cpp。

bash 复制代码
# 从设备读取 super 元数据
adb shell lpdump

# 或者直接解析 super 镜像文件
lpdump /path/to/super.img

输出示例:

复制代码
Device: super
Slot 0:
  Metadata version: 10.0
  Block device size: 4,294,967,296 bytes (4096 MB)
  Partition layout:
    Name: system_a
      Group: default
      Attributes: readonly+slot-suffixed
      Size: 1,048,576,000 bytes (1000 MB)
      Extents:
        [0: 0, 2048000 sectors]
    Name: vendor_a
      Group: default
      Attributes: readonly+slot-suffixed
      Size: 524,288,000 bytes (500 MB)
      Extents:
        [0: 2048000, 1024000 sectors]
    Name: product_a
      Group: default
      Attributes: readonly+slot-suffixed
      Size: 524,288,000 bytes (500 MB)
      Extents:
        [0: 3072000, 1024000 sectors]

  Groups:
    Name: default
      Maximum size: 4,294,967,296 bytes
    Name: google_dynamic_updatable
      Maximum size: 1,073,741,824 bytes

4.2 解析过程详解

lpdump 内部的工作流程:

复制代码
1. 打开设备/文件
2. 跳过 GPT(前 512 字节的 MBR + GPT header)
3. 在 sector 8 (4096 字节偏移) 开始搜索 LP 元数据
4. 读取 LP_META_HEADER,验证 magic
5. 根据 header 中的 partition_count 读取分区表项
6. 读取 extent 表构建完整映射
7. 读取 group 描述符
8. 格式化输出

关键代码片段:

cpp 复制代码
// system/liblp/lpdump.cpp (简化)
int DumpMetadata(int fd) {
    // 1. 读取 header
    LpMetadataHeader header;
    if (!ReadHeader(fd, &header)) return -1;

    // 2. 验证魔数
    if (header.magic != LP_META_HEADER_MAGIC) {
        fprintf(stderr, "Invalid magic: 0x%08x\n", header.magic);
        return -1;
    }

    // 3. 读取分区表
    std::vector<LpMetadataPartition> partitions;
    partitions.resize(header.partition_count);
    read(fd, partitions.data(), sizeof(LpMetadataPartition) * header.partition_count);

    // 4. 输出每个分区的详细信息
    for (auto& part : partitions) {
        printf("Name: %s\n", part.name);
        printf("  Size: %llu bytes\n", CalculateSize(part));
        printf("  Extents:\n");
        for (int i = 0; i < part.num_extents; i++) {
            auto& ext = extents[part.first_extent_index + i];
            printf("    [%d: %llu, %llu sectors]\n",
                   i, ext.sector_offset, ext.num_sectors);
        }
    }
    return 0;
}

八、lpunpack:从 super 拆解逻辑分区

5.1 基本用法

lpunpack 用于从 super 镜像中提取出各个逻辑分区的原始镜像:

bash 复制代码
# 解包整个 super
lpunpack super.img output_dir/

# 指定 metadata slot
lpunpack --slot=0 super.img output_dir/

输出:

复制代码
output_dir/
├── system_a.img
├── vendor_a.img
├── product_a.img
└── system_b.img (如果可用)

5.2 实现原理

lpunpack 的核心是读取 extent 映射,然后从 super 中按照 extent 描述的物理扇区位置读取数据,拼合为原始分区镜像:

cpp 复制代码
// system/liblp/lpunpack.cpp (简化)
int UnpackImage(int fd, const LpMetadata& metadata,
                const LpMetadataPartition& partition,
                const std::string& output_dir) {
    // 创建输出文件
    std::string path = output_dir + "/" + partition.name + ".img";
    int out_fd = open(path.c_str(), O_CREAT | O_WRONLY, 0644);

    // 遍历该分区的所有 extent
    for (int i = 0; i < partition.num_extents; i++) {
        auto& extent = metadata.extents[partition.first_extent_index + i];

        // 从 super 分区的对应扇区偏移读取数据
        lseek(fd, extent.sector_offset * 512, SEEK_SET);
        std::vector<char> buffer(extent.num_sectors * 512);
        read(fd, buffer.data(), buffer.size());

        // 写入输出文件
        write(out_fd, buffer.data(), buffer.size());
    }

    close(out_fd);
    return 0;
}

注意 :如果分区包含多个不连续的 extent,lpunpack 会按照分区表项中的顺序依次读取并拼接,最终在用户空间呈现为一个连续的文件。


九、liblp 核心源码分析

9.1 liblp 的整体架构

liblp 是 AOSP 中管理动态分区元数据的基础库,位于 system/liblp/。其核心组件:

复制代码
system/liblp/
├── include/liblp/
│   ├── metadata_format.h     ← 数据结构定义
│   ├── partition_opts.h      ← 分区选项
│   ├── builder.h             ← 元数据构建器
│   ├── reader.h              ← 元数据读取器
│   └── writer.h              ← 元数据写入器
├── builder.cpp               ← 构建/修改元数据
├── reader.cpp                ← 从设备/镜像读取元数据
├── writer.cpp                ← 写入元数据到设备/镜像
├── utility.cpp               ← 工具函数
└── lpdump.cpp                ← lpdump 工具

6.2 读取流程 (reader.cpp)

cpp 复制代码
// 核心读取函数
std::unique_ptr<LpMetadata> ReadMetadata(int fd, uint32_t slot) {
    auto metadata = std::make_unique<LpMetadata>();

    // Step 1: 定位 metadata 位置
    // metadata 从 super 分区的 LPMETA_OFFSET 开始
    // 每份 metadata 间隔 LP_SECTOR_SIZE (4096 字节)
    off64_t offset = GetMetadataOffset(slot);
    lseek64(fd, offset, SEEK_SET);

    // Step 2: 读取 header
    LpMetadataHeader header;
    ReadFully(fd, &header, sizeof(header));

    // Step 3: 验证完整性
    if (header.magic != LP_META_HEADER_MAGIC) {
        return nullptr;  // 无效的 metadata
    }

    // Step 4: 读取所有分区表项
    metadata->partitions.resize(header.partition_count);
    ReadFully(fd, metadata->partitions.data(),
              sizeof(LpMetadataPartition) * header.partition_count);

    // Step 5: 读取所有 extent
    metadata->extents.resize(header.extent_count);
    ReadFully(fd, metadata->extents.data(),
              sizeof(LpMetadataExtent) * header.extent_count);

    // Step 6: 读取 group 表
    metadata->groups.resize(header.group_count);
    ReadFully(fd, metadata->groups.data(),
              sizeof(LpMetadataPartitionGroup) * header.group_count);

    return metadata;
}

6.3 写入流程 (writer.cpp)

写入流程比读取复杂,需要处理 CRC 校验和对齐:

cpp 复制代码
bool WriteMetadata(int fd, const LpMetadata& metadata) {
    // Step 1: 序列化所有元数据到缓冲区
    std::vector<uint8_t> buffer;
    SerializeMetadata(metadata, &buffer);

    // Step 2: 计算 CRC32 校验和
    uint32_t crc32 = ComputeCrc32(buffer);

    // Step 3: 写入到所有 metadata slot
    for (int slot = 0; slot < kMaxMetadataSlots; slot++) {
        off64_t offset = slot * 4096;
        WriteFully(fd, GetMetadataMagic(), 4);
        WriteFully(fd, buffer.data(), buffer.size());
        // 写入 CRC 到 slot 末尾
        WriteFully(fd, &crc32, sizeof(crc32));
    }

    // Step 4: 同步 (fsync)
    fsync(fd);
    return true;
}

6.4 构建器 (builder.cpp)

builder 用于从零创建或修改元数据------这在编译时生成 super 镜像时非常关键:

cpp 复制代码
class PartitionOpener {
public:
    // 添加一个逻辑分区
    bool AddPartition(const std::string& name, uint64_t size,
                      uint32_t attributes) {
        LpMetadataPartition part = {};
        part.name = name;
        part.attributes = attributes;

        // 自动分配 extent
        uint64_t remaining = size;
        uint64_t sector = FindFreeSectors();
        while (remaining > 0) {
            LpMetadataExtent extent;
            uint64_t alloc_size = std::min(remaining, kMaxExtentSize);
            extent.num_sectors = alloc_size / 512;
            extent.sector_offset = sector;

            part.extents.push_back(extent);
            sector += extent.num_sectors;
            remaining -= alloc_size;
        }

        partitions_.push_back(part);
        return true;
    }

    // 最终生成完整的 LpMetadata
    LpMetadata Build() {
        LpMetadata metadata;
        // 合并所有分区和 extent 到最终的线性表中
        LinearizeExtents(&metadata);
        // 检查空间约束
        ValidateGroupSizes(metadata);
        return metadata;
    }
};

十、Super 分区生命周期管理

7.1 编译时:生成 super.img

在 AOSP 编译过程中,build/make/core/config.mk 和 BoardConfig.mk 共同决定了 super 分区的大小和内容:

makefile 复制代码
# BoardConfig.mk 典型配置
BOARD_SUPER_PARTITION_SIZE := 4294967296        # 4GB
BOARD_SUPER_PARTITION_GROUPS := default
BOARD_SUPER_PARTITION_ERROR_LIMIT := 4194304000  # 约 4GB

# 各逻辑分区大小
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1073741824  # 1GB
BOARD_VENDORIMAGE_PARTITION_SIZE := 536870912   # 512MB
BOARD_PRODUCTIMAGE_PARTITION_SIZE := 536870912  # 512MB

# A/B 切片
TARGET_NO_RECOVERY := true
BOARD_USES_RECOVERY_AS_BOOT := true
AB_OTA_UPDATER := true

构建超级镜像的核心脚本在 build/make/tools/releasetools/:

python 复制代码
# add_img_to_target_files.py (简化)
def BuildSuperImage(input_dir, output_file):
    # 1. 收集所有逻辑分区的镜像文件
    images = {
        'system_a': input_dir + '/SYSTEM',
        'vendor_a': input_dir + '/VENDOR',
        'product_a': input_dir + '/PRODUCT',
    }

    # 2. 计算空间分配
    total_size = sum(os.path.getsize(f) for f in images.values())

    # 3. 调用 lpmake 生成 super 镜像
    args = [
        'lpmake',
        '--device-size', str(BOARD_SUPER_PARTITION_SIZE),
        '--metadata-size', '65536',
        '--super-name', 'super',
    ]
    for name, path in images.items():
        args += ['--partition', f'{name}:readonly:{os.path.getsize(path)}']
        args += ['--image', f'{name}={path}']

    subprocess.run(args, check=True)
    # 输出 super.img

7.2 刷写时:fastboot 的处理

fastboot flash super 是动态分区刷写的入口:

bash 复制代码
# 刷写整个 super
fastboot flash super super.img

# 仅刷写特定逻辑分区(稀疏模式)
fastboot flash system_a system_a.img

fastboot 内部的处理逻辑:

复制代码
1. 解析 super.img 的 LP 元数据
2. 对比设备当前的元数据
3. 按 extent 顺序逐块写入
4. 更新元数据(新 slot)
5. 验证写入完整性

关键策略:fastboot 支持稀疏写入,即只写入 super 中实际包含数据的区域,跳过空洞。这大幅减少了刷写时间。

7.3 OTA 升级时:动态调整

动态分区最大的优势体现在 OTA 升级中:
#mermaid-svg-ezqt53hzsrzF9STG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ezqt53hzsrzF9STG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ezqt53hzsrzF9STG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ezqt53hzsrzF9STG .error-icon{fill:#552222;}#mermaid-svg-ezqt53hzsrzF9STG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ezqt53hzsrzF9STG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ezqt53hzsrzF9STG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ezqt53hzsrzF9STG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ezqt53hzsrzF9STG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ezqt53hzsrzF9STG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ezqt53hzsrzF9STG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ezqt53hzsrzF9STG .marker.cross{stroke:#333333;}#mermaid-svg-ezqt53hzsrzF9STG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ezqt53hzsrzF9STG p{margin:0;}#mermaid-svg-ezqt53hzsrzF9STG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster-label text{fill:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster-label span{color:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster-label span p{background-color:transparent;}#mermaid-svg-ezqt53hzsrzF9STG .label text,#mermaid-svg-ezqt53hzsrzF9STG span{fill:#333;color:#333;}#mermaid-svg-ezqt53hzsrzF9STG .node rect,#mermaid-svg-ezqt53hzsrzF9STG .node circle,#mermaid-svg-ezqt53hzsrzF9STG .node ellipse,#mermaid-svg-ezqt53hzsrzF9STG .node polygon,#mermaid-svg-ezqt53hzsrzF9STG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .rough-node .label text,#mermaid-svg-ezqt53hzsrzF9STG .node .label text,#mermaid-svg-ezqt53hzsrzF9STG .image-shape .label,#mermaid-svg-ezqt53hzsrzF9STG .icon-shape .label{text-anchor:middle;}#mermaid-svg-ezqt53hzsrzF9STG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .rough-node .label,#mermaid-svg-ezqt53hzsrzF9STG .node .label,#mermaid-svg-ezqt53hzsrzF9STG .image-shape .label,#mermaid-svg-ezqt53hzsrzF9STG .icon-shape .label{text-align:center;}#mermaid-svg-ezqt53hzsrzF9STG .node.clickable{cursor:pointer;}#mermaid-svg-ezqt53hzsrzF9STG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ezqt53hzsrzF9STG .arrowheadPath{fill:#333333;}#mermaid-svg-ezqt53hzsrzF9STG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ezqt53hzsrzF9STG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ezqt53hzsrzF9STG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ezqt53hzsrzF9STG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ezqt53hzsrzF9STG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ezqt53hzsrzF9STG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ezqt53hzsrzF9STG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .cluster text{fill:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster span{color:#333;}#mermaid-svg-ezqt53hzsrzF9STG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ezqt53hzsrzF9STG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ezqt53hzsrzF9STG rect.text{fill:none;stroke-width:0;}#mermaid-svg-ezqt53hzsrzF9STG .icon-shape,#mermaid-svg-ezqt53hzsrzF9STG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ezqt53hzsrzF9STG .icon-shape p,#mermaid-svg-ezqt53hzsrzF9STG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ezqt53hzsrzF9STG .icon-shape .label rect,#mermaid-svg-ezqt53hzsrzF9STG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ezqt53hzsrzF9STG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ezqt53hzsrzF9STG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ezqt53hzsrzF9STG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OTA 升级后
system: 1.2GB
vendor: 512MB
product: 512MB
空闲: 0MB
OTA 升级前
system: 1GB
vendor: 512MB
product: 256MB
空闲: 256MB

OTA 包中的 update_engine 通过如下步骤调整分区:

  1. 读取旧元数据:获取当前 slot 的分配情况
  2. 计算新需求:根据 OTA 包内容计算各分区的新大小
  3. 重新分配空间:减小某分区 → 增大另一分区
  4. 写入 COW 分区:对于启用快照的设备,写入改动前的备份
  5. 更新元数据:将新元数据写入备用 slot
  6. 切换 slot:重启进入新系统

snapshot(快照)机制(Android 11+):在 OTA 过程中,被覆盖的数据块会先备份到 COW(Copy-On-Write)分区。如果 OTA 失败,可以回滚到升级前的状态。这是 Android 虚拟 A/B (Virtual A/B) 的核心基础。


十一、Virtual A/B 与 Snapshot 机制详解

11.1 为什么需要 Virtual A/B

传统 A/B(也称为"物理 A/B")需要为每个逻辑分区维护 A 和 B 两份副本:
#mermaid-svg-dFFjDqol7lI2GVU7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dFFjDqol7lI2GVU7 .error-icon{fill:#552222;}#mermaid-svg-dFFjDqol7lI2GVU7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dFFjDqol7lI2GVU7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 .marker.cross{stroke:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dFFjDqol7lI2GVU7 p{margin:0;}#mermaid-svg-dFFjDqol7lI2GVU7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster-label text{fill:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster-label span{color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster-label span p{background-color:transparent;}#mermaid-svg-dFFjDqol7lI2GVU7 .label text,#mermaid-svg-dFFjDqol7lI2GVU7 span{fill:#333;color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .node rect,#mermaid-svg-dFFjDqol7lI2GVU7 .node circle,#mermaid-svg-dFFjDqol7lI2GVU7 .node ellipse,#mermaid-svg-dFFjDqol7lI2GVU7 .node polygon,#mermaid-svg-dFFjDqol7lI2GVU7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .rough-node .label text,#mermaid-svg-dFFjDqol7lI2GVU7 .node .label text,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape .label,#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-dFFjDqol7lI2GVU7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .rough-node .label,#mermaid-svg-dFFjDqol7lI2GVU7 .node .label,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape .label,#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape .label{text-align:center;}#mermaid-svg-dFFjDqol7lI2GVU7 .node.clickable{cursor:pointer;}#mermaid-svg-dFFjDqol7lI2GVU7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 .arrowheadPath{fill:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dFFjDqol7lI2GVU7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dFFjDqol7lI2GVU7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dFFjDqol7lI2GVU7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dFFjDqol7lI2GVU7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dFFjDqol7lI2GVU7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster text{fill:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster span{color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dFFjDqol7lI2GVU7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape p,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape .label rect,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dFFjDqol7lI2GVU7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dFFjDqol7lI2GVU7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dFFjDqol7lI2GVU7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} super 分区
A Slot
B Slot
system_a
vendor_a
product_a
system_b
vendor_b
product_b

这意味着 50% 的空间被用于"备份",对于存储空间受限的设备这是巨大的浪费。

Virtual A/B 的核心思想是:不再为 B slot 预分配空间,而是使用 Copy-On-Write (COW) 机制,在 OTA 过程中只备份那些被覆盖的数据块:
#mermaid-svg-94ACHoFaVRSVe2gC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-94ACHoFaVRSVe2gC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-94ACHoFaVRSVe2gC .error-icon{fill:#552222;}#mermaid-svg-94ACHoFaVRSVe2gC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-94ACHoFaVRSVe2gC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC .marker.cross{stroke:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-94ACHoFaVRSVe2gC p{margin:0;}#mermaid-svg-94ACHoFaVRSVe2gC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster-label text{fill:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster-label span{color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster-label span p{background-color:transparent;}#mermaid-svg-94ACHoFaVRSVe2gC .label text,#mermaid-svg-94ACHoFaVRSVe2gC span{fill:#333;color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .node rect,#mermaid-svg-94ACHoFaVRSVe2gC .node circle,#mermaid-svg-94ACHoFaVRSVe2gC .node ellipse,#mermaid-svg-94ACHoFaVRSVe2gC .node polygon,#mermaid-svg-94ACHoFaVRSVe2gC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .rough-node .label text,#mermaid-svg-94ACHoFaVRSVe2gC .node .label text,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape .label,#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape .label{text-anchor:middle;}#mermaid-svg-94ACHoFaVRSVe2gC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .rough-node .label,#mermaid-svg-94ACHoFaVRSVe2gC .node .label,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape .label,#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape .label{text-align:center;}#mermaid-svg-94ACHoFaVRSVe2gC .node.clickable{cursor:pointer;}#mermaid-svg-94ACHoFaVRSVe2gC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC .arrowheadPath{fill:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-94ACHoFaVRSVe2gC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-94ACHoFaVRSVe2gC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-94ACHoFaVRSVe2gC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-94ACHoFaVRSVe2gC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-94ACHoFaVRSVe2gC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-94ACHoFaVRSVe2gC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster text{fill:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster span{color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-94ACHoFaVRSVe2gC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-94ACHoFaVRSVe2gC rect.text{fill:none;stroke-width:0;}#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape p,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape .label rect,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-94ACHoFaVRSVe2gC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-94ACHoFaVRSVe2gC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-94ACHoFaVRSVe2gC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} super 分区
A Slot

激活使用
COW 分区

按需分配
system
vendor
product
OTA 备份数据块

11.2 COW 分区的数据结构

COW(Copy-On-Write)分区使用了专门的数据结构来跟踪被修改的 block:

c 复制代码
// system/update_engine/的分区快照相关结构
struct CowHeader {
    uint32_t magic;              // "COW\0"
    uint32_t major_version;
    uint32_t minor_version;
    uint64_t block_size;         // 通常为 4096
    uint64_t num_blocks;         // COW 管理的 block 总数
    uint64_t num_merges;         // 已合并的操作数
    uint64_t op_count;           // 操作条目数
};

// COW 操作条目:记录哪些 block 被修改了
struct CowOperation {
    uint64_t source_block;   // 在源分区中的 block 号
    uint64_t target_block;   // 在目标分区中的 block 号
    uint32_t num_blocks;     // 连续的 block 数量
    uint8_t  type;           // 操作类型
    // 类型: kCowCopyOp (拷贝), kCowReplaceOp (替换),
    //       kCowZeroOp (清零), kCowXorOp (异或增量)
};

11.3 OTA 升级过程中的 Snapshot 处理

一次完整的 Virtual A/B OTA 流程如下:

复制代码
Phase 1: 下载 OTA 包 (在 system_a 运行中)
  ├── update_engine 下载 OTA 包到 /data/ota_package
  ├── 解析 OTA 包中的 payload
  └── 读取当前 system_a 的 LP 元数据

Phase 2: 生成新分区布局
  ├── 从 OTA payload 中提取新分区的 sizes
  ├── 计算需要增大的分区和需要减小的分区
  ├── 重新分配 super 中的空闲空间
  └── 创建新的 LP 元数据 (写入 slot 2)

Phase 3: 写时复制 (COW)
  ├── 对于将要被覆盖的 block:
  │   1. 读取原始 block 数据
  │   2. 写入 COW 分区
  │   3. 记录 CowOperation
  ├── 将新数据写入目标位置
  └── 更新 COW 元数据

Phase 4: 切换与合并
  ├── 更新 boot_control 的 slot 标记
  ├── 重启进入新系统 (system_b)
  ├── 在 init 阶段检查是否有未完成的 COW
  ├── 如果有,启动 cow_merge 进程
  └── 合并完成后释放 COW 空间

11.4 Snapshot 的合并过程 (cow_merge)

合并是 Virtual A/B 中最关键的环节------它将 COW 中的修改"合并"到目标分区,使新系统可以独立运行:

cpp 复制代码
// 简化的 cow_merge 逻辑
bool MergeCowToTarget(const std::string& cow_device,
                      const std::string& target_device) {
    // 1. 读取 COW 头部
    CowHeader header = ReadCowHeader(cow_device);

    // 2. 遍历所有操作
    for (uint64_t i = 0; i < header.op_count; i++) {
        CowOperation op = ReadCowOp(cow_device, i);

        switch (op.type) {
        case kCowCopyOp:
            // 从 COW 设备读取备份数据,写回目标位置
            char buffer[op.num_blocks * 4096];
            ReadCowData(cow_device, op.source_block, buffer);
            WriteDevice(target_device, op.target_block, buffer);
            break;

        case kCowZeroOp:
            // 目标位置清零
            ZeroDevice(target_device, op.target_block, op.num_blocks);
            break;

        case kCowReplaceOp:
            // 数据已经在新位置,只需更新映射
            UpdateMapping(target_device, op.target_block, op.num_blocks);
            break;
        }

        // 标记操作为"已合并"
        MarkCowOpCompleted(cow_device, i);
    }

    // 3. 合并完成后,释放 COW 空间
    ReleaseCowSpace(cow_device);
    return true;
}

合并过程可以在系统空闲时后台执行,也可以在 init 阶段同步执行。Google 的设计目标是将合并时间控制在 2 分钟以内。


十二、AVB 与动态分区的集成

12.1 完整性校验链

Android Verified Boot (AVB) 2.0 与动态分区紧密配合:
#mermaid-svg-3qglujpHiQDQJ2KO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3qglujpHiQDQJ2KO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3qglujpHiQDQJ2KO .error-icon{fill:#552222;}#mermaid-svg-3qglujpHiQDQJ2KO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3qglujpHiQDQJ2KO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO .marker.cross{stroke:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3qglujpHiQDQJ2KO p{margin:0;}#mermaid-svg-3qglujpHiQDQJ2KO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster-label text{fill:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster-label span{color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster-label span p{background-color:transparent;}#mermaid-svg-3qglujpHiQDQJ2KO .label text,#mermaid-svg-3qglujpHiQDQJ2KO span{fill:#333;color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .node rect,#mermaid-svg-3qglujpHiQDQJ2KO .node circle,#mermaid-svg-3qglujpHiQDQJ2KO .node ellipse,#mermaid-svg-3qglujpHiQDQJ2KO .node polygon,#mermaid-svg-3qglujpHiQDQJ2KO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .rough-node .label text,#mermaid-svg-3qglujpHiQDQJ2KO .node .label text,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape .label,#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape .label{text-anchor:middle;}#mermaid-svg-3qglujpHiQDQJ2KO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .rough-node .label,#mermaid-svg-3qglujpHiQDQJ2KO .node .label,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape .label,#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape .label{text-align:center;}#mermaid-svg-3qglujpHiQDQJ2KO .node.clickable{cursor:pointer;}#mermaid-svg-3qglujpHiQDQJ2KO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO .arrowheadPath{fill:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3qglujpHiQDQJ2KO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3qglujpHiQDQJ2KO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3qglujpHiQDQJ2KO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3qglujpHiQDQJ2KO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3qglujpHiQDQJ2KO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3qglujpHiQDQJ2KO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster text{fill:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster span{color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3qglujpHiQDQJ2KO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3qglujpHiQDQJ2KO rect.text{fill:none;stroke-width:0;}#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape p,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape .label rect,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3qglujpHiQDQJ2KO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3qglujpHiQDQJ2KO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3qglujpHiQDQJ2KO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} avb: 验证签名
avb: 验证签名
avb: 验证 vbmeta
avb: 验证 init
avb: 验证文件系统
avb: 验证文件系统
avb: 验证文件系统
PBL 一级引导
SBL 二级引导
ABL 应用引导
Linux Kernel
init 进程
system
vendor
product

Bootloader (ABL) 通过 GPT 找到 super 分区,读取 LP 元数据,通过 device-mapper 创建逻辑分区设备,然后校验 system 分区的 vbmeta 头部。

12.2 vbmeta 在动态分区中的位置

对于动态分区,AVB 的 vbmeta 信息嵌入在逻辑分区的镜像头部:
#mermaid-svg-HSvfPi4sCJpOjUV6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HSvfPi4sCJpOjUV6 .error-icon{fill:#552222;}#mermaid-svg-HSvfPi4sCJpOjUV6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HSvfPi4sCJpOjUV6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .marker.cross{stroke:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HSvfPi4sCJpOjUV6 p{margin:0;}#mermaid-svg-HSvfPi4sCJpOjUV6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster-label text{fill:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster-label span{color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster-label span p{background-color:transparent;}#mermaid-svg-HSvfPi4sCJpOjUV6 .label text,#mermaid-svg-HSvfPi4sCJpOjUV6 span{fill:#333;color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node rect,#mermaid-svg-HSvfPi4sCJpOjUV6 .node circle,#mermaid-svg-HSvfPi4sCJpOjUV6 .node ellipse,#mermaid-svg-HSvfPi4sCJpOjUV6 .node polygon,#mermaid-svg-HSvfPi4sCJpOjUV6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .rough-node .label text,#mermaid-svg-HSvfPi4sCJpOjUV6 .node .label text,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .rough-node .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .node .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape .label{text-align:center;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node.clickable{cursor:pointer;}#mermaid-svg-HSvfPi4sCJpOjUV6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .arrowheadPath{fill:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HSvfPi4sCJpOjUV6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster text{fill:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster span{color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HSvfPi4sCJpOjUV6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape p,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape .label rect,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HSvfPi4sCJpOjUV6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HSvfPi4sCJpOjUV6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} system_a 逻辑分区
AVB Header

vbmeta 元数据 + 签名
ext4 / erofs

文件系统数据
哈希树 / dm-verity

完整性校验

Bootloader 读取方式:

cpp 复制代码
// bootloader vbmeta 校验伪代码
void VerifyDynamicPartition(const char* name) {
    // 1. 通过 liblp 获取分区的 extent
    auto metadata = ReadMetadata(super_fd, current_slot);

    // 2. 计算分区设备路径
    char devpath[64];
    snprintf(devpath, sizeof(devpath),
             "/dev/block/dm-%d", GetDmNumber(name));

    // 3. 在分区头部查找 AVB 信息
    int fd = open(devpath, O_RDONLY);
    AvbVBMetaData vbmeta = AvbReadVBMeta(fd);

    // 4. 验证签名链
    if (!AvbVerifyVBMeta(vbmeta, trusted_keys)) {
        EnterRecovery();
    }
}

十三、实际案例:AOSP 配置分析

13.1 Pixel 6 (Oriole) 的 super 分区配置

让我们以 Pixel 6 为例,分析实际设备中的动态分区布局:

bash 复制代码
# 从 Pixel 6 读取 lpdump 输出 (Android 13)
super: 5,345,285,376 bytes (5GB)
Slot 0:
  逻辑分区 (共 6 个):
    Name          Size        Group
    system_a      1,572,864,000  default
    system_b      1,572,864,000  default
    vendor_a      536,870,912    default
    vendor_b      536,870,912    default
    product_a     805,306,368    google_dynamic_updatable
    product_b     805,306,368    google_dynamic_updatable

  空闲空间: ~523,000,000 bytes (~500MB)

这个配置反映了几个关键设计决策:

  1. A/B 对称分配:两个 slot 的大小完全一致,这是 A/B 升级的基本要求
  2. product 单独分组 :google_dynamic_updatable 组允许 Google Play 直接更新 product 分区,而无需完整 OTA
  3. 500MB 预留空间:用于应对未来 OTA 可能增长的分区

13.2 定制 ROM 的 BoardConfig 示例

对于自定义 ROM 开发者,BoardConfig.mk 的配置示例如下:

makefile 复制代码
# 启用动态分区
BOARD_SUPER_PARTITION_SIZE := 5368709120  # 5GB
BOARD_SUPER_PARTITION_GROUPS := default

# default group 包含所有 A/B 分区
BOARD_SUPER_PARTITION_GROUP_DEFAULT_SIZE := 5368709120

# 分区大小
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1572864000
BOARD_VENDORIMAGE_PARTITION_SIZE := 536870912
BOARD_PRODUCTIMAGE_PARTITION_SIZE := 805306368

# 启用 AVB
BOARD_AVB_ENABLE := true
BOARD_AVB_MAKE_VBMETA_IMAGE_ARGS += --set_hashtree_disabled_flag

# 文件系统
BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := ext4
BOARD_USERDATAIMAGE_FILE_SYSTEM_TYPE := f2fs

关键提醒 :修改动态分区配置后,必须执行 make clean 重新编译,否则旧的分区镜像大小与新的元数据不匹配,会导致 lpmake 失败。


十四、常见问题与调试技巧

14.1 调试工具汇总

bash 复制代码
# 1. 查看设备 super 分区元数据
adb shell lpdump

# 2. 查看逻辑分区的 device-mapper 映射
adb shell dmsetup table
# 输出示例:
# system_a: 0 2048000 linear /dev/block/sda16 2048000

# 3. 查看分区设备
ls -l /dev/block/by-name/
# super -> /dev/block/sda16
# system_a -> /dev/block/dm-0
# vendor_a -> /dev/block/dm-1

# 4. 分析 super 镜像
lpdump super.img
lpunpack super.img out/

# 5. 十六进制查看元数据
xxd -l 128 super.img | head -20

14.2 常见错误及解决方案

错误 原因 解决方案
lpdump: Invalid magic 元数据损坏或非 super 镜像 确认文件是有效的 super 分区镜像
lpmake: Partition size exceeds group limit 分区总大小超过 group 限制 调整 BOARD_SUPER_PARTITION_GROUP_*_SIZE
fastboot: No such partition GPT 中没有 super 分区 检查分区表或刷写正确的 GPT
liblp: CRC mismatch 元数据校验失败 尝试另一份 metadata slot(--slot=1)
update_engine: Not enough space OTA 时空间不足 清理 cache 或增大 super 分区

14.3 手动修复元数据

在极端情况下,如果 super 分区的元数据损坏,可以尝试手动修复:

bash 复制代码
# 1. 备份当前扇区
dd if=/dev/block/sda16 of=/tmp/super_backup.img bs=4M

# 2. 读取元数据区域
dd if=/dev/block/sda16 of=/tmp/metadata.bin bs=512 count=32

# 3. 使用 lpdump 诊断
lpdump /tmp/super_backup.img

# 4. 如果有备用 slot,尝试切换
lpdump --slot=1 /tmp/super_backup.img

警告:直接操作分区数据风险极高,仅限开发者调试设备使用。生产环境应通过 fastboot 或 OTA 刷写完整镜像。

14.4 实战:从 super.img 中提取并分析某个逻辑分区

以下是完整的"从 super.img 提取并分析 system 分区"的实战流程:

bash 复制代码
# Step 1: 先查看 super 镜像的元数据
lpdump super.img

# 输出中可以看到 system_a 的 extent 信息:
# system_a: size=1GB, extents=[0: 65536, 2048000]

# Step 2: 使用 lpunpack 提取所有逻辑分区
lpunpack super.img extracted/

# Step 3: 检查提取的分区
ls -lh extracted/
# -rw-r--r--  system_a.img   (1GB)
# -rw-r--r--  vendor_a.img   (500MB)

# Step 4: 挂载提取的 system 分区
sudo mount -o loop,ro extracted/system_a.img /mnt/android_system/
ls /mnt/android_system/
# bin  build.prop  etc  framework  lib  media  ...

# Step 5: 分析 build.prop 确认版本
cat /mnt/android_system/build.prop | grep "ro.build.version"

# Step 6: 卸载
sudo umount /mnt/android_system/

直接 dd 提取特定分区的方法(不依赖 lpunpack):

bash 复制代码
# 直接从 super 分区中 dd 出 system_a 的数据
# 假设 lpdump 显示 system_a 的 extent 为:
#   [0: 65536 sectors, 2048000 sectors]
# 1 sector = 512 bytes
# 偏移 = 65536 * 512 = 33554432 bytes
# 大小 = 2048000 * 512 = 1048576000 bytes

dd if=super.img of=system_a_direct.img \
   bs=512 skip=65536 count=2048000

# 验证提取的镜像
file system_a_direct.img
# system_a_direct.img: Linux rev 1.0 ext4 filesystem data

# 检查文件系统完整性
e2fsck -n system_a_direct.img

十五、总结与展望

15.1 动态分区的核心优势回顾

  1. 空间池化:将多个逻辑分区合并到一个物理分区中管理,消除传统分区之间的空间浪费
  2. 弹性调整:可以在 OTA 升级时动态调整各分区大小,适配不同版本的容量需求
  3. 快照回滚:结合虚拟 A/B 和 COW,实现安全的 OTA 升级和回滚
  4. 元数据开销小:整个元数据通常仅占用 64KB~128KB,相对分区大小可以忽略不计

15.2 技术演进趋势

Android 版本 动态分区相关变化
Android 10 引入动态分区,super 作为物理容器
Android 11 引入 Virtual A/B,COW 快照
Android 12 支持压缩的 super 镜像(稀疏格式优化)
Android 13 完善 snapshot 机制,减少 OTA 空间要求
Android 14+ 进一步优化逻辑分区布局和空间效率

15.3 对开发者意味着什么

对于 Android 系统开发者、BSP 工程师和自定义 ROM 开发者而言,理解动态分区意味着:

  • 编译系统配置:需要准确配置 BoardConfig.mk 中的分区参数
  • 刷写流程变化:不再是逐个刷写 system/vendor,而是刷写 super 整体
  • 调试复杂度增加:需要掌握 lpdump、lpunpack 等新工具
  • OTA 测试要求更高:必须充分验证分区调整逻辑
bash 复制代码
# 一条命令掌握 super 分区全貌
adb shell lpdump && \
adb shell dmsetup table | grep linear && \
ls -l /dev/block/dm-*

本文从数据结构到源码实现,从编译配置到调试排查,系统性地分析了 Android 动态分区(super)的完整技术栈。希望对从事 AOSP 开发、系统移植和省固件优化的读者有所帮助。


参考资料:

  1. Android 官方文档:Dynamic Partitions - source.android.com/docs/core/ota/dynamic_partitions
  2. AOSP 源码:system/liblp/ - android.googlesource.com/platform/system/liblp/
  3. AOSP 源码:build/make/tools/releasetools/ - android.googlesource.com/platform/build/
  4. Android Verified Boot - source.android.com/docs/security/features/avb