引導加載程序
引導加載程序是供應商專有的映像,負責在設備上啟動內核。它會監護設備狀態,負責初始化可信執行環境 (TEE)以及綁定其信任根。
引導加載程序由許多部分組成,包括啟動畫面。要開始啟動,引導加載程序可能會直接將一個新映像刷寫到相應的分區中,也可能會使用 recovery
開始重新刷寫過程,該過程與 OTA 的操作過程一致。一些設備制造商會創建多部分引導加載程序,然后將它們組合到一個 bootloader.img 文件中。在刷寫時,引導加載程序會提取各個引導加載程序並刷寫所有這些引導加載程序。
最重要的是,引導加載程序會在將執行工作移到內核之前先驗證 boot 分區和 recovery 分區的完整性,並顯示啟動狀態部分中指定的警告。
規范化啟動原因
Android 9 對引導加載程序啟動原因規范進行了以下更改。
啟動原因
引導加載程序使用專用的硬件和內存資源來確定設備重新啟動的原因,然后將 androidboot.bootreason=<reason>
添加到用於啟動設備的 Android 內核命令行中,以傳達這一決定。然后,init
會轉換此命令行,使其傳播到 Android 屬性 bootloader_boot_reason_prop
(ro.boot.bootreason
) 中。
啟動原因規范
之前的 Android 版本中指定的啟動原因格式如下:不使用空格,全部為小寫字母,只有非常少的要求(例如報告 kernel_panic
、watchdog
、cold
/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>…
格式設置規則如下:
- 小寫
- 無空格(可使用下划線)
- 全部為可打印字符
- 以英文逗號分隔的
reason
、subreason
,以及一個或多個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
狀態和硬件狀態未知。該值是與cold
、hard
和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 除外)都包含兩個副本(插槽):
|
每個分區都包含一個副本,無其他備份分區。
|
要詳細了解 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:
|
僅包含正常啟動內核。 |
recovery.img | 包含恢復內核和 recovery-ramdisk.img。 | ||
system.img | 包含以下內容:
|
包含原始 system.img 和 ramdisk.img 的合並內容:
|
|
分區布局 | 無 |
|
|
設置 dm-verity
在 system-as-root 中,內核必須使用 dm-verity 在 /
(裝載點)下裝載 system.img
。AOSP 支持 system.img
的下列 dm-verity 實現:
- 對於 vboot 1.0,內核必須在
/system
上解析 Android 專用元數據,然后轉換為 dm-verity 參數以設置 dm-verity。需要這些內核補丁程序。 - 對於 vboot 2.0 (AVB),引導加載程序必須先整合 external/avb/libavb,然后 external/avb/libavb 會解析
/system
的哈希樹描述符,再將解析結果轉換為 dm-verity 參數,最后通過內核命令行將這些參數傳遞給內核。(/system
的哈希樹描述符可能位於/vbmeta
或/system
本身上)。
需要下列內核補丁程序:- https://android-review.googlesource.com/#/c/kernel/common/+/158491/
- 內核 4.4 補丁程序、內核 4.9 補丁程序等注意:您也可以在 external/avb/contrib/linux/(4.4、4.9 等)/* 上找到上述 AVB 專用內核補丁程序文件。
下面是來自真實設備的示例,顯示的是內核命令行中 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 更新,設備需要為 boot
、system
、vendor
和 radio
分區分別單獨配置一個槽位。
- boot:
boot
分區包含通過mkbootimg
組合在一起的內核映像和 RAM 磁盤。為了直接刷寫內核而不刷寫新的 boot 分區,可以使用虛擬分區:- kernel:
kernel
虛擬分區僅覆蓋內核(zImage、zImage-dtb、Image.gz-dtb),方法是寫入新的映像來覆蓋舊的映像。為此,它會確定 eMMC 中現有內核映像的起始位置並將新內核映像復制到該位置。請記住,新內核映像可能會大於現有內核映像。引導加載程序可以通過移動其后的任何數據來騰出空間或放棄出錯的操作。如果提供的開發內核不兼容,則可能需要使用相關的內核模塊更新 dtb 分區(如果存在)、vendor 分區或 system 分區。 - ramdisk:
ramdisk
虛擬分區通過將新映像寫入舊磁盤來僅覆蓋 RAM 磁盤。為此,它會確定 eMMC 中現有ramdisk.img
的起始位置並將新 RAM 磁盤映像復制到該位置。請記住,新 RAM 磁盤映像可能會大於現有 RAM 磁盤映像。引導加載程序可以通過移動其后的任何數據來騰出空間或放棄出錯的操作。
- kernel:
- system:
system
分區主要包含 Android 框架。 - recovery:
recovery
分區用於存儲在 OTA 過程中啟動的恢復映像。如果設備支持 A/B 更新,則恢復映像可以是啟動映像中包含的 RAM 磁盤,而不是單獨的映像。 - cache:
cache
分區用於存儲臨時數據,如果設備使用 A/B 更新,則可以不要此分區。cache 分區不需要可從引導加載程序寫入,而只需要可清空。大小取決於設備類型和 userdata 分區的可用空間。目前,50MB 至 100MB 應該沒問題。 - misc:
misc
分區供恢復映像使用,存儲空間不能小於 4KB。 - userdata:
userdata
分區包含用戶安裝的應用和數據,包括自定義數據。 - metadata:如果設備被加密,則需要使用
metadata
分區,該分區的存儲空間不能小於 16MB。 - vendor:
vendor
分區包含所有不可分發給 Android 開源項目 (AOSP) 的二進制文件。如果沒有專有信息,則可以省略此分區。 - radio:
radio
分區包含無線裝置映像。只有包含無線裝置且在專用分區中包含無線裝置專用軟件的設備才需要此分區。 - tos:
tos
分區用於存儲 Trusty 操作系統的二進制映像文件,僅在設備包含 Trusty 時使用。
流程
引導加載程序的運作方式如下:
- 首先加載引導加載程序。
- 引導加載程序初始化內存。
- 如果使用 A/B 更新,則確定要啟動的當前槽位。
- 確定是否應按照支持更新中所述改為啟動恢復模式。
- 引導加載程序加載映像,其中包含內核和 RAM 磁盤(在 Treble 中,包含更多部分)。
- 引導加載程序開始將內核作為可自行執行的壓縮二進制文件加載到內存中。
- 內核將自身解壓縮並開始執行到內存中。
- 自此,舊版設備從 RAM 磁盤加載
init
,而新版設備從/system
分區加載它。 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 |
|
/product |
|
出於這些原因,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) 穩定性時,請記住以下架構:

/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_DTBO
、BOARD_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_ACPIO
、BOARD_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_CMDLINEboot.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
- DT 屬性:
- 引導加載程序應支持 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。
- 引導加載程序還應確定要加載的槽位。有關決策流程的描述,請參見本部分中的示意圖,一般步驟如下:
- 確定要嘗試加載的槽位。不要嘗試加載標記為“slot-unbootable”的槽位。此槽位應與 fastboot 返回的值一致,並且從現在開始稱為當前槽位。
- 當前槽位是否未標記為
slot-successful
且slot-retry-count
= 0?
將當前槽位標記為“slot-unbootable”,並選擇一個未標記為“slot-unbootable”而是標記為“slot-successful”的不同槽位。此槽位現在是選定的槽位。如果沒有當前槽位可用,則系統會啟動到恢復模式或向用戶顯示一條有意義的錯誤消息。 - 選擇相應的 boot.img,並在內核命令行上添加正確的 system 分區的路徑。
- 如果不啟動到恢復模式,請將 skip_initramfs 添加到內核命令行
- 填充 DT 或命令行 slot_suffix 參數
- 啟動。如果未標記為“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;
}
現在支持 boolean
、char
、float
、double
、byte
、int
、long
和 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; }
}
這是因為所生成的類(IFoo
、IFoo.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 接口。
-
確定您的接口的所有依賴項。對於該接口所依賴的每個軟件包,確定該軟件包是否已在穩定的 AIDL 中定義。如果未定義,則必須轉換該軟件包。
-
將您的接口中的所有 Parcelable 全部轉換為穩定的 Parcelable(接口文件本身可以保持不變)。可通過直接在 AIDL 文件中表示它們的結構來實現這一點。您必須重寫管理類以使用這些新類型。可以在創建
aidl_interface
軟件包(如下所示)之前完成重寫。 -
創建
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
字段不變(以重啟中斷的恢復任務)。status
、stage
和 reserved
字段也保持不變。引導加載程序應該會在 BCB 命令中看到 boot-fastboot
時加載並啟動到恢復映像。然后,recovery 會解析 BCB 消息並切換到 fastbootd
模式。
新 adb
命令
本部分介紹了集成 fastbootd
所需的另一個 adb
命令。該命令具有不同的行為,具體取決於是 system 還是 recovery 在執行該命令。
命令 | |
---|---|
reboot fastboot |
|
新 fastboot 命令
本部分介紹了集成 fastbootd
所需的其他 fastboot 命令,包括用於刷寫和管理邏輯分區的新命令。某些命令具有不同的行為,具體取決於是引導加載程序還是 fastbootd
在執行這些命令。
命令 | |
---|---|
reboot recovery |
|
reboot fastboot |
重新啟動到 fastbootd 。 |
getvar is-userspace |
|
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
,請執行以下操作:
-
將
fastbootd
添加到device.mk
中的PRODUCT_PACKAGES
:PRODUCT_PACKAGES += fastbootd
。 -
確保將 fastboot HAL、引導控制 HAL 和運行狀況 HAL 打包為恢復映像的一部分。
-
添加
fastbootd
所需的任何設備專屬 sepolicy 權限。例如,fastbootd
需要對設備專屬分區進行寫入訪問,才能刷寫該分區。此外,fastboot HAL 實現還可能需要設備專屬權限。
驗證用戶空間 fastboot
供應商測試套件 (VTS) 包括用於驗證用戶空間 fastboot 的測試。