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

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

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

1.1 传统分区方案的困境

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

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 描述);而 systemvendor 等逻辑分区通过 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.hinclude/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 元数据副本数 23
--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.mkBoardConfig.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