每特17劃

及時當勉勵 2004/06/07

為什麼刷 LineageOS 時是用 `.zip` 而不是 `.img`? — 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:

試用 Open Build Service — 2022-09-03

試用 Open Build Service

註冊帳號

  1. 先至 Open Build Service 網站: URL
  2. 點選 "Sign Up"

  1. 將註冊表單填一填

  1. 之後 … ,跑完 email 認證手續後,即可登入使用。

設定 Repository

作業系統環境

以 Ubuntu 22.04 作業系統為例:

方法A: 使用 Live CD/USB

至 Ubuntu 官方網站下載 ISO 檔

URL: https://releases.ubuntu.com/22.04/

(選取 "64-bit PC (AMD64) desktop image" 的項目)

再以此 ISO 開機直接使用

方法B: 使用 docker

以下使用 docker 作示例:

$ docker run --rm --tty --interactive ubuntu:22.04 bash

進到 docker container 後,先建立一般使用者 ubuntu 並以該身份執行:

# apt update && apt install -y sudo
# useradd ubuntu --create-home --groups sudo --shell /bin/bash
# echo "ubuntu ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/casper
# su - ubuntu

上傳套件

先安裝必要的工具程式如下:

$ sudo apt update
$ sudo apt install -y software-properties-common dpkg-dev osc

其中:

  • software-properties-common: 提供 add-apt-repository 指令
  • dpkg-dev: 在下載 firefox source package 時用到
  • osc: Open Build Service 的專屬 CLI 工具指令

執行下述指令引入 firefox 的 PPA,注意這裡要加上 --enable-source

sudo add-apt-repository --enable-source ppa:mozillateam/ppa

之後執行下述指令,下載 firefox 的 debian source package:

apt source firefox

下載完後,會得到檔案及目錄概況如下:

.
|-- firefox-104.0.1+build1
|   |-- debian
|   |   |-- ...
|   |   |-- changelog
|   |   |-- control
|   |   |-- rules
|   |   `-- ...
|-- firefox_104.0.1+build1-0ubuntu0.22.04.1~mt1.debian.tar.xz
|-- firefox_104.0.1+build1-0ubuntu0.22.04.1~mt1.dsc
`-- firefox_104.0.1+build1.orig.tar.xz

接著 checkout 在 Open Build Server 上的 repository

osc checkout home:matlinuxer2

首次下載, 在輸入完帳號跟密碼之後,若遇到下列 prompt 時,可選擇 4

1) chainer ChainerBackend (Backend provided by python-keyring)
2) fail Keyring (Backend provided by python-keyring)
3) Config file credentials manager (Store the credentials in the config file (plain text))
4) Obfuscated Config file credentials manager (Store the credentials in the config file (obfuscated))
5) Transient password store (Do not store the password and always ask for the password)
Select credentials manager: 4

Reference

試用 zswap —

試用 zswap

根據 Pellaeon 的 blog 分享的資訊:

原本要安裝 systemd-swap ,就改為直接用 tmpfiles.d 來設定。

主要調整參數為:

  • /sys/module/zswap/parameters/max_pool_percent => 30
  • /proc/sys/vm/swappiness => 100

編輯 /etc/tmpfiles.d/zswap.conf 設定如下:

w- /proc/sys/vm/swappiness - - - - 100
w- /sys/module/zswap/parameters/max_pool_percent - - - - 30

但重開機後,發現只有部份參數生效,原因是 /proc/sys/vm/swappiness 有 race condition,會被 sysctl 的開機設定蓋寫如下:

/etc/sysctl.d/99-sysctl.conf:1:vm.swappiness = 10

改修改 /etc/tmpfiles.d/zswap.conf 設定如下:

w+ /etc/sysctl.d/99-sysctl.conf - - - - vm.swappiness = 100
w- /sys/module/zswap/parameters/max_pool_percent - - - - 30

重開機之後就生效開始運作了。

FAQ

Q: hibernate 的機制跟 swap 有關,是否能正常運作?

目前測試 OK 。但不確定在 MEM 吃緊時,是否還是正常

Q: 如果 reset swap 讓 swap 歸零?

可執行下述指令

sudo swapoff -a; sleep 30; sudo swapon -a

Reference

Linux 上的 Shebang 是在那裡定義的? — 2022-08-02

Linux 上的 Shebang 是在那裡定義的?

在 Linux 的指令執行檔的開頭,常常會看到以 #! 字串開頭的宣告,像是:

  • #!/bin/bash
  • #!/usr/bin/env python3
  • #!/bin/sh

這個開頭宣告的作用,是告訴系統要用什麼指令來執行該指令執行檔。 例如: #!/usr/bin/env python3 開頭的,就會以 /usr/bin/env python3 這個指令來執行。

不過,這開頭的 #! 這一行是那裡讀取並判讀的呢? 是 Bash? glibc? … or kernel?

答案是,直接由 Linux kernel 處理的,其對應的 source code 是在

fs/binfmt_script.c 裡的 load_script() 函式:

static int load_script(struct linux_binprm *bprm)
{
	const char *i_name, *i_sep, *i_arg, *i_end, *buf_end;
	struct file *file;
	int retval;

	/* Not ours to exec if we don't start with "#!". */
	if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
		return -ENOEXEC;

(出處: https://github.com/torvalds/linux/blob/v5.18/fs/binfmt_script.c#L41)

同時,Linux kernel config 有個設定選項 BINFMT_SCRIPT ,來決定是否支援 #! 這樣的用法。

(出處: https://github.com/torvalds/linux/blob/v5.18/fs/Kconfig.binfmt#L99)

可以使用下列指令來檢查系統的支援狀況:

zless /proc/config | grep -e "^CONFIG_BINFMT_SCRIPT="

預期會得到:

CONFIG_BINFMT_SCRIPT=y

一般來說,絕大多數的 Linux 系統都內建支援 #! 的語法。 但還是遇有一些 embedded system 或是資安考量的系統,會拿掉這個設定。 若在設定 at , cron 的排程設定時,發現程式未如預期執行時,可順便檢查這一個設定。

Q: 關於 Shebang 的歷史?

可參考維基百科的說明:

https://en.wikipedia.org/wiki/Shebang_(Unix)

Q: Linux 上大約何時支援?

從官方的 git 紀錄,支援 #! 宣告的功能,至少在 v2.6.12 時就己經存在:

https://github.com/torvalds/linux/blob/v2.6.12/fs/binfmt_script.c#L26

實際時間應該更早。 不過現下已經很少遇到這類舊系統,所以暫時沒再往前追溯。

Q: 為什麼有的會用 #!/usr/bin/env 開頭?

以 Bash 為例,有些系統會放在 /bin/bash 的位置,有些會放在 /usr/bin/bash。 若我的 script 寫 #!/bin/bash ,那遇到放在 /usr/bin/bash 位置的系統,就會路徑不符而失效。 所以,就有了這一支 env 的程式,他在所有系統都固定放在 /usr/bin/env 這個位置。 當用下述的指令時:

/usr/bin/env bash

env 就會根據該系統的環境設定(ex: $PATH, /etc/environment, …),去找出對應的 bash 的位置去執行。 如果一來,用上述的宣告在橫跨不同系統時,都能執行,而不需要再調整開頭的 Shebang 路徑。

Facebook post 2021-10-23 — 2021-10-23
Facebook post 2021-09-05 — 2021-09-05
Facebook post 2021-06-24 — 2021-06-24
Facebook post 2021-06-17 — 2021-06-17
Facebook post 2021-06-03 — 2021-06-03

Facebook post 2021-06-03

在 thingiverse 網站上搜尋 "mask strap" 關鍵字,可以找到不少口罩減壓帶的 3D 列印模型。我試了幾款,其中 https://www.thingiverse.com/thing:4284533 這個可解除耳朵束縳,固定較佳,列印也簡單(PLA即可)。提供給有需要的人參考。

Imgur

Comments:

  • Daniel Lin: Mat, 你白髮變好多
    • 李俊裕: 對啊,老了XD
      • Daniel Lin: 李俊裕 我只記得你年輕的樣子(太少見面)
  • Travis Yeh: 好久不見
    • 李俊裕: 希望疫情過后,有機會再來聚聚吧。 Stay safe!

Shares:

  • Shih-Yuan Lee
Facebook post 2021-05-29 — 2021-05-29