Android Recovery升級原理

摘要

Recovery模式指的是一種能夠對安卓機內部的數據或系統進行修改的模式(相似於windows PE或DOS)。也能夠稱之爲安卓的恢復模式,在這個所謂的恢復模式下,咱們能夠刷入新的安卓系統,或者對已有的系統進行備份或升級,也能夠在此恢復出廠設置(格式化數據和緩存)。linux

1. Recovery相關概念

  • Recovery: Recovery模式指的是一種能夠對安卓機內部的數據或系統進行修改的模式,也指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系統。windows


System:這個分區幾乎包含了除kerner和ramdisk以外的整個android操做系統,包括了用戶界面、和全部預裝的系統應用程序和庫文件(AOSP中能夠獲取到源代碼)。在運行的過程當中,這個分區是read-only的。固然,一些Android設備,也容許在remount的狀況下,對system分區進行讀寫。 擦除這個分區,至關於刪除整個安卓系統,會致使不能進入Main System, 但不會影響到Recovery。所以,能夠經過進入Recovery程序或者bootloader程序中,升級安裝一個新ROM。緩存


Userdata:用戶數據區,用戶安裝的應用程序會把數據保存在這裏,包含了用戶的數據:聯繫人、短信、設置、用戶安裝的程序。擦除這個分區,本質上等同於手機恢復出廠設置,也就是手機系統第一次啓動時的狀態,或者是最後一次安裝官方或第三方ROM後的狀態。在Recovery程序中進行的「data/factory reset 」操做就是在擦除這個分區。正常狀況下OTA是不會清除這裏的數據的,指定要刪除數據的除外。網絡


Cache:系統緩存區,臨時的保存應用數據(要把數據保存在這裏,須要特意的app permission), OTA的升級包也能夠保存在這裏。OTA升級過程可能會清楚這個分區的數據。通常來說,Android差分包升級也須要依賴此分區存放一些中間文件。app


Recovery:包括了一個完整Linux內核和一些特殊的recovery binary,能夠讀取升級文件用這些文件來更新其餘的分區。tcp


Misc:一個很是小的分區,4 MB左右。recovery用這個分區來保存一些關於升級的信息,應對升級過程當中的設備掉電重啓的情況,Bootloader啓動的時候,會讀取這個分區裏面的信息,以決定系統是否進Recovery System 或 Main System。函數


以上幾個分區是Google官方的標準,對於第三方Android設備廠商來說,分區的狀況可能稍微不同,好比Rockchip平臺,還增長了user分區、kernel分區和backup分區。其中:工具

kernel:顧名思義,是存放kernel.img鏡像的。在boot分區裏面的kernel內核鏡像損壞的狀況下(好比flash損壞),bootloader會嘗試加載kerner分區裏面的內核鏡像。ui

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的區別,一圖以概之:

image

2.3 如何進入Recovery模式

通常來說,進入recovery有兩種方式,一種是經過組合鍵進入recovery,按鍵指引的方式,各個Android平臺都不同,好比三星的手機是在關機狀態下同時按住【音量上】、【HOME鍵】、【電源鍵】,等待屏幕亮起後便可放開,進入Recovery模式。而Rockchip的機頂盒,則是使用按【Reset鍵】加【電源鍵】開機的方式,形式不一。

另外一種,則是使用系統命令啓動到Recovery模式的,這對絕大部分Android設備是通用的:

reboot recovery

3. Recovery升級原理

3.1 應用層升級流程

在Android應用層部分,OTA系統升級流程。大概的流程圖以下所示:

image

以上部分,只介紹了應用層層面的 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)通訊)

image

這裏,詳解介紹一下升級流程中的各個模塊。

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具體的流程以下圖所示:

image

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, "\r\n"));  // Strip newline.
            }

            check_and_fclose(fp, COMMAND_FILE);
            LOGI("Got arguments from %s\n", 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: %s\n", 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包檢查時大體會分三步:

  1. 檢驗SF文件與RSA文件是否匹配;

  2. 檢驗MANIFEST.MF與簽名文件中的digest是否一致;

  3. 檢驗包中的文件與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經常使用的命令以下:

image

所以,根據上面的升級腳本,能夠知道,升級包的大體升級流程以下:

  1. 判斷是否是升級包是否適用於該設備,若是不適用,則中止升級,不然繼續。
  2. 顯示進度條
  3. 格式化system分區
  4. 掛載system分區
  5. 將ota升級包裏面的system、recovery目錄解壓到system分區
  6. 創建一些軟連接,升級過程須要用到
  7. 設置部分文件權限
  8. 將升級包裏面的boot.img寫入到/boot分區
  9. 清空misc分區,即BCB塊置爲NULL
  10. 卸載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分區下的文件進行信息交互的。具體以下:

image

其中,command的值通常有如下一個或多個:

image

其次,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四者相互影響,又獨立工做。它們之間斬不斷理還亂的關係,能夠如下圖歸納之:

image

相關文章
相關標籤/搜索