引导加载程序

时间:2024-03-06 09:51:54

引导加载程序

引导加载程序是供应商专有的映像,负责在设备上启动内核。它会监护设备状态,负责初始化可信执行环境 (TEE)以及绑定其信任根。

引导加载程序由许多部分组成,包括启动画面。要开始启动,引导加载程序可能会直接将一个新映像刷写到相应的分区中,也可能会使用 recovery 开始重新刷写过程,该过程与 OTA 的操作过程一致。一些设备制造商会创建多部分引导加载程序,然后将它们组合到一个 bootloader.img 文件中。在刷写时,引导加载程序会提取各个引导加载程序并刷写所有这些引导加载程序。

最重要的是,引导加载程序会在将执行工作移到内核之前先验证 boot 分区和 recovery 分区的完整性,并显示启动状态部分中指定的警告。

 

规范化启动原因

Android 9 对引导加载程序启动原因规范进行了以下更改。

启动原因

引导加载程序使用专用的硬件和内存资源来确定设备重新启动的原因,然后将 androidboot.bootreason=<reason> 添加到用于启动设备的 Android 内核命令行中,以传达这一决定。然后,init 会转换此命令行,使其传播到 Android 属性 bootloader_boot_reason_prop (ro.boot.bootreason) 中。

启动原因规范

之前的 Android 版本中指定的启动原因格式如下:不使用空格,全部为小写字母,只有非常少的要求(例如报告 kernel_panicwatchdogcold/warm/hard),并且允许其他特殊原因。这种宽松的规范导致出现了成百上千个自定义启动原因字符串(有时毫无意义),进而造成了无法管理的情况。到目前最新的 Android 版本发布之前,引导加载程序提交的近乎无法解析或毫无意义的内容急剧增加已经为 bootloader_boot_reason_prop 造成了合规性问题。

在开发 Android 9 版本中,Android 团队发现旧的 bootloader_boot_reason_prop 中内容会急剧增加,并且无法在系统运行时重写。因此,要对启动原因规范进行任何改进,都必须与引导加载程序开发者进行互动交流,并对现有系统进行调整。为此,Android 团队采取了以下措施:

  • 与引导加载程序开发者互动交流,鼓励他们:
    • 向 bootloader_boot_reason_prop 提供规范、可解析且可识别的原因。
    • 向 system/core/bootstat/bootstat.cpp kBootReasonMap 列表添加内容。
  • 添加受控且可在系统运行时重写的 system_boot_reason_prop (sys.boot.reason) 源代码。只有少量的系统应用(如 bootstat 和 init)可重写此属性,不过,所有应用都可以通过获得 sepolicy 权限来读取它。
  • 将启动原因告知用户,让他们等到用户数据装载完毕后再信任系统启动原因属性 system_boot_reason_prop 中的内容。

为什么要等这么久?虽然 bootloader_boot_reason_prop 在启动过程的早期阶段就已可用,但 Android 安全政策根据需要对其进行了屏蔽,因为它表示不准确、不可解析且不合规范的信息。大多数情况下,只有对启动系统有深入了解的开发者才需要访问这些信息。只有在用户数据装载完毕之后,才可以通过 system_boot_reason_prop准确可靠地提取经过优化、可解析且合乎规范的启动原因 API。具体而言:

  • 在用户数据装载完毕之前system_boot_reason_prop 将包含 bootloader_boot_reason_prop 中的值。
  • 在用户数据装载完毕之后,可以更新 system_boot_reason_prop,以使其符合要求或报告更准确的信息。

出于上述原因,Android 9 延长了可以正式获取启动原因之前需要等待的时间段,将其从启动时立即准确无误的状态(使用 bootloader_boot_reason_prop)更改为仅在用户数据装载完毕之后才可用(使用 system_boot_reason_prop)。

Bootstat 逻辑依赖于信息更丰富且合规的 bootloader_boot_reason_prop。当该属性使用可预测的格式时,能够提高所有受控重新启动和关机情况的准确性,从而优化和扩展 system_boot_reason_prop 的准确性和含义。

规范化启动原因格式

在 Android 9 中,bootloader_boot_reason_prop 的规范化启动原因格式使用以下语法:

<reason>,<subreason>,<detail>
 

格式设置规则如下:

  • 小写
  • 无空格(可使用下划线)
  • 全部为可打印字符
  • 以英文逗号分隔的 reasonsubreason,以及一个或多个 detail
    • 必需的 reason,表示设备为什么必须重新启动或关机且优先级最高的原因。
    • 选用的 subreason,表示设备为什么必须重新启动或关机的简短摘要(或重新启动设备/将设备关机的人员)。
    • 一个或多个选用的 detail 值。detail 可以指向某个子系统,以协助确定是哪个具体系统导致了 subreason。您可以指定多个 detail 值,这些值通常应按照重要程度排序。不过,也可以报告多个具有同等重要性的 detail 值。

如果 bootloader_boot_reason_prop 为空值,则会被视为非法(因为这会允许其他代理在事后添加启动原因)。

原因要求

为 reason(第一个跨度,位于终止符或英文逗号之前)指定的值必须是以下集合(分为内核原因、强原因和弱原因)之一:

  • 内核集:
    • "watchdog"
    • "kernel_panic"
  • 强集:
    • "recovery"
    • "bootloader"
  • 弱集:
    • "cold":通常表示完全重置所有设备,包括内存。
    • "hard":通常表示硬件重置了状态,并且 ramoops 应保留持久性内容。
    • "warm":通常表示内存和设备保持某种状态,并且 ramoops(请参阅内核中的 pstore 驱动程序)后备存储空间包含持久性内容。
    • "shutdown"
    • "reboot":通常意味着 ramoops 状态和硬件状态未知。该值是与 coldhard 和 warm 一样的通用值,可提供关于设备重置深度的提示。

引导加载程序必须提供内核集或弱集 reason,强烈建议引导加载程序提供 subreason(如果可以确定的话)。例如,电源键长按(无论是否有 ramoops 备份)的启动原因为 "reboot,longkey"

第一个跨度 reason 不能是任何 subreason 或 detail 的组成部分。不过,由于用户空间无法产生内核集原因,因此可能会在弱集原因之后重复使用 "watchdog" 以及源代码的详细信息(例如 "reboot,watchdog,service_manager_unresponsive" 或 "reboot,software,watchdog")。

启动原因应该无需专家级内部知识即可解读,并且(或者)应该能让人看懂并提供直观报告。示例:"shutdown,vbxd"(糟糕)、"shutdown,uv"(较好)、"shutdown,undervoltage"(首选)。

“原因-子原因”组合

Android 保留了一组 reason-subreason 组合,在正常使用情况下不应过量使用这些组合;不过,如果组合能准确反映相关状况,则可根据具体情况加以使用。保留组合的示例包括:

  • "reboot,userrequested"
  • "shutdown,userrequested"
  • "shutdown,thermal"(来自 thermald
  • "shutdown,battery"
  • "shutdown,battery,thermal"(来自 BatteryStatsService
  • "reboot,adb"
  • "reboot,shell"
  • "reboot,bootloader"
  • "reboot,recovery"

如需更多详细信息,请参阅 system/core/bootstat/bootstat.cpp 中的 kBootReasonMap 以及 Android 源代码库中的关联 git 变更日志记录。

报告启动原因

所有启动原因(无论是来自引导加载程序还是记录在规范化启动原因中)都必须记录在 system/core/bootstat/bootstat.cpp 的 kBootReasonMap 部分中。kBootReasonMap 列表包含各种合规原因和不合规的旧版原因。引导加载程序开发者应在此处仅登记新的合规原因(除非产品已发货且无法更改,否则不应登记不合规的原因)。

注意:虽然 system/core/bootstat/bootstat.cpp 包含一个 kBootReasonMap 部分,其中列出了大量旧版原因,但这些原因的存在并不意味着 reason 字符串已获准使用。该列表的一个子集内列出了合规原因;随着引导加载程序开发者不断登记更多合规原因并加以说明,这个子集预计将不断增大。

强烈建议使用 system/core/bootstat/bootstat.cpp 中现有的合规条目,如果要使用不合规字符串,要先对其加以限制。请参阅以下指导原则:

  • 允许从引导加载程序中报告 "kernel_panic",因为 bootstat 或许能检查 kernel_panic signatures的 ramoops,以便将子原因优化为规范的 system_boot_reason_prop
  • 不允许从引导加载程序中以 kBootReasonMap(如 "panic"))的形式报告不合规的字符串,因为这最终将导致无法优化 reason

例如,如果 kBootReasonMap 包含 "wdog_bark",则引导加载程序开发者应采取以下措施:

  • 更改为 "watchdog,bark",并将其添加到 kBootReasonMap 中的列表内。
  • 考虑 "bark" 对于不熟悉该技术的人来说意味着什么,并确定是否存在更有意义的 subreason

验证启动原因合规性

目前,对于引导加载程序可能提供的所有启动原因,Android 没有提供能够准确触发或检查这些原因的主动 CTS 测试;合作伙伴仍然可以尝试运行被动测试来确定兼容性。

因此,要实现引导加载程序合规性,引导加载程序开发者需要自愿遵循上述规则和准则的精神。我们会敦促此类开发者为 AOSP(特别是 system/core/bootstat/bootstat.cpp)做贡献,并将这个机会作为一个讨论启动原因问题的论坛。

 

启动映像标头版本编号

从 Android 9 起,启动映像标头开始包含一个用于指示标头版本的字段。引导加载程序必须检查该标头版本字段,并相应地解析标头。通过对启动映像标头进行版本编号,可在将来对标头进行修改,同时保持向后兼容性。

所有搭载 Android 9 的设备都必须使用启动标头版本 1。

启动映像标头更改

对于搭载 Android 9 的设备,旧版启动映像标头(如下所示)中的 unused 字段将会转换为标头版本字段。

struct boot_img_hdr
{
    uint8_t magic[BOOT_MAGIC_SIZE];
    uint32_t kernel_size;  /* size in bytes */
    uint32_t kernel_addr;  /* physical load addr */

    uint32_t ramdisk_size; /* size in bytes */
    uint32_t ramdisk_addr; /* physical load addr */

    uint32_t second_size;  /* size in bytes */
    uint32_t second_addr;  /* physical load addr */

    uint32_t tags_addr;    /* physical addr for kernel tags */
    uint32_t page_size;    /* flash page size we assume */
    uint32_t unused;
    uint32_t os_version;
    uint8_t name[BOOT_NAME_SIZE]; /* asciiz product name */
    uint8_t cmdline[BOOT_ARGS_SIZE];
    uint32_t id[8]; /* timestamp / checksum / sha1 / etc */
    uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE];
};
 

如果设备搭载 Android 9 之前的版本且使用旧版启动映像标头,则会被视为使用启动映像标头版本 0。所有搭载 Android 9 的设备都必须使用以下启动映像标头结构,同时标头版本设为 1。

struct boot_img_hdr
{
    uint8_t magic[BOOT_MAGIC_SIZE];
    uint32_t kernel_size;  /* size in bytes */
    uint32_t kernel_addr;  /* physical load addr */

    uint32_t ramdisk_size; /* size in bytes */
    uint32_t ramdisk_addr; /* physical load addr */

    uint32_t second_size;  /* size in bytes */
    uint32_t second_addr;  /* physical load addr */

    uint32_t tags_addr;    /* physical addr for kernel tags */
    uint32_t page_size;    /* flash page size we assume */
    uint32_t header_version;
    uint32_t os_version;
    uint8_t name[BOOT_NAME_SIZE]; /* asciiz product name */
    uint8_t cmdline[BOOT_ARGS_SIZE];
    uint32_t id[8]; /* timestamp / checksum / sha1 / etc */
    uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE];
    uint32_t recovery_dtbo_size;   /* size of recovery dtbo image */
    uint64_t recovery_dtbo_offset; /* offset in boot image */
    uint32_t header_size;   /* size of boot image header in bytes */
};
 

header_size 字段包含启动映像标头大小。如果启动映像标头版本设为 1,则除了内核、ramdisk 和 second 部分之外,ID 字段还包含启动映像 recovery_dtbo 部分的 SHA1 摘要。要详细了解 recovery_dtbo_size 和 recovery_dtbo_offset 字段,请参阅在非 A/B 设备的恢复映像中添加 DTBO

实现

用于创建启动映像的 mkbootimg 工具添加了以下参数,以支持新的启动映像标头:

参数说明
header_version 设置启动映像标头版本。
recovery_dtbo 要添加到恢复映像的恢复 DTBO 映像的路径。

设备 BoardConfig.mk 使用 BOARD_MKBOOTIMG_ARGS 配置,以便将 header version 添加到 mkbootimg 的其他专门针对主板的参数。例如:

  BOARD_MKBOOTIMG_ARGS := --ramdisk_offset $(BOARD_RAMDISK_OFFSET) --tags_offset $(BOARD_KERNEL_TAGS_OFFSET) --header_version $(BOARD_BOOTIMG_HEADER_VERSION)
 

Android 编译系统使用 BoardConfig 变量 BOARD_PREBUILT_DTBOIMAGE,以便在创建恢复映像期间设置 mkbootimg 工具的 recovery_dtbo 参数。

要详细了解 Android 开源项目 (AOSP) 的变化,请查看与启动映像标头版本编号相关的更改列表

验证

对于所有搭载 Android 9 的设备,供应商测试套件 (VTS) 都会检查启动/恢复映像的格式,以确保启动映像标头使用版本 1。

System-as-root

搭载 Android 9 的所有新设备都必须使用 system-as-root(BOARD_BUILD_SYSTEM_ROOT_IMAGE 必须为 true),它可以将 ramdisk.img 合并到 system.img,而后者会反过来再作为 rootfs 进行装载。对于要升级到 Android 9 的设备,使用 system-as-root 并非强制要求。本文档介绍了 system-as-root、列出了 dm-verity 支持的内核要求(包括所依赖的内核补丁程序),还提供了一些设置示例。

关于系统专用 OTA

当前 Android 生态系统支持两种类型的分区布局

  • 在 A/B 分区架构中,system 分区作为 rootfs 装载。
  • 在非 A/B 分区架构中,/boot 分区中的 ramdisk.img 会被加载到内存中(反过来再作为 rootfs 进行装载),而 system 分区则在 /system 中装载。

在 Android 8.0 中进行的架构更改(在 Project Treble 中)支持系统专用 OTA。在系统专用 OTA 中,可以在不更改其他分区的情况下跨主要 Android 版本更新 system.img。不过,对于非 A/B 设备来说,由于 ramdisk.img 位于 /boot 分区中,因此它无法使用 Android 8.x 架构通过系统专用 OTA 进行更新。这样一来,旧的 ramdisk.img可能不适用于新的 system.img,具体原因如下:

  • ramdisk.img 中较旧的 /init 可能无法解析 /system 上的 *.rc 文件。
  • ramdisk 包含 /init.rc,它也可能已过期(相较于新 /system 所要求的)。

为确保系统专用 OTA 按预期运行,Android 9 中必须使用 system-as-root。非 A/B 设备必须从 ramdisk 分区布局切换到 system-as-root 分区布局;A/B 设备已被要求使用 system-as-root,因此无需做出改动。

关于 A/B 设备和非 A/B 设备

A/B 设备和非 A/B 设备的分区详情如下:

A/B 设备非 A/B 设备
每个分区(userdata 除外)都包含两个副本(插槽):
  • /boot_a
  • /boot_b
  • /system_a
  • /system_b
  • /vendor_a
  • /vendor_b
每个分区都包含一个副本,无其他备份分区。
  • /boot
  • /system
  • /vendor

要详细了解 A/B 设备和非 A/B 设备,请参阅 A/B(无缝)系统更新

关于 system-as-root

在 Android 9 中,非 A/B 设备应采用 system-as-root,以便通过系统专用 OTA 进行更新。

注意:如果设备使用的是 A/B 分区架构,则无需做出任何改动。

不同于将 /boot 改为 recovery 分区的 A/B 设备,非 A/B 设备由于没有后备插槽分区(例如,从 boot_a 到 boot_b),必须保留单独的 /recovery 分区。如果在非 A/B 设备上移除 /recovery 并使其与 A/B 架构类似,那么在 /boot 分区更新失败时,恢复模式可能会遭到破坏。因此,在非 A/B 设备上,必须将 /recovery 分区与 /boot 分区分开,这意味着将继续延迟更新恢复映像(即和搭载 Android 9 之前版本的设备一样)。

非 A/B 设备在使用 Android 9 前后的分区布局差异:

组件映像ramdisk(9 之前)system-as-root(9 之后)
映像内容 boot.img 包含内核和 ramdisk.img:

ramdisk.img
  -/
    - init.rc
    - init
    - etc -> /system/etc
    - system/ (mount point)
    - vendor/ (mount point)
    - odm/ (mount point)
    ...
 
仅包含正常启动内核。
recovery.img 包含恢复内核和 recovery-ramdisk.img。
system.img 包含以下内容:

system.img
  -/
    - bin/
    - etc
    - vendor -> /vendor
    - ...
 
包含原始 system.img 和 ramdisk.img 的合并内容:

system.img
  -/
    - init.rc
    - init
    - etc -> /system/etc
    - system/
      - bin/
      - etc/
      - vendor -> /vendor
      - ...
    - vendor/ (mount point)
    - odm/ (mount point)
    ...
 
分区布局
  1. /boot
  2. /system
  3. /recovery
  4. /vendor 等
  1. /boot
  2. /system
  3. /recovery
  4. /vendor 等

设置 dm-verity

在 system-as-root 中,内核必须使用 dm-verity 在 /(装载点)下装载 system.img。AOSP 支持 system.img 的下列 dm-verity 实现:

  1. 对于 vboot 1.0,内核必须在 /system 上解析 Android 专用元数据,然后转换为 dm-verity 参数以设置 dm-verity。需要这些内核补丁程序
  2. 对于 vboot 2.0 (AVB),引导加载程序必须先整合 external/avb/libavb,然后 external/avb/libavb 会解析 /system 的哈希树描述符,再将解析结果转换为 dm-verity 参数,最后通过内核命令行将这些参数传递给内核。(/system 的哈希树描述符可能位于 /vbmeta 或 /system 本身上)。

    需要下列内核补丁程序:

下面是来自真实设备的示例,显示的是内核命令行中 system-as-root 的 dm-verity 相关设置:

vboot 1.0

ro root=/dev/dm-0 rootwait skip_initramfs init=/init
dm="system none ro,0 1 android-verity /dev/sda34"
veritykeyid=id:7e4333f9bba00adfe0ede979e28ed1920492b40f
 

vboot 2.0 (AVB)

ro root=/dev/dm-0 rootwait  skip_initramfs init=/init

dm="1 vroot none ro 1,0 5159992 verity 1
PARTUUID=00000016-0000-0000-0000-000000000000
PARTUUID=00000016-0000-0000-0000-000000000000 4096 4096 644999 644999
sha1 d80b4a8be3b58a8ab86fad1b498640892d4843a2
8d08feed2f55c418fb63447fec0d32b1b107e42c 10 restart_on_corruption
ignore_zero_blocks use_fec_from_device
PARTUUID=00000016-0000-0000-0000-000000000000 fec_roots 2 fec_blocks
650080 fec_start 650080"
 
 

特定于设备的根文件夹

使用 system-as-root 时,在设备上刷写常规系统映像 (GSI) 之后(以及在运行供应商测试套件测试之前),任何通过 BOARD_ROOT_EXTRA_FOLDERS 添加的特定于设备的根文件夹都会消失,因为整个根目录内容已被 system-as-root GSI 取代。如果对特定于设备的根文件夹有依赖性(例如将此类文件夹用作装载点),则移除这些文件夹可能会导致设备无法启动。

要避免出现此问题,请不要使用 BOARD_ROOT_EXTRA_FOLDERS 来添加特定于设备的根文件夹(此类文件夹将来可能会被弃用)。如果需要指定特定于设备的装载点,请使用 /mnt/vendor/<mount point>(已添加到这些更改列表中)。这些特定于供应商的装载点可在 fstab 设备树(适用于第一阶段的装载)和 /vendor/etc/fstab.{ro.hardware} 文件中直接指定,而无需进行额外设置(因为 fs_mgr 将在 /mnt/vendor/* 下自动创建它们)。

分区和映像

分区

Android 设备包含若干个分区,这些分区在启动过程中发挥不同的作用。为了支持 A/B 更新,设备需要为 bootsystemvendor 和 radio 分区分别单独配置一个槽位。

  • bootboot 分区包含通过 mkbootimg 组合在一起的内核映像和 RAM 磁盘。为了直接刷写内核而不刷写新的 boot 分区,可以使用虚拟分区:
    • kernelkernel 虚拟分区仅覆盖内核(zImage、zImage-dtb、Image.gz-dtb),方法是写入新的映像来覆盖旧的映像。为此,它会确定 eMMC 中现有内核映像的起始位置并将新内核映像复制到该位置。请记住,新内核映像可能会大于现有内核映像。引导加载程序可以通过移动其后的任何数据来腾出空间或放弃出错的操作。如果提供的开发内核不兼容,则可能需要使用相关的内核模块更新 dtb 分区(如果存在)、vendor 分区或 system 分区。
    • ramdiskramdisk 虚拟分区通过将新映像写入旧磁盘来仅覆盖 RAM 磁盘。为此,它会确定 eMMC 中现有 ramdisk.img 的起始位置并将新 RAM 磁盘映像复制到该位置。请记住,新 RAM 磁盘映像可能会大于现有 RAM 磁盘映像。引导加载程序可以通过移动其后的任何数据来腾出空间或放弃出错的操作。
  • systemsystem 分区主要包含 Android 框架。
  • recoveryrecovery 分区用于存储在 OTA 过程中启动的恢复映像。如果设备支持 A/B 更新,则恢复映像可以是启动映像中包含的 RAM 磁盘,而不是单独的映像。
  • cachecache 分区用于存储临时数据,如果设备使用 A/B 更新,则可以不要此分区。cache 分区不需要可从引导加载程序写入,而只需要可清空。大小取决于设备类型和 userdata 分区的可用空间。目前,50MB 至 100MB 应该没问题。
  • miscmisc 分区供恢复映像使用,存储空间不能小于 4KB。
  • userdatauserdata 分区包含用户安装的应用和数据,包括自定义数据。
  • metadata:如果设备被加密,则需要使用 metadata 分区,该分区的存储空间不能小于 16MB。
  • vendorvendor 分区包含所有不可分发给 Android 开源项目 (AOSP) 的二进制文件。如果没有专有信息,则可以省略此分区。
  • radioradio 分区包含无线装置映像。只有包含无线装置且在专用分区中包含无线装置专用软件的设备才需要此分区。
  • tostos 分区用于存储 Trusty 操作系统的二进制映像文件,仅在设备包含 Trusty 时使用。

流程

引导加载程序的运作方式如下:

  1. 首先加载引导加载程序。
  2. 引导加载程序初始化内存。
  3. 如果使用 A/B 更新,则确定要启动的当前槽位。
  4. 确定是否应按照支持更新中所述改为启动恢复模式。
  5. 引导加载程序加载映像,其中包含内核和 RAM 磁盘(在 Treble 中,包含更多部分)。
  6. 引导加载程序开始将内核作为可自行执行的压缩二进制文件加载到内存中。
  7. 内核将自身解压缩并开始执行到内存中。
  8. 自此,旧版设备从 RAM 磁盘加载 init,而新版设备从 /system 分区加载它。
  9. init 从 /system 分区中启动并开始装载其他所有分区,如 /vendor/oem 和 /odm,然后开始执行代码以启动设备

映像

引导加载程序依赖于下面这些映像。

内核映像

内核映像以标准 Linux 格式创建,如 zImage、Image 或 Image.gz。内核映像可以单独刷写,也可以与 RAM 磁盘映像相结合,可以刷写到 boot 分区,也可以从内存启动。在创建内核映像时,建议使用连接的设备树二进制文件,而不是为设备树使用一个单独的分区。为不同的电路板修订版本使用多个设备树 Blob (DTB) 时,应按电路板修订版本降序连接多个 DTB。

RAM 磁盘映像

RAM 磁盘应包含适合作为 rootfs 装载的根文件系统。RAM 磁盘映像先通过 mkbootfs 与内核映像组合在一起,然后再刷写到 boot 分区中。

启动映像

启动映像应包含通过未经修改的 mkbootimg 组合在一起的内核和 RAM 磁盘。

您可以在以下位置找到 mkbootimg 实现:system/core/mkbootimg

引导加载程序会读取由 mkbootimg 生成的 bootimg.h 头文件,并更新内核标头,使其包含被刷写的 RAM 磁盘的正确位置和大小、内核基址、命令行参数以及其他内容。然后,引导加载程序会将启动映像中指定的命令行附加到引导加载程序生成的命令行的末尾。

文件系统映像(系统、用户数据和恢复映像)

YAFFS2 映像格式

如果使用原始 NAND 存储空间,则这些映像必须是由未经修改的 mkyaffs2image 生成的 YAFFS2(可以在 Android 开源项目 (AOSP) 的 external/yaffs2/yaffs2/utils 中找到)。它们采用以下格式:


| 2k bytes of data| yaffs extra data | padding | | 0  2048 | 0 64 | variable|

 

引导加载程序负责使用这些映像,并将 YAFFS 额外数据转移到给定 NAND 硬件的带外区域中的适当位置。如果需要软件 ECC,则引导加载程序还应在此时执行相应的计算。

稀疏映像格式

应支持稀疏映像格式。“ext4 压缩映像”文档以及 system/core/libsparse/sparse_format.h 中都对这种格式进行了说明,该格式的实现位置为:system/core/libsparse/sparse_read.cpp

如果使用基于块的存储设备,则应支持 ext4 或 f2fs。要快速传输并刷写大型空 ext4 文件系统 (userdata),应以稀疏格式存储映像,映像中包含有关文件系统的哪些区域可以保留不写的信息。文件格式由 mke2fs 实用程序编写,该实用程序还用于创建文件格式由引导加载程序读取和刷写的映像。有关属性,请参见下面几部分:

 

文件格式
  • 所有字段都采用无符号的小端字节序
  • 文件包含一个文件标头,后跟一系列区块
  • 文件标头、区块标头和区块数据全部都是 4 个字节长的倍数
  • 32 位魔数:0xed26ff3a
  • 16 位 Major 版本 (0x1) - 拒绝 Major 版本更高的映像
  • 16 位 Minor 版本 (0x0) - 允许 Minor 版本更高的映像
  • 以字节为单位的 16 位文件标头大小(在 v1.0 中为 28 位)
  • 以字节为单位的 16 位区块标头大小(在 v1.0 中为 12 位)
  • 以字节为单位的 32 位块大小,必须是 4 的倍数
  • 输出文件中的 32 位块总大小
  • 输入文件中的 32 位区块总大小

原始数据的 32 位 CRC32 校验和(将“随意”算作 0 标准 802.3 多项式)使用公开域表格实现

区块
  • 16 位区块类型:
    • 0xCAC1 原始
    • 0xCAC2 填充
    • 0xCAC3 随意
  • 16 位保留(写入为 0,读取时忽略)
  • 输出映像中以块为单位的 32 位区块大小
  • 区块输入文件中以字节为单位的 32 位总大小(包括区块标头和数据)
数据
  • 对于原始类型,表示原始数据,即以块为单位的大小 * 以字节为单位的块大小
  • 对于填充类型,表示 4 字节的填充数据
实现写入程序

mke2fs 实用程序已经知道需要写入映像的哪些区域,并且会在它们之间编码“随意”区块。另一个工具 img2simg会将常规(非稀疏)映像转换为稀疏映像。常规映像没有关于“随意”区域的信息,转换过程至多能做到的是查找重复数据块,以减小所生成映像的大小。

实现读取程序

读取程序应拒绝 Major 版本未知的映像,但应接受 Minor 版本未知的映像。读取程序可能会拒绝区块大小不受其支持的映像。

Major 版本经过验证后,读取程序应忽略具有未知类型字段的区块。它应使用“文件中的区块大小”跳过文件中的区块,并跳过输出中的“以块为单位的区块大小”块。

应针对将要写入磁盘的数据计算循环冗余校验 802.3 CRC32。任何未写入的区域(随意区域或跳过的区块)都应在 CRC 中算作 0。应将写入或跳过的总块数与标头中的“总块数”字段进行比较。simg2img 工具会将稀疏映像格式转换为标准映像,这样会丢失稀疏信息。

构建 product 分区

Android 9 及更高版本支持使用 Android 编译系统构建 /product 分区。之前,Android 8.x 强制将 SoC 专属组件从 /system 分区分隔到了 /vendor 分区,不会为从 Android 编译系统构建的原始设备制造商 (OEM) 专属组件提供专用空间。Android 9 及更高版本提供了适用于不同分区上的特权应用的其他权限和列入白名单功能

product 分区简介

许多 OEM 会自定义 AOSP 系统映像,以实现自己的功能并满足运营商的要求。不过,如果进行这类自定义,则无法针对多个软件 SKU 使用单个系统映像。映像必须各不相同,才能支持不同的语言区域、运营商等自定义。如果使用单独的 /product 分区来包含自定义项,则可以针对多个软件 SKU 使用单个系统映像。(/system 分区会托管可在众多软件 SKU 之间共享的通用代码)。/vendor 分区会继续托管 SoC 专属的板级 (BSP) 代码,这类代码可以基于指定 SoC 在多台设备之间共享。

使用单独的分区存在一些弊端,例如,难以管理磁盘空间(应该预留一定的空间满足未来增长的空间需求),以及难以在各分区之间维护稳定的应用二进制接口 (ABI)。在决定使用 /product 分区之前,请花些时间考虑一下您的 AOSP 实现的具体情况和可行的缓解策略(例如,在无线下载 (OTA) 更新期间对设备进行重新分区;此操作不是由 Google 来完成,而是由某些 OEM 来完成)。

product 分区和权限

在 Android 9 及更高版本中,权限和列入白名单过程的更改会影响您在 product 分区上授予特权应用权限的方式。permissions.xml 文件必须与特权应用位于同一个分区中。在特权应用 /system 分区中放置 permissions.xml 文件不会将这些权限扩展到 /product 分区中的特权应用,即使前者是后者的扩展也不例外。有关权限和列入白名单过程的详细信息,请参阅特许权限白名单

旧式 /oem 与 /product

新 /product 分区与旧式 /oem 分区不同:

分区属性
/oem
  • 不可更新;通常在出厂时刷写一次。
  • 根据品牌信息和颜色等细微差异进行构建。具有不同的 /oem 分区内容并不意味着产品软件是不同的。
  • /system 分区不依赖于 /oem 分区。(仅当在其中找到特定文件时才使用 /oem 分区)。
  • 仅在 /system 分区上使用公共 API。
/product
  • 可更新
  • 搭配系统图像(该分区与系统映像一起更新)
  • 按产品或产品系列构建。
  • 系统分区可以依赖于 /product 分区。
  • 可以使用非公共 API,因为它们同时更新。

出于这些原因,Android 9 支持新的 /product 分区,同时针对依赖于旧式 /oem 分区的设备保留了对该分区的支持。

/product 组件

/product 分区包含以下组件:

  • 产品专用的系统属性 (/product/build.prop)
  • 产品专用的 RRO (/product/overlay/*.apk)
  • 产品专用的应用 (/product/app/*.apk)
  • 产品专用的特权应用 (/product/priv-app/*.apk)
  • 产品专用的内容库 (/product/lib/*)
  • 产品专用的 Java 库 (/product/framework/*.jar)
  • 产品专用的 Android 框架系统配置(/product/etc/sysconfig/* 和 /product/etc/permissions/*
  • 产品专用的媒体文件 (/product/media/audio/*)
  • 产品专用的 bootanimation 文件

不得使用 custom_images

您不能使用 custom_images。它们缺乏对以下方面的支持:

  • 将模块安装到特定目标分区中。 custom_images 支持将软件工件复制到映像中,但无法将模块安装到特定分区中(通过将其目标分区指定为编译规则的一部分)。
  • Soong 支持。无法使用 Soong 编译系统编译 custom_images
  • OTA 更新支持custom_images 用作出厂 ROM 映像,无法接收 OTA 更新。

维护分区之间的 ABI

Android 9 中的 /product 分区是 /system 分区的扩展。由于 /product 和 /system 分区之间的 ABI 稳定性较弱,因此必须同时升级这两者,而且 ABI 应基于系统 SDK。如果系统 SDK 不涵盖 /product 和 /system 之间的所有 API 表面,则 OEM 必须在这两个分区之间维护自己的 ABI。

/product 分区和 /system 分区可以相互依赖。不过,在没有 /product 分区的情况下,对通用系统映像 (GSI)的测试必须能够正常运行。

/product 分区不能对 /vendor 分区有任何依赖。/product 和 /vendor 分区之间不允许有任何直接交互。(这一规则将通过 SEpolicy 强制实施。)

实现 product 分区

在实现新 product 分区之前,请先了解 AOSP 中的相关 product 分区变化。接下来,为了设置 /product,请添加以下开发板编译标记或 product 编译标记:

  • BOARD_USES_PRODUCTIMAGE
  • BOARD_PRODUCTIMAGE_PARTITION_SIZE
  • BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE
  • PRODUCT_PRODUCT_PROPERTIES for /product/build.prop。这些必须在 $(call inherit-product path/to/device.mk) 内,例如 PRODUCT_PRODUCT_PROPERTIES += product.abc=ok

向 product 分区中安装模块

使用以下编译标记向 product 分区中安装模块。

  • Android.bp 中的 product_specific: true
  • Android.mk 中的 LOCAL_PRODUCT_MODULE := true

启用验证启动

为防止 /product 分区被恶意软件篡改,您应该为该分区启用 Android 启动时验证 (AVB)(就像为 /vendor/ 和 /system 分区启用一样)。要启用 AVB,请添加以下编译标记:BOARD_AVB_PRODUCT_ADD_HASHTREE_FOOTER_ARGS

 

构建 ODM 分区

Android 10 支持使用 Android 构建系统构建 /odm 分区。

ODM 分区简介

原始设计制造商 (ODM) 能够为其特定设备(开发板)自定义系统芯片 (SoC) 供应商板级支持包 (BSP)。这样,他们就可以为板级组件、板级守护进程或者其基于硬件抽象层 (HAL) 的自有功能实现内核模块。他们可能还需要替换或自定义 SoC 组件。

在之前的 Android 版本中,对于使用相同 SoC(或使用同一系列中的不同 SoC)的设备,此类自定义会阻止使用单个供应商映像。在 Android 10 中,您可以为自定义使用单独的 /odm 分区,因而能够针对多个硬件 SKU 使用单个供应商映像。

使用产品分区和 ODM 分区

Android 9 添加了对构建 /product 分区的支持,让您可以针对由不同 product.img 映像提供的多个软件 SKU 使用单个系统映像。/product 分区适用于软件 SKU,而 /odm 分区适用于硬件 SKU。

有了专用的产品分区和 ODM 分区,您可以使用 /system 分区来托管通用代码(这类代码在许多软件 SKU 之间共享),以及使用 /vendor 分区来托管 SoC 专属 BSP 代码(这类代码基于指定 SoC 在多台设备之间共享)。

使用单独的分区存在一些弊端,例如,难以管理磁盘空间(您必须预留一定的空间满足未来增长的空间需求)。但是,Android 10 对动态分区的支持解决了磁盘空间问题,并且让您可以在无线下载 (OTA) 更新期间对设备进行重新分区。

/odm 组件

/odm 分区包含以下 ODM 专用组件(类似于 /vendor 分区),如下表所示。

ODM 专用组件位置
可加载内核模块 (LKM) /odm/lib/modules/*.ko
原生库 /odm/lib[64]
HAL /odm/lib[64]/hw
SEPolicy /odm/etc/selinux
VINTF 对象数据 /odm/etc/vintf
init.rc 文件 /odm/etc/init
系统属性 /odm/build.prop
运行时资源叠加层 (RRO) /odm/overlay/*.apk
应用 /odm/app/*.apk
特权应用 /odm/priv-app/*.apk
Java 库 /odm/framework/*.jar
Android 框架系统配置 /odm/etc/sysconfig/* 和 /odm/etc/permissions/*

 

不得使用 custom_images

请勿使用 custom images,因为它们缺乏对以下方面的支持:

  • 将模块安装到特定目标分区中。custom_images 支持将软件工件复制到映像中,但无法通过将目标分区指定为构建规则的一部分,来将模块安装到特定分区中。
  • Soong。 无法使用 Soong 构建系统构建 custom_images
  • OTA 更新。custom_images 用作出厂 ROM 映像,无法执行 OTA 更新。

维护分区之间的 ABI

/odm 分区是 /vendor 分区的扩展。在考虑应用二进制接口 (ABI) 稳定性时,请记住以下架构:

维护分区之间的 ABI图 1. 维护分区之间的 ABI
  • /odm 和 /vendor 分区之间不具有 ABI 稳定性。必须同时升级这两个分区。
  • /odm 和 /vendor 分区可以相互依赖,但是在没有 /odm 分区的情况下,/vendor 分区必须运行。
  • /odm 和 /system 之间的 ABI 与 /vendor 和 /system 之间的 ABI 相同。

/product 分区与 /vendor 或 /odm 分区之间不允许有任何直接交互。(这一规则将由 SEpolicy 强制执行。)

实现 ODM 分区

在实现新分区之前,请先了解相关 AOSP 变化

设置 ODM 分区

要设置 /odm 分区,请添加以下构建标记:

  • BOARD_ODMIMAGE_PARTITION_SIZE(适用于固定分区大小)
  • PRODUCT_USE_DYNAMIC_PARTITIONS 和 BOARD_ODMIMAGE_PARTITION_RESERVED_SIZE(适用于动态分区大小)
  • BOARD_ODMIMAGE_FILE_SYSTEM_TYPE 文件系统类型(用于 ODM 映像)
  • PRODUCT_ODM_PROPERTIES(适用于 /odm/build.prop
    在 $(call inherit-product path/to/device.mk) 中使用该标记,例如 PRODUCT_ODM_PROPERTIES += product.abc=ok

向 ODM 分区中安装模块

使用以下构建标记向 /odm 分区中安装模块:

  • Android.bp 中的 device_specific: true
  • Android.mk 中的 LOCAL_ODM_MODULE := true

启用启动时验证

要防止恶意软件篡改 /odm 分区,请为这些分区启用 Android 启动时验证 (AVB)(就像为 /vendor 和 /system分区启用一样)。

要启用 AVB,请添加构建标记 BOARD_AVB_ODM_ADD_HASHTREE_FOOTER_ARGS。要详细了解如何在动态分区上配置 AVB,请参阅 AVB 配置更改

将 /odm 作为另一个 /vendor 分区处理

要确保系统将 /odm 分区作为 /vendor 分区处理,请将所有硬编码的 /vendor 引用替换为一组面向硬件的分区(当前为 /odm 和 /vendor)。平台中值得注意的 /vendor 引用位置包括动态链接器软件包管理器和 shell/libc

 

非 A/B 设备的恢复映像

为了防止非 A/B 设备上出现无线下载 (OTA) 失败的情况,恢复分区必须“自给自足”,不得依赖于其他分区。设备制造商可以使用设备树高级配置与电源接口 (ACPI) 描述所有无法检测到的设备。

启动到恢复模式时,引导加载程序必须加载与恢复映像兼容的设备树 Blob 叠加层 (DTBO) 或高级配置与电源接口叠加层 (ACPIO) 映像(叠加层映像)。在 OTA 更新期间,如果在叠加层映像更新后(但在完成全部更新之前)出现问题,则设备会尝试启动到恢复模式,以完成 OTA 更新。不过,由于叠加层分区已更新,恢复映像(尚未更新)可能会出现不匹配的情况。

为防止出现这种情况,在 Android 9 及更高版本中,恢复映像也必须包含来自叠加层映像的信息。非 A/B 设备的恢复映像还必须包含附加到内核的设备叠加层 Blob,以便在更新期间不依赖于叠加层分区。

Android 10 及更高版本支持使用 ACPI(而非 DTBO)的架构。

启动映像更改

要允许恢复映像包含恢复 DTBO 或 ACPIO,Android 9 及更高版本中启动映像的格式应如下所示:

启动头文件(1 页)
内核(l 页)
Ramdisk(m 页)
第二阶段(n 页)
恢复 DTBO(o 页)

此外,用于创建启动映像的 mkbootimg 工具包含下列参数,以支持这些叠加层。

参数说明
header_version 设置启动映像头文件版本。头文件版本高于或等于 1 的启动映像支持恢复 DTBO 部分。
recovery_dtbo 恢复 DTBO 映像的路径。
recovery_acpio 恢复 ACPIO 映像的路径。

如需详细了解对旧版启动映像头文件的改动,请参阅启动映像头文件版本编号

DTBO 实现

虽然搭载 Android 9 及更高版本的所有设备都必须使用新的启动映像头文件(版本 1),但只有非 A/B 设备才必须填充恢复映像的 recovery_dtbo 部分。要在 BoardConfig.mk 设备的 recovery.img 中添加 recovery_dtbo映像,请执行以下操作:

  • 将 BOARD_INCLUDE_RECOVERY_DTBO 配置设置为 true
    BOARD_INCLUDE_RECOVERY_DTBO := true
     
  • 扩展 BOARD_MKBOOTIMG_ARGS 变量以指定启动映像头文件版本:
          BOARD_MKBOOTIMG_ARGS := --ramdisk_offset $(BOARD_RAMDISK_OFFSET) --tags_offset $(BOARD_KERNEL_TAGS_OFFSET) --header_version $(BOARD_BOOTIMG_HEADER_VERSION)
     
  • 确保将 BOARD_PREBUILT_DTBOIMAGE 变量设置为 DTBO 映像的路径。Android 编译系统使用该变量在创建恢复映像期间设置 mkbootimg 工具的 recovery_dtbo 参数。
  • 如果变量 BOARD_INCLUDE_RECOVERY_DTBOBOARD_MKBOOTIMG_ARGS 和 BOARD_PREBUILT_DTBOIMAGE均正确设置,Android 编译系统会将变量 BOARD_PREBUILT_DTBOIMAGE 指定的 DTBO 添加到 recovery.img 中。

ACPIO 实现

虽然搭载 Android 10 及更高版本的所有设备都必须使用新的启动映像头文件(版本 1),但只有非 A/B 设备才必须填充恢复映像的 recovery_acpio 部分。要在 BoardConfig.mk 设备的 recovery.img 中添加 recovery_acpio 映像,请执行以下操作:

  • 将 BOARD_INCLUDE_RECOVERY_ACPIO 配置设置为 true
    BOARD_INCLUDE_RECOVERY_ACPIO := true
     
  • 扩展 BOARD_MKBOOTIMG_ARGS 变量以指定启动映像头文件版本。变量必须大于或等于 1,才能支持恢复 ACPIO。
    BOARD_MKBOOTIMG_ARGS += --header_version $(BOARD_BOOTIMG_HEADER_VERSION)
     
  • 确保将 BOARD_RECOVERY_ACPIO 变量设置为 ACPIO 映像的路径。Android 编译系统使用该变量在创建恢复映像期间设置 mkbootimg 工具的 recovery_acpio 参数。
  • 如果变量 BOARD_INCLUDE_RECOVERY_ACPIOBOARD_MKBOOTIMG_ARGS 和 BOARD_RECOVERY_ACPIO 均正确设置,Android 编译系统会将变量 BOARD_RECOVERY_ACPIO 指定的 ACPIO 添加到 recovery.img 中。

验证

对于搭载 Android 9 及更高版本的所有设备,供应商测试套件 (VTS) 会检查启动/恢复映像的格式,以确保启动映像头文件使用版本 1。

 

刷写、启动和更新

刷写映像

除非主机 fastboot 工具先发送 erase 命令,否则 flash 命令不得清空分区。这样的话,便可以使用多个以“跳过”块开始的稀疏映像刷写非常庞大的分区(包含多个较小的区块),以寻找并跳过已经写入的区域。实时创建这些映像的任务已由 fastboot 主机端工具处理。

在解锁模式下刷写之前,应该对无线装置和引导加载程序映像进行健全性检查。例如,与从细分版本创建的 android-info.txt 进行比较并确认版本匹配。此外,还应在刷写时检查引导加载程序映像签名,以确保它在启动(可能包括防回滚功能)过程中能够通过验证。

在 Google 品牌设备上,刷写到较低版本的引导加载程序应该能够正常工作,从首款以商业形式推出的引导加载程序开始支持,理想情况下,也能支持更早的版本。

启动:内核命令行

内核命令行应该从以下位置连接在一起:

  • 引导加载程序命令行:由引导加载程序确定的一组静态和动态参数
  • 设备树:从 chosen/bootargs 节点
  • defconfig:从 CONFIG_CMDLINE
  • boot.img:从命令行(对于偏移和大小,请参阅 system/core/mkbootimg/bootimg.h
  • 通过 PMIC(电源管理集成电路)、其他硬件资源和重新启动魔数参数 (LINUX_REBOOT_CMD_RESTART2) 消息传递确定且遵循 Android 兼容性定义文档的规范性重新启动或关闭原因,会记录为:androidboot.bootreason=<reason>

启动:设备树/设备树叠加层

为了支持各种配置,引导加载程序可以识别自己在其中运行的硬件/产品版本,并加载一组正确的设备树叠加层。

支持随机生成内核地址空间布局

为了支持随机生成加载内核映像的虚拟地址(由内核配置 RANDOMIZE_BASE 启用),引导加载程序需要通过在 DT 节点 /chosen/kaslr-seed 中传递一个随机的 u64 值来提供熵。

实现验证启动

请参阅验证启动

支持更新

要支持 Google 无线 (GOTA) 更新流程,必须存在恢复 RAM 磁盘。

如果使用标准 AOSP 恢复映像,那么在启动过程中,引导加载程序应该读取 misc 分区上的前 32 个字节,如果相应的数据匹配,它将启动到恢复映像:“启动-恢复”。这样一来,可以继续执行任何待处理的恢复工作(例如,应用 OTA、执行数据移除等),直到成功完成为止。

如需详细了解刷写过程中恢复进程与引导加载程序进行通信时用到的块中的内容,请参阅 bootable/recovery/bootloader_message/bootloader_message.h

A/B 更新

对于给定设备,如果 OEM 选择支持 A/B 更新,则引导加载程序应满足以下条件:

  • 所有通过 OTA 更新的分区都应可以在主系统启动时更新,而不是通过恢复来更新。
  • 对于 A/B 更新,更新程序将查询启动控件 HAL,更新当前未使用的启动槽位,通过 HAL 更改活动槽位,并重新启动到更新后的操作系统。请参阅实现启动控件 HAL
  • 所有支持 A/B 的分区都会在其名称后面附加一个后缀。此后缀可区分属于引导加载程序中特定槽位的分区。对于每个这样的分区,都有一个相应的变量 has-slot:,其值为“yes”
  • 槽位按字母顺序命名为 a、b 和 c 等,与后缀为 _a、_b 和 _c 等的分区相对应。
  • 引导加载程序应通过以下某种方式通知操作系统启动了哪个槽位:
    • DT 属性:/firmware/android/slot_suffix 或:
    • 命令行属性:androidboot.slot_suffix
  • 引导加载程序应支持 boot_control HAL (hardware/libhardware/include/hardware/boot_control.h)。
  • 要在 A/B 下启动 /system,引导加载程序应在内核命令行上传递 ro root=/dev/[node] rootwait skip_initramfs init=/init。如果不传递 skip_initramfs,将启动到恢复模式。
  • slot-retry-count 将由启动控件 HAL 通过 setActiveBootSlot 回调或通过 fastboot set_active 命令重置为一个正值(通常为“3”)。
  • 修改属于某个槽位的分区时,引导加载程序会清除“成功启动”的分区,并为相应的槽位重置 try_count。
  • 引导加载程序还应确定要加载的槽位。有关决策流程的描述,请参见本部分中的示意图,一般步骤如下:
    1. 确定要尝试加载的槽位。不要尝试加载标记为“slot-unbootable”的槽位。此槽位应与 fastboot 返回的值一致,并且从现在开始称为当前槽位。
    2. 当前槽位是否未标记为 slot-successful 且 slot-retry-count = 0?
      将当前槽位标记为“slot-unbootable”,并选择一个未标记为“slot-unbootable”而是标记为“slot-successful”的不同槽位。此槽位现在是选定的槽位。如果没有当前槽位可用,则系统会启动到恢复模式或向用户显示一条有意义的错误消息。
    3. 选择相应的 boot.img,并在内核命令行上添加正确的 system 分区的路径。
    4. 如果不启动到恢复模式,请将 skip_initramfs 添加到内核命令行
    5. 填充 DT 或命令行 slot_suffix 参数
    6. 启动。如果未标记为“slot-successful”,请递减 slot-retry-count。引导加载程序槽位加载流程图 1. 引导加载程序槽位加载流程
  • fastboot 实用程序将确定在运行任何刷写命令时要刷写的分区:例如,fastboot flash system system.img 将首先查询 current-slot 变量,然后将结果与 system 连接在一起,生成应刷写的分区的名称(例如 system_a 或 system_b)。
  • 通过 fastboot set_active 或启动控件 HAL 的 setActiveBootSlot 设置当前槽位时,引导加载程序应更新当前槽位、清除 slot-unbootable、清除 slot-successful 并重置 retry-count。只能通过这些方法来清除 slot-unbootable
  • Android 框架负责从 HAL 调用 markBootSuccessful。引导加载程序绝不应将分区标记为已成功启动。

非 A/B 更新

非 A/B 更新设备应满足以下条件才能支持更新:

  • recovery 分区应包含能够从某些受支持的分区(cache 和 userdata)读取系统映像并将其写入 system 分区的映像。
  • 引导加载程序应支持直接重新启动到恢复模式。
  • 如果支持无线装置映像更新,则 recovery 分区也应能够刷写无线装置。这可通过以下两种方式来完成:
    • 引导加载程序刷写无线装置。在这种情况下,应该可以从 recovery 分区重新启动回引导加载程序以完成更新。
    • 恢复映像刷写无线装置。可以采用二进制库或实用程序的形式来提供此功能。
 

使用引导加载程序

解锁和 Trusty

建议

所有 Google 品牌设备都应设为可解锁,以便可以重新刷写上述所有分区。可以使用 fastboot flashing unlock 设置此解锁模式,设置后,此模式在系统重新启动后应保留。

除非 fastboot flashing get_unlock_ability 为“1”,否则设备应拒绝 fastboot flashing unlock 命令。如果 get_unlock_ability 为“0”,则用户需要启动进入主屏幕,然后依次转到“设置”>“系统”> 开发者选项菜单,并启用 OEM 解锁选项以将 unlock_ability 设置为“1”。该标记在重新启动后以及恢复出厂设置后应保持不变。

发送 fastboot flashing unlock 命令后,设备应提示用户,警告他们非官方映像可能会有问题。确认后,应恢复出厂设置,以防止未经授权的数据访问。即使引导加载程序无法正确重新格式化设备,也应将设备恢复出厂设置。只有在恢复出厂设置后,才能设置持久性标记,以便重新刷写设备。

fastboot flashing lock 命令会重新锁定设备并使其恢复出厂设置,以便将来尝试刷写/解锁设备时需要再次恢复出厂设置。

所有尚未覆盖的 RAM 都应在 fastboot flashing unlock 过程中被重置。此措施可防止出现读取上次启动的剩余 RAM 内容这一攻击。同样,解锁的设备应在每次启动时清除 RAM(前提是这样做不会造成不可接受的延迟),但应保留供内核的 ramoops 使用的区域。

打算零售的设备应以锁定状态发货(并且 get_unlock_ability 返回“0”)。这是为了确保攻击者不能通过安装自己的系统或启动映像来损害设备。

属性

ro.oem_unlock_supported 属性应在编译时根据设备是否支持刷写解锁来设置。 如果设备不支持刷写解锁,应将 ro.oem_unlock_supported 设置为“0”;如果支持刷写解锁,应将其设置为“1”。

如果设备支持刷写解锁(即 ro.oem_unlock_supported = 1),则引导加载程序应通过将内核命令行变量 androidboot.flash.locked(或 /firmware/android/flash.locked DT 属性)设置为“1”(如果已锁定)或“0”(如果已解锁)来指示锁定状态。

注意:对于支持 dm-verity 的设备,您可以使用 ro.boot.verifiedbootstate 设置 ro.boot.flash.locked 的值(如果验证启动状态显示为橙色,则值为“0”,即已解锁)。

刷写锁定/解锁关键部分

设备应支持锁定和解锁关键部分。这些关键部分是指将设备启动到引导加载程序所需的任何部分,可能包括 fuse、传感器中枢的虚拟分区、第一阶段引导加载程序等等。

锁定关键部分是指防止设备上运行的任何代码(内核、恢复映像、OTA 代码等)故意修改任何关键部分。这意味着,如果设备处于锁定关键部分状态,则 OTA 应无法更新关键部分。从锁定状态转换为解锁状态应需要与设备进行物理交互。

该物理交互与 fastboot flashing unlock 产生的效果相似:用户必须按设备上的某些物理按钮。设计不应允许在没有进行物理交互的情况下以编程方式从 lock critical 状态转换为 unlock critical 状态。设备应以 unlock critical 状态发货。

关键分区/数据的定义

即运行设备所需的任何分区或数据,需要满足以下任一条件:

  • 可重新刷写 - 可重新编译、已提供或可通过某个 fastboot oem 命令提取
  • 受到全面保护(即,根据上一部分被视为关键部分)

包括每个设备的出厂特定设置、序列号、校准数据等。

关机模式充电

如果设备支持“关机模式充电”或在接通电源后自动启动到一种特殊模式,则 fastboot oem off-mode-charge 0应绕过这些特殊模式,并像用户按了电源按钮一样启动。

Trusty 的引导加载程序

Trusty 是 Google 可信执行环境 (TEE) 操作系统的实现,它与 Android 一起运行。此处列出的是使用 ARM TrustzoneTM 技术提供 TEE 的设备需遵循的规范。

如果将 Trusty 用作 ARM 设备上的安全操作系统解决方案,则应按照下面几个部分所述的内容实现引导加载程序。

初始化

要加载并初始化 Trusty 操作系统 (TOS),引导加载程序应执行以下操作:

  • 设置并配置所有可用的 RAM
  • 至少初始化一个串行端口
  • 验证 TOS 映像的签名
  • 将 TOS 加载到 RAM 中(不支持通过刷写或 TCM 执行)
  • 在设置状态和寄存器后,跳转到 TOS 映像中的第一条指令,如下一部分中所述

调用 TOS 映像

应在入口时配置以下状态:

  • 已关闭 MMU
  • 已刷写并关闭数据缓存(指令缓存可以开启或关闭)
  • 已停用所有中断(IRQ 和 FIQ)
  • 在 ARM v7 上,CPU 处于 SVC 模式;在 ARM v8 上,CPU 处于 EL3 模式
  • 寄存器处于以下状态:
    • r0/x0:分配给 TOS 的内存大小。
    • r1/x1:连续内存块(包含平台特有的启动参数)的物理地址。此块的布局特定于平台。
    • r2/x2:上述内存块的大小。
    • r14/x30:返回在 TOS 初始化后跳转到(在非安全模式下)的地址。

注意:r0-r3/x0-x3 也可充当 TOS 的擦写寄存器。返回时不会保留它们的值。

在 64 位平台上:

  • 只有 w0-w2 用于参数,所以 x0-x2 应仅包含 32 位值。
  • x30 可以包含一个 64 位值。
  • 将 x0 中的值添加到 TOS 入口点的基址时,应得出一个 32 位值。添加到 x1 中的启动参数块的地址时,寄存器 x2 中的大小也是如此。

从 TOS 返回

TOS 在完成初始化后会在非安全模式下(SCR.NS 设置为“1”)返回引导加载程序,以便引导加载程序可以继续加载主要操作系统(例如 Android)。

 

稳定的 AIDL

Android 10 添加了对稳定的 Android 接口定义语言 (AIDL) 的支持。稳定的 AIDL 是一种跟踪由 AIDL 接口提供的应用编程接口 (API)/应用二进制接口 (ABI) 的新方法。稳定的 AIDL 与 AIDL 的主要区别如下:

  • 在编译系统中使用 aidl_interfaces 定义接口。
  • 接口只能包含结构化数据。对于代表所需类型的 Parcelable,系统会根据其 AIDL 定义自动创建,并自动对其进行编组和解组。
  • 可以将接口声明为“稳定”接口(向后兼容)。声明之后,会在 AIDL 接口旁的一个文件中对这些接口的 API 进行跟踪和版本编号。

定义 AIDL 接口

aidl_interface 的定义如下:

aidl_interface {
    name: "my-module-name",
    local_include_dir: "tests_1",
    srcs: [
        "tests_1/some/package/IFoo.aidl",
        "tests_1/some/package/Thing.aidl",
    ],
    api_dir: "api/test-piece-1",
    versions: ["1"],
}
 
  • name:模块的名称。在这种情况下,系统会分别创建两个使用相应语言的存根库:my-module-name-java和 my-module-name-cpp。要防止创建 C++ 库,请使用 gen_cpp。这还会创建可用于检查和更新 API 的其他编译系统操作。
  • local_include_dir:指向软件包开始位置的路径。
  • srcs:编译为目标语言的稳定 AIDL 源文件的列表。
  • api_dir:转储接口的先前版本的 API 定义的路径,该路径用于确保捕捉到破坏 API 的接口更改(请参阅下面介绍的流程)。
  • versions:冻结在 api_dir 下的接口的先前版本。此参数为可选参数。

编写 AIDL 文件

稳定 AIDL 中的接口与传统接口相似,不同之处在于前者不允许使用非结构化的 Parcelable,因为这些 Parcelable 不稳定。稳定 AIDL 的最大不同就在于如何定义 Parcelable。以前,Parcelable 是前向声明的,而在稳定的 AIDL 中,Parcelable 字段和变量是显式定义的。

// in a file like \'some/package/Thing.aidl\'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}
 

现在支持 booleancharfloatdoublebyteintlong 和 String 的默认值(但不是必需的)。

使用存根库

将存根库作为依赖项添加到模块之后,您可以将这些库添加到您的文件中。下面是编译系统中的存根库的示例(Android.mk 也可用于旧版模块定义):

cc_... {
    name: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    static_libs: ["my-module-name-java"],
    ...
}
 

采用 C++ 语言的示例:

#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
    // use just like traditional AIDL
 

采用 Java 语言的示例:

import some.package.IFoo;
import some.package.Thing;
...
    // use just like traditional AIDL
 

对接口进行版本编号

如果声明一个名为 foo 的模块,则同时也会在编译系统中创建一个目标,您可以用该目标来管理该模块的 API。编译后,foo-freeze-api 会在 api_dir 下为接口的下一个版本添加新的 API 定义。

要保持接口的稳定性,您可以:

  • 在方法的末尾添加新方法(或添加具有显式定义的新序列的方法)
  • 在 Parcel 的末尾添加新元素(需要为每个元素添加一个默认值)

不允许执行其他操作。

新增的元接口方法

Android 10 针对稳定的 AIDL 推出了几种新的元接口方法。

查询远程对象的接口版本

客户端可以查询远程对象正在实现的接口版本,并将返回的版本与客户端正在使用的接口版本进行比较。

采用 C++ 语言的示例:

sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}
 

采用 Java 语言的示例:

IFoo foo = ... // the remote object
int my_ver = IFoo.VERSION;
int remote_ver = foo.getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}
 

对于 Java 语言,远程端必须实现 getInterfaceVersion(),如下所示:

class MyFoo extends IFoo.Stubs {
    @Override
    public final int getInterfaceVersion() { return IFoo.VERSION; }
}
 

这是因为所生成的类(IFooIFoo.Stubs 等)将在客户端和服务器之间共享(例如,这些类可以位于启动类路径下)。共享类时,服务器仍会链接到类的最新版本,即使该版本可能是用接口的旧版本编译的。如果该元接口是在共享类中实现的,则该接口始终会返回最新版本。不过,如果按照上述方式实现该方法,便会将接口的版本号嵌入到服务器的代码中(因为 IFoo.VERSION 是一个在引用时内嵌的 static final int),从而使该方法能够返回编译该服务器时所用的确切版本。

处理旧版接口

可能会存在以下情况:客户端使用的是较新的 AIDL 接口版本进行更新,而服务器使用的是旧版 AIDL 接口。在这种情况下,客户端便不能调用旧版接口中不存在的新方法。在使用稳定的 AIDL 之前,系统会静默忽略对此类不存在的方法的调用,客户端不会得知是否调用了该方法。

使用稳定的 AIDL,客户端将拥有更多的控制权。在客户端,您可以设置 AIDL 接口的默认实现。对于默认实现中的方法,只有当其没有在远程端实现时,才会被调用。这是因为该方法是使用旧版接口编译的。

采用 C++ 语言的示例:

class MyDefault : public IFooDefault {
  Status anAddedMethod(...) {
   // do something default
  }
};

// once per an interface in a process
IFoo::setDefaultImpl(std::unique_ptr<IFoo>(MyDefault));

foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                         // remote side is not implementing it
 

采用 Java 语言的示例:

IFoo.Stubs.setDefaultImpl(new IFoo.Default() {
    @Override
    public xxx anAddedMethod(...)  throws RemoteException {
        // do something default
    }
}); // once per an interface in a process

foo.anAddedMethod(...);
 
注意:在 Java 中,setDefaultImpl 包含在 Stubs 类中,而不包含在 interface 类中。

您无需为 AIDL 接口中的所有方法提供默认实现。您也不需要在默认 impl 类中替换一定会在远程端实现的方法(因为您确定远程端是在这些方法位于 AIDL 接口描述中时编译的)。

将现有的 AIDL 转换为结构化/稳定的 AIDL

如果您已经有一个 AIDL 接口以及使用该接口的代码,可以按照以下步骤将该接口转换为稳定的 AIDL 接口。

  1. 确定您的接口的所有依赖项。对于该接口所依赖的每个软件包,确定该软件包是否已在稳定的 AIDL 中定义。如果未定义,则必须转换该软件包。

  2. 将您的接口中的所有 Parcelable 全部转换为稳定的 Parcelable(接口文件本身可以保持不变)。可通过直接在 AIDL 文件中表示它们的结构来实现这一点。您必须重写管理类以使用这些新类型。可以在创建 aidl_interface 软件包(如下所示)之前完成重写。

  3. 创建 aidl_interface 软件包(如上所述),其中应包含模块的名称和依赖项以及您需要的任何其他信息。为使其保持稳定(不仅仅是结构化),您还需要指定 api_dir 路径。

将 Fastboot 移至用户空间

Android 10 通过将 fastboot 实现从引导加载程序转移至用户空间,添加了对可调整大小的分区的支持。经过重定位之后,便可将代码移动和刷写到可维护且可测试的公共位置,仅由硬件抽象层 (HAL) 来实现 fastboot 的供应商专属部分。

统一 fastboot 和 recovery

由于用户空间 fastboot 和 recovery 类似,因此您可以将它们合并为一个分区/二进制文件。这样做的优势包括从整体上减少所占用的空间以及使用的分区数,并使 fastboot 和 recovery 能够共享其内核和库。

要支持 fastbootd,引导加载程序必须实现一个新的启动控制块 (BCB) 命令:boot-fastboot。要进入 fastbootd 模式,引导加载程序应将 boot-fastboot 写入 BCB 消息的命令字段,并保持 BCB 的 recovery 字段不变(以重启中断的恢复任务)。statusstage 和 reserved 字段也保持不变。引导加载程序应该会在 BCB 命令中看到 boot-fastboot 时加载并启动到恢复映像。然后,recovery 会解析 BCB 消息并切换到 fastbootd 模式。

新 adb 命令

本部分介绍了集成 fastbootd 所需的另一个 adb 命令。该命令具有不同的行为,具体取决于是 system 还是 recovery 在执行该命令。

命令
reboot fastboot
  • 重新启动到 fastbootd (system)。
  • 直接进入 fastbootd,而不重新启动 (recovery)。

新 fastboot 命令

本部分介绍了集成 fastbootd 所需的其他 fastboot 命令,包括用于刷写和管理逻辑分区的新命令。某些命令具有不同的行为,具体取决于是引导加载程序还是 fastbootd 在执行这些命令。

命令
reboot recovery
  • 重新启动到 recovery(引导加载程序)。
  • 直接进入 recovery,而不重新启动 (fastbootd)。
reboot fastboot 重新启动到 fastbootd
getvar is-userspace
  • 返回“yes”(fastbootd)。
  • 返回“no”(引导加载程序)。
getvar is-logical:<partition> 如果指定分区是逻辑分区,则返回“yes”,否则返回“no”。逻辑分区支持下面列出的所有命令。
getvar super-partition-name 返回超级分区的名称。如果超级分区是 A/B 分区(通常并不是),则该名称包括当前槽后缀。
create-logical-partition <partition> <size> 创建具有指定名称和大小的逻辑分区。不得存在使用该名称的逻辑分区。
delete-logical-partition <partition> 删除指定的逻辑分区(有效擦除分区)。
resize-logical-partition <partition> <size> 将逻辑分区的大小调整为新大小,而不更改其内容。如果空间不足以执行调整大小操作,则会失败。
update-super <partition> 合并对超级分区元数据的更改。如果无法进行合并(例如,设备上的格式版本不受支持),则此命令将失败。可选的“擦除”参数会覆盖设备的元数据,而不是执行合并。

fastbootd 继续支持以下预先存在的 fastboot 命令。

命令
flash <partition> [ <filename> ] 将文件写入刷写分区。设备必须处于解锁状态。
erase <partition> 擦除分区(不一定是安全擦除)。设备必须处于解锁状态。
getvar <variable> | all 显示引导加载程序变量或所有变量。如果变量不存在,则返回错误。
set_active <slot>

将指定的 A/B 启动槽设置为 active。下次尝试启动时,system 将从指定的槽启动。

对于 A/B 支持,槽是指可以单独启动的重复分区集。槽的名称为 a、b 等,通过向分区名称添加后缀 _a、_b 等加以区分。

reboot 正常重新启动设备。
reboot-bootloader(或 reboot bootloader 将设备重新启动到引导加载程序。

对引导加载程序的修改

引导加载程序继续支持刷写引导加载程序、无线装置和启动/恢复分区,之后设备将启动到 fastboot(用户空间)并刷写所有其他分区。引导加载程序应支持以下命令。

命令
download 下载映像以进行刷写。
flash recovery <image>/ flash boot <image>/ flash bootloader <image>/ 刷写恢复/启动分区和引导加载程序。
reboot 重新启动设备。
reboot fastboot 重新启动到 fastboot。
reboot recovery 重新启动到 recovery。
getvar 获取刷写恢复/启动映像所需的引导加载程序变量(例如,current-slot 和 max-download-size)。
oem 由原始设备制造商 (OEM) 定义的命令。

引导加载程序不得允许刷写动态分区,并且必须返回错误:Partition should be flashed in fastbootd。对于改装的动态分区设备,fastboot 工具支持强制模式,以便在引导加载程序模式下直接刷写动态分区。引导加载程序可以支持此操作。例如,如果 system 是改装设备上的动态分区,则 fastboot --force flash system 允许引导加载程序刷写分区,而不是 fastbootd。此强制模式旨在灵活进行出厂刷写,不建议开发者使用。

Fastboot OEM HAL

要完全替换引导加载程序 fastboot,fastboot 必须处理所有现有的 fastboot 命令。其中很多命令都来自 OEM 并且记录下来,但需要自定义实现(很多命令也特定于 OEM,没有进行记录)。为处理此类命令,fastboot HAL 会指定所需的 OEM 命令,并允许 OEM 实现自己的命令。

fastboot HAL 的定义如下:

import IFastbootLogger;

/**
 * IFastboot interface implements vendor specific fastboot commands.
 */
interface IFastboot {
    /**
     * Returns a bool indicating whether the bootloader is enforcing verified
     * boot.
     *
     * @return verifiedBootState True if the bootloader is enforcing verified
     * boot and False otherwise.
     */
    isVerifiedBootEnabled() generates (bool verifiedBootState);

    /**
     * Returns a bool indicating the off-mode-charge setting. If off-mode
     * charging is enabled, the device autoboots into a special mode when
     * power is applied.
     *
     * @return offModeChargeState True if the setting is enabled and False if
     * not.
     */
    isOffModeChargeEnabled() generates (bool offModeChargeState);

    /**
     * Returns the minimum battery voltage required for flashing in mV.
     *
     * @return batteryVoltage Minimum battery voltage (in mV) required for
     * flashing to be successful.
     */
    getBatteryVoltageFlashingThreshold() generates (int32_t batteryVoltage);

    /**
     * Returns the file system type of the partition. This is only required for
     * physical partitions that need to be wiped and reformatted.
     *
     * @return type Can be ext4, f2fs or raw.
     * @return result SUCCESS if the operation is successful,
     * FAILURE_UNKNOWN if the partition is invalid or does not require
     * reformatting.
     */
    getPartitionType(string partitionName) generates (FileSystemType type, Result result);

    /**
     * Executes a fastboot OEM command.
     *
     * @param oemCmdArgs The oem command that is passed to the fastboot HAL.
     * @response result Returns the status SUCCESS if the operation is
     * successful,
     * INVALID_ARGUMENT for bad arguments,
     * FAILURE_UNKNOWN for an invalid/unsupported command.
     */
    doOemCommand(string oemCmd) generates (Result result);

};
 

启用 fastbootd

要在设备上启用 fastbootd,请执行以下操作:

  1. 将 fastbootd 添加到 device.mk 中的 PRODUCT_PACKAGESPRODUCT_PACKAGES += fastbootd

  2. 确保将 fastboot HAL、引导控制 HAL 和运行状况 HAL 打包为恢复映像的一部分。

  3. 添加 fastbootd 所需的任何设备专属 sepolicy 权限。例如,fastbootd 需要对设备专属分区进行写入访问,才能刷写该分区。此外,fastboot HAL 实现还可能需要设备专属权限。

验证用户空间 fastboot

供应商测试套件 (VTS) 包括用于验证用户空间 fastboot 的测试。