為什麼刷 LineageOS 時是用 .zip
而不是 .img
?
近期在 Hacking Thursday 聚會和 Pellaeon Lin 在協助阿寬刷 LineageOS 的過程中,阿寬問道:
為什麼是刷 `.zip` 檔,而不是 `.img` 檔?
這個疑問題的背景是在,LineageOS 的刷機過程中的兩個步驟:
fastboot flash boot lineage-19.1-20220925-recovery-flame.img
adb sideload lineage-19.1-20220925-nightly-flame-signed.zip
前者,是將 image 檔案資料寫進 boot
分割區,容易理解。但後者卻不同,是採用 .zip
檔而不採用 .img
的方式寫入。為什麼呢?
當下,我簡單的回應說,這個 .zip
檔裡,還是有包 image 檔案,Android 的工具會解壓縮並解析裡面的描述檔,然後再依裡面的參數去刷 image 。因為當時是憑印象回答,且自己之前也有些疑惑沒解開,回想起來總感到心虛。所以決定進一步爬一下 source code 來確認跟檢驗一下。
Q: 這個 zip 檔案內容是什麼?
解開 lineage-19.1-20220925-nightly-flame-signed.zip
並瞧了一下,結構大致如下:
.
├── apex_info.pb
├── care_map.pb
├── META-INF
│ └── com
│ └── android
│ ├── metadata
│ ├── metadata.pb
│ └── otacert
├── payload.bin
└── payload_properties.txt
其中 payload.bin
就是將刷入的 image 檔案,同時有 payload_properties.txt
輔助 checksum 檢查。
FILE_HASH=WlmU5qsJP4G449TJz3DYQjQuHiPKCJJGj/7fyHcICRI=
FILE_SIZE=1094414259
METADATA_HASH=Husi2CZRsqijFVEd5aWTK4gnxiN3kTGKfmd8EliY8dU=
METADATA_SIZE=134083
另外,還有 META-INF/com/android/metadata
的檔案,描述參數如下:
ota-property-files=payload_metadata.bin:41:134350,payload.bin:41:1094414259,payload_properties.txt:1094414352:156,care_map.pb:1094414549:732,metadata:1094416436:610,metadata.pb:1094415385:992
ota-required-cache=0
ota-streaming-property-files=payload.bin:41:1094414259,payload_properties.txt:1094414352:156,care_map.pb:1094414549:732,metadata:1094416436:610,metadata.pb:1094415385:992
ota-type=AB
post-build=google/flame/flame:12/SQ3A.220705.003.A1/8672226:user/release-keys
post-build-incremental=6fd1676fd5
post-sdk-level=32
post-security-patch-level=2022-09-05
post-timestamp=1664086215
pre-device=flame
Q: adb
的 source code 放在那裡?
因為 .zip
是透過 adb sideload
指令執行,所以先查 adb
source code。但找 adb
的 source code 花了點時間。一開始 google 搜尋找到 platform/system/core/adb/adb.c
的路徑,但在最新的 git repository 卻找不到。
後循線索發現:
- 先是
adb/adb.c
於 2015-02-25 時 (commit:bac3474
) 改為 C++ 的adb/adb.cpp
- 再於 2020-10-23 (commit:
a88ef8c
) 從system/core/adb
搬到packages/modules/adb
所以目前 source code 的最新位置是在:
https://android.googlesource.com/platform/packages/modules/adb
Q: 這些參數和檔案是在那裡處理的?
查了 adb
的 source code 之後發現 adb sideload
只是將 .zip
檔案傳進去,並非真正處理的地方。真正處理的地方,後來發現是在 recovery 相關的程式碼,位置如下:
https://android.googlesource.com/platform/bootable/recovery
其中,在 install/install.cpp
有函式呼叫流程如下:
… => InstallPackage()
=> VerifyAndInstallPackage()
=> TryUpdateBinary()
而在 TryUpdateBinary()
的函式中,有分成兩個 cases:
SetUpAbUpdateCommands()
,這是用於AB
type 的 case- 這個 case 會根據
payload_properties.txt
,payload.bin
作刷 image 的動作
- 這個 case 會根據
SetUpNonAbUpdateCommands()
, 這是用於非AB
type 的 case- 這個 case 會去找這個檔案
META-INF/com/google/android/update-binary
並執行之
- 這個 case 會去找這個檔案
對應的 source 區段如下:
if (auto setup_result =
package_is_ab
? SetUpAbUpdateCommands(package_path, zip, pipe_write.get(), &args)
: SetUpNonAbUpdateCommands(package_path, zip, retry_count, pipe_write.get(), &args);
!setup_result) {
log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure));
return INSTALL_CORRUPT;
}
Q: 那麼系統是如何判別 type 是 AB
或否的呢?
答案是,在之前的 META-INF/com/android/metadata
檔案中,有一欄參數寫有:
ota-type=AB
這參數會在 install/install.cpp
的 TryUpdateBinary()
裡採值如下:
bool package_is_ab = get_value(metadata, "ota-type") == OtaTypeToString(OtaType::AB);
Q: 非 AB
type 會怎麼處理?
依前述的流程,它會被系統分類為非AB
type,而進到 SetUpNonAbUpdateCommands()
的流程去,去找到並執行 META-INF/com/google/android/update-binary
這個檔案。
以另一個可能在刷 LineageOS 時會用到的套件為例:
https://mirrorbits.lineageos.org/tools/copy-partitions-20220613-signed.zip
其檔案結構如下:
.
└── META-INF
└── com
├── android
│ └── otacert
└── google
└── android
├── update-binary
└── updater-script
其中最重要的是 META-INF/com/google/android/update-binary
檔案,這個 Android 會解開並執行之 。若他是 binary excutable 檔,則執行 binary 。而此處它是個 Linux Shebang 執行腳本:
#!/sbin/sh
#####################################################
# Flashize Runtime (2016-04-06) #
# Copyright 2016, Lanchon #
#####################################################
#####################################################
# The Flashize Runtime is free software licensed #
# under GNU's Lesser General Public License (LGPL) #
# version 3 and any later version. #
# ------------------------------------------------- #
# Note: The code appended to the Flashize Runtime, #
# if any, is independently licensed. #
#####################################################
if [ "$1" != "lanchon-magic" ]; then
export FLASHIZE_VERSION='2016-04-06'
log=''
if [ -f /tmp/flashize-log ]; then
log="$(cat /tmp/flashize-log)"
if [ -z "$log" ]; then
log=/tmp/flashize.log
fi
fi
if [ "$log" == "-" ]; then
log=""
fi
...
echo "Partition $partition"
inactive=$(echo $active | sed "s/${suffix_active}\$/${suffix_swap}/");
part_active=$(readlink -fn $active);
part_inactive=$(readlink -fn $inactive);
if [[ -n "$part_active" && -n "$part_active" && "$active" != "$part_active" && "$inactive" != "$part_inactive" ]]; then
blockdev --setrw $part_inactive
dd if=$part_active of=$part_inactive bs=4k
fi
fi
done
依 Linux Shebang 的慣例規則,會由 /sbin/sh
載入並執行之。
另一個 updater-script
在此處並未觸發,似乎在別的 use case 會用到。這個留待之後有遇到時,再繼續探討。
Q: 為何不直接寫入 image 而要包成 zip 檔跟層層檔案參數呢?
回到問題的動機面,有很多方法可以寫入需要的 binary data。為何要包裝成這個樣子,這樣不是找自己麻煩嗎?
其實這樣的作法,是韌體更新作業的常態。因為 Reflash/Update/Upgrade Firmware 的作業,通常出現在嵌入法系統(Embedded System), 單晶片(MCU), IoT, … 等情境。有很多狀況一旦環節有錯,就可能再也無法開機,例如:
- 刷錯檔案 => 於是需要一些 Checksum 機制來驗證
- 在更新過程中,少了某個檔案 => 需要類似像 Manifest 檔案的清單機制
- ARCH, type, … 參數搞錯了 => 需要有 metadata 檔案描述這個 image 的性質跟類別
- 有些 Firmware 版本之間有衝突 => 需要 Firmware Version,跟 Dependencies 檢查機制
- …
把上述種種因素都考慮進來之後,就能理解單靠一個 image 檔本身,是作不到防呆的。不難想見像是 BIOS 的更新檔案,一些 4G/LTE modem 的更新檔, … 等,其更新檔案通常會以打包過的壓縮檔的形式來呈現。
補充
總結爬完 source code 的內容,和之前的印象差不多一致,總算是解掉心中一個懸念。
需要注意到此處討論到的 case ,都是在特殊模式(recovery
, sideload
, rescue
, …)下運行的。跟一般正常運行時,所用的 adb install <some package.apk>
的情況不同。
Reference:
- LineageOS: https://lineageos.org/
- adb: https://android.googlesource.com/platform/packages/modules/adb
- android recovery: https://android.googlesource.com/platform/bootable/recovery
- Linux Shebang: https://6bcf7279.info/2022/08/02/h6u2ryqvy/
- Hacking Thursday: https://www.hackingthursday.org
- 阿寬: https://lin19921127.medium.com/
- Pellaeon Lin: https://nyllep.wordpress.com/