為什麼刷 LineageOS 時是用 `.zip` 而不是 `.img`?

Posted by 每特17劃 on 2022-10-11

為什麼刷 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 的動作
  • SetUpNonAbUpdateCommands() , 這是用於 AB type 的 case
    • 這個 case 會去找這個檔案 META-INF/com/google/android/update-binary 並執行之

對應的 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.cppTryUpdateBinary() 裡採值如下:

  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: