Android Recovery升級原理
- 2019 年 10 月 7 日
- 筆記
摘要
Recovery模式指的是一種可以對Android機內部的數據或系統進行修改的模式(類似於windows PE或DOS)。也可以稱之為Android的恢復模式,在這個所謂的恢復模式下,我們可以刷入新的Android系統,或者對已有的系統進行備份或升級,也可以在此恢復出廠設置(格式化數據和快取)。
1. Recovery相關概念
- Recovery: Recovery模式指的是一種可以對Android機內部的數據或系統進行修改的模式,也指Android的Recovery分區
- OTA: Over-the-Air Technology,即空中下載技術,是 Android 系統提供的標準軟體升級方式。 它功能強大,提供了完全升級、增量升級模式,可以通過 SD 卡升級,也可以通過網路升級。不管是哪種方式,都有幾個過程:生成升級包、下載升級包、安裝升級包。
- RecoverySystem:Android系統內部實現的一個工具類,Android應用層操作Recovery模式的一個重要途徑,它提供了幾個重要的API,用於實現OTA包校驗、升級以及恢復出廠設置(格式化數據和快取)。
- Main System:主系統模式,即Android正常開機所進入的Android系統
- Bootloader:Bootloader是嵌入式系統在加電後執行的第一段程式碼,在它完成CPU和相關硬體的初始化之後,再將作業系統映像或固化的嵌入式應用程式裝在到記憶體中然後跳轉到作業系統所在的空間,啟動作業系統運行。
- BCB:Bootloader Control Block,啟動控制資訊塊,位於misc分區,從程式碼上看,就是一個結構體。
2. Android系統的啟動模式
2.1 Android 各個分區介紹
一般來說,Android手機和平板一般包括以下標準內部分區:
Boot:包含Linux內核和一個最小的root文件系統(裝載到ramdisk中),用於掛載系統和其他的分區,並開始Runtime。正如名字所代表的意思(註:boot的意思是啟動),這個分區使Android設備可以啟動。如果沒有這個分區,Android設備通常無法啟動到Android系統。
System:這個分區幾乎包含了除kerner和ramdisk之外的整個android作業系統,包括了用戶介面、和所有預裝的系統應用程式和庫文件(AOSP中可以獲取到源程式碼)。在運行的過程中,這個分區是read-only的。當然,一些Android設備,也允許在remount的情況下,對system分區進行讀寫。 擦除這個分區,相當於刪除整個Android系統,會導致不能進入Main System, 但不會影響到Recovery。因此,可以通過進入Recovery程式或者bootloader程式中,升級安裝一個新ROM。
Userdata:用戶數據區,用戶安裝的應用程式會把數據保存在這裡,包含了用戶的數據:聯繫人、簡訊、設置、用戶安裝的程式。擦除這個分區,本質上等同於手機恢復出廠設置,也就是手機系統第一次啟動時的狀態,或者是最後一次安裝官方或第三方ROM後的狀態。在Recovery程式中進行的「data/factory reset 」操作就是在擦除這個分區。正常情況下OTA是不會清除這裡的數據的,指定要刪除數據的除外。
Cache:系統快取區,臨時的保存應用數據(要把數據保存在這裡,需要特地的app permission), OTA的升級包也可以保存在這裡。OTA升級過程可能會清楚這個分區的數據。一般來講,Android差分包升級也需要依賴此分區存放一些中間文件。
Recovery:包括了一個完整Linux內核和一些特殊的recovery binary,可以讀取升級文件用這些文件來更新其他的分區。
Misc:一個非常小的分區,4 MB左右。recovery用這個分區來保存一些關於升級的資訊,應對升級過程中的設備掉電重啟的狀況,Bootloader啟動的時候,會讀取這個分區裡面的資訊,以決定系統是否進Recovery System 或 Main System。
以上幾個分區是Google官方的標準,對於第三方Android設備廠商來講,分區的情況可能稍微不一樣,比如Rockchip平台,還增加了user分區、kernel分區和backup分區。其中:
kernel:顧名思義,是存放kernel.img鏡像的。在boot分區裡面的kernel內核鏡像損壞的情況下(比如flash損壞),bootloader會嘗試載入kerner分區裡面的內核鏡像。
backup:存放整個系統鏡像(update.img), 可用於恢復設備到出廠ROM。
user: 用戶分區,也就是平時我們所說的內置sdcard。另外還有外置的sdcard分區,用於存放用戶相片、影片、文檔、ROM安裝包等。
2.2 Android的啟動模式
一般來講,Android有三種啟動模式:Fastboot模式,Recovery System 以及Main System。
- Fastboot:在這種模式下,可以修改手機的硬體,並且允許我們發送一些命令給Bootloader。如使用電腦刷機,則需要進入fastboot模式,通過電腦執行命令將系統鏡像刷到通過USB刷到Android設備中中。
- Recovery:Recovery是一個小型的作業系統,並且會載入部分文件系統,這樣才能從sdcard中讀取升級包。
- Main System: 即我們平時正常開機後所使用的手機作業系統模式
首先說一下,正常啟動和進入Recovery的區別,一圖以概之:

2.3 如何進入Recovery模式
一般來講,進入recovery有兩種方式,一種是通過組合鍵進入recovery,按鍵指引的方式,各個Android平台都不一樣,比如三星的手機是在關機狀態下同時按住【音量上】、【HOME鍵】、【電源鍵】,等待螢幕亮起後即可放開,進入Recovery模式。而Rockchip的機頂盒,則是使用按【Reset鍵】加【電源鍵】開機的方式,形式不一。
另一種,則是使用系統命令啟動到Recovery模式的,這對絕大部分Android設備是通用的:
reboot recovery
3. Recovery升級原理
3.1 應用層升級流程
在Android應用層部分,OTA系統升級流程。大概的流程圖如下所示:

以上部分,只介紹了應用層層面的 ota升級包的下載、校驗以及最後的發起安裝過程。在這裡,重要講解進入Recovery模式後,OTA包的升級過程。
首先,在應用層下載升級包後,會調用RecoverySystem.installPackage(Context context, File packageFile)函數來發起安裝過程,這個過程主要的原理,實際上只是往 /cache/recovery/command 寫入ota升級包存放路徑,然後重啟到recovery模式,僅此而已。
public static void installPackage(Context context, File packageFile) throws IOException { String filename = packageFile.getCanonicalPath(); Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); final String filenameArg = "--update_package=" + filename; final String localeArg = "--locale=" + Locale.getDefault().toString(); bootCommand(context, filenameArg, localeArg); } private static void bootCommand(Context context, String... args) throws IOException { RECOVERY_DIR.mkdirs(); // In case we need it COMMAND_FILE.delete(); // In case it's not writable LOG_FILE.delete(); FileWriter command = new FileWriter(COMMAND_FILE); try { for (String arg : args) { if (!TextUtils.isEmpty(arg)) { command.write(arg); command.write("n"); } } } finally { command.close(); } // Having written the command file, go ahead and reboot PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); pm.reboot(PowerManager.REBOOT_RECOVERY); throw new IOException("Reboot failed (no permissions?)"); }
因此,實質上等同於以下命令:
echo -e "--update_package=/mnt/sdcard/ota/update.zip" > /cache/recovery/command reboot recovery
3.2 OTA升級包的目錄結構
OTA升級包的目錄結構大致如下所示:
|----boot.img |----system/ |----recovery/ |----recovery-from-boot.p |----etc/ `|----install-recovery.sh |---META-INF/ |CERT.RSA |CERT.SF |MANIFEST.MF |----com/ |----google/ |----android/ |----update-binary |----updater-script |----android/ |----metadata
其中:
- boot.img 是更新boot分區所需要的鏡像文件。這個boot.img主要包括kernel、ramdisk。
- system/目錄的內容在升級後會放在系統的system分區,主要是系統app,library和binary二進位文件
- update-binary是一個二進位文件,相當於一個腳本解釋器,能夠識別updater-script中描述的操作。
- updater-script:此文件是一個腳本文件,具體描述了更新過程。
- metadata文件是描述設備資訊及環境變數的元數據。主要包括一些編譯選項,簽名公鑰,時間戳以及設備型號等。
- 我們還可以在包中添加userdata目錄,來更新系統中的用戶數據部分。這部分內容在更新後會存放在系統的/data目錄下。
- update.zip包的簽名:update.zip更新包在製作完成後需要對其簽名,否則在升級時會出現認證失敗的錯誤提示。而且簽名要使用和目標板一致的加密公鑰。默認的加密公鑰及加密需要的三個文件在Android源碼編譯後生成的具體路徑為:
out/host/linux-x86/framework/signapk.jar build/target/product/security/testkey.x509.pem build/target/product/security/testkey.pk8
- MANIFEST.MF:這個manifest文件定義了與包的組成結構相關的數據。類似Android應用的mainfest.xml文件。
- CERT.RSA:與簽名文件相關聯的簽名程式塊文件,它存儲了用於簽名JAR文件的公共簽名。
- CERT.SF:這是JAR文件的簽名文件,其中前綴CERT代表簽名者。
3.3 Recovery模式下的OTA升級流程
進入Recovery模式之後,便開始對下載的升級包進行升級,整體的流程圖如下所示:
BCB(Bootloader與Recovery通過BCB(Bootloader Control Block)通訊)

這裡,詳解介紹一下升級流程中的各個模組。
1. get_args(&argc, &argv)
get_args的原理流程圖如下所示:
get_args()函數的主要作用是獲取系統的啟動參數,並回寫到bootloader control block(BCB)塊中。如果系統在啟動recovery時已經傳遞了啟動參數,那麼這個函數只是把啟動參數的內容複製到函數的參數boot對象中,否則函數會首先通過get_bootloader_message()函數從/misc分區的BCB中獲取命令字元串來構建啟動參數。如果/misc分區下沒有內容,則會嘗試解析/cache/recovery/command文件並讀取文件的內容來建立啟動參數。
接著,會把啟動參數的資訊通過set_bootloader_message()函數又保存到了BCB塊中。這樣做的目的是防止一旦升級或擦除數據的過程中發生崩潰或不正常斷電,下次重啟,Bootloader會依據BCB的指示,引導進入Recovery模式,從/misc分區中讀取更新的命令,繼續進行更新操作。因此,可以說是一種掉電保護機制。
get_args具體的流程如下圖所示:

get_args函數核心程式碼如下:
static void get_args(int *argc, char ***argv) { struct bootloader_message boot; memset(&boot, 0, sizeof(boot)); //解析BCB模組 get_bootloader_message(&boot); // this may fail, leaving a zeroed structure ...... // --- if that doesn't work, try the command file if (*argc <= 1) { FILE *fp = fopen_path(COMMAND_FILE, "r");//COMMAND_FILE指/cache/recovery/command if (fp != NULL) { char *argv0 = (*argv)[0]; *argv = (char **) malloc(sizeof(char *) * MAX_ARGS); (*argv)[0] = argv0; // use the same program name char buf[MAX_ARG_LENGTH]; for (*argc = 1; *argc < MAX_ARGS; ++*argc) { if (!fgets(buf, sizeof(buf), fp)) break; (*argv)[*argc] = strdup(strtok(buf, "rn")); // Strip newline. } check_and_fclose(fp, COMMAND_FILE); LOGI("Got arguments from %sn", COMMAND_FILE); } } ...... set_bootloader_message(&boot); //回寫BCB
這裡需要說一下「BCB」,即bootloader control block, 中文可以呼之為「啟動控制模資訊塊」**,位於/misc分區,從程式碼上看,就是一個struct 結構體 :
struct bootloader_message { char command[32]; char status[32]; char recovery[1024]; };
bootloader_message 結構體包含三個欄位,具體含義如下:
command 欄位中存儲的是命令,它有以下幾個可能值:
- boot-recovery:系統將啟動進入Recovery模式
- update-radia 或者 update-hboot:系統將啟動進入更新firmware的模式,這個更新過程由bootloader完成
- NULL:空值,系統將啟動進入Main System主系統,正常啟動。
status 欄位存儲的是更新的結果。更新結束後,由Recovery或者Bootloader將更新結果寫入到這個欄位中。
recovery 欄位存放的是recovry模組的啟動參數,一般包括升級包路徑。其存儲結構如下:第一行存放字元串「recovery」,第二行存放路徑資訊「–update_package=/mnt/sdcard/update.zip」等。 因此,參數之間是以「n」分割的。
2. update_package
ota升級包的存放路徑,從BCB或者/cache/recovery/command裡面解析得到的,升級包一般下載後存放在cache或sdcard分區,當然,也有一些是存放到U盤之類的外接存儲設備中的。一般賦值格式如下:
--update_package=/mnt/sdcard/update.zip 或 --update_package=CACHE:update.zip
3. int install_package (const char path, int wipe_cache, const char* install_file)
int install_package(const char* path, int* wipe_cache, const char* install_file) { //install_file 為 /cache/recovery/last_install FILE* install_log = fopen_path(install_file, "w"); if (install_log) { fputs(path, install_log); fputc('n', install_log); } else { LOGE("failed to open last_install: %sn", strerror(errno)); } int result = really_install_package(path, wipe_cache); if (install_log) { fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log); fputc('n', install_log); fclose(install_log); } return result; }
4. static int really_install_package(const char path, int wipe_cache)
really_install_package函數在install_package函數中被調用,函數的主要作用是調用ensure_path_mounted確保升級包所在的分區已經掛載,另外,還會對升級包進行一系列的校驗,在具體升級時,對update.zip包檢查時大致會分三步:
- 檢驗SF文件與RSA文件是否匹配;
- 檢驗MANIFEST.MF與簽名文件中的digest是否一致;
- 檢驗包中的文件與MANIFEST中所描述的是否一致
通過校驗後,調用try_update_binary函數去實現真正的升級。
5. static int try_update_binary(const char path, ZipArchive zip, int wipe_cache)
try_update_binary是真正實現對升級包進行升級的函數:
static int try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache) { const ZipEntry* binary_entry = mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME); ...... const char* binary = "/tmp/update_binary"; unlink(binary); int fd = creat(binary, 0755); ..... //將升級包裡面的update_binary解壓到/tmp/update_binary bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd); close(fd); mzCloseZipArchive(zip); ...... int pipefd[2]; pipe(pipefd); const char** args = (const char**)malloc(sizeof(char*) * 5); args[0] = binary; //update_binary存放路徑 args[1] = EXPAND(RECOVERY_API_VERSION); // Recovery版本號 char* temp = (char*)malloc(10); sprintf(temp, "%d", pipefd[1]); args[2] = temp; args[3] = (char*)path; //升級包存放路徑 args[4] = NULL; pid_t pid = fork();//fork一個子進程 if (pid == 0) { close(pipefd[0]); //子進程調用update-binary執行升級操作 execv(binary, (char* const*)args); fprintf(stdout, "E:Can't run %s (%s)n", binary, strerror(errno)); _exit(-1); } ...... int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { //安裝失敗,返回INSTALL_ERROR return INSTALL_ERROR; } //安裝成功,返回INSTALL_SUCCESS return INSTALL_SUCCESS; }
總的來說,try_update_binary主要做了以下幾個操作:
(1)mzOpenZipArchive():打開升級包,並將相關的資訊拷貝到一個臨時的ZipArchinve變數中。注意這一步並未對我們的update.zip包解壓。
(2)mzExtractZipEntryToFile(): 解壓升級包特定文件,將升級包裡面的META-INF/com/google/android/update-binary 解壓到記憶體文件系統的/tmp/update_binary中。
(3)fork創建一個子進程 , 使用系統調用函數execv( ) 去執行/tmp/update-binary程式,
(4)update-binary: 這個是Recovery OTA升級的核心程式,是一個二進位文件,實現程式碼位於系統源碼bootable/recovery/updater。其實質是相當於一個腳本解釋器,能夠識別updater-script中描述的操作並執行。
(5)updater-script:updater-script是我們升級時所具體使用到的腳本文件,具體描述了更新過程,它主要用以控制升級流程的主要邏輯。具體位置位於升級包中/META-INF/com/google/android/update-script,在我們製作升級包的時候產生。在升級的時候,由update_binary程式從升級包裡面解壓到記憶體文件系統的/tmp/update_script中,並按照update_script裡面的命令,對系統進行升級。比如,一個完整包升級的update_script的內容大致如下所示:
assert(getprop("ro.product.device") == "rk31sdk" || getprop("ro.build.product") == "rk301dk"); show_progress(0.500000, 0); format("ext4", "EMMC", "/dev/block/mtd/by-name/system", "0", "/system"); mount("ext4", "EMMC", "/dev/block/mtd/by-name/system", "/system"); package_extract_dir("recovery", "/system"); package_extract_dir("system", "/system"); symlink("Roboto-Bold.ttf", "/system/fonts/DroidSans-Bold.ttf"); symlink("mksh", "/system/bin/sh"); ...... set_perm_recursive(0, 0, 0755, 0644, "/system"); set_perm_recursive(0, 2000, 0755, 0755, "/system/bin"); ...... set_perm(0, 0, 06755, "/system/xbin/su"); set_perm(0, 0, 06755, "/system/xbin/tcpdump"); show_progress(0.200000, 0); show_progress(0.200000, 10); write_raw_image(package_extract_file("boot.img"), "boot"); show_progress(0.100000, 0); clear_misc_command(); unmount("/system");
update_script常用的命令如下:

因此,根據上面的升級腳本,可以知道,升級包的大致升級流程如下:
- 判斷是不是升級包是否適用於該設備,如果不適用,則停止升級,否則繼續。
- 顯示進度條
- 格式化system分區
- 掛載system分區
- 將ota升級包裡面的system、recovery目錄解壓到system分區
- 建立一些軟鏈接,升級過程需要用到
- 設置部分文件許可權
- 將升級包裡面的boot.img寫入到/boot分區
- 清空misc分區,即BCB塊置為NULL
- 卸載system分區
6. wipe data/cache
main函數,在執行完install_package後,會根據傳入的wipe_data/wipe_cache,決定是否執行/data和/cache分區的清空操作。
7. prompt_and_wait
這個函數的作用就是一直在等待用戶輸入,是一個不斷的循環,可以選擇Recovery模式下的一些選項進行操作,包括恢復出廠設置和重啟等。如果升級失敗, prompt_and_wait會顯示錯誤,並等待用戶響應。
8. finish_recovery
OTA升級成功,清空misc分區(BCB置零),並將保存到記憶體系統的升級日誌/tmp/recovery.log保存到/cache/recovery/last_log。重啟設備進入Main System,升級完成。
9. install-recovery.sh
從上面的流程中,可以知道,Recovery模式下的OTA升級成功,只是更新了/system和/boot兩個最核心的分區,而本身用來升級的Recovery自身並沒有在那個時候得到更新。Recovery分區的更新,是在重啟進入主系統的時候,由install-recovery.sh來更新的。這樣可以保證,即使升級失敗,Recovery模式也不會受到影響,仍然可以手動進入Recovery模式執行升級或擦除數據操作。
在Recovery升級的時候,有一句:
package_extract_dir("recovery", "/system");
這條命令就是將升級包裡面的recovery目錄的內容,解壓到/system分區
recovery目錄下的文件,主要有install-recovery.sh和 recovery-from-boot.p,目錄結構如下所示:
├── bin │ └── install-recovery.sh └── recovery-from-boot.p
其中:
- recovery-from-boot.p 是boot.img和recovery.img的修補程式(patch)
- install-recovery.sh 則是來用安裝recovery-from-boot.p的升級腳本, 主要是利用android系統的 applypatch 工具來打修補程式。
至此,一個完整的OTA包升級就正式完成了!
4. Bootloader、BCB、Recovery與Main System之間的交互
首先,通過前面的介紹,可以知道, Recovery System與Main System的交互,主要是通過/cache分區下的文件進行資訊交互的。具體如下:

其中,command的值一般有以下一個或多個:

其次,Bootloader與Recovery和Main System之間也是存在交互的: Bootloader會通過解析BCB模組,決定啟動系統到Recovery或Main System。而Recovery或Main System也能夠操作BCB,進而影響到Bootloader的行為。
當Main System系統關鍵進程崩潰太多次的時候,系統還會自發啟動進入到Recovery模式。
另外,部分平台的Android設備,在Recovery模式下,也能夠對Bootloader進行升級。
Bootloader、BCB、Recovery與Main System四者相互影響,又獨立工作。它們之間斬不斷理還亂的關係,可以以下圖概括之:
