淺談Android Recovery升級

在講解recovery升級前先介紹幾個相關概念

Recovery: Recovery首先可以指Android的Recovery分區,同時也可以說是一種可以對安卓機內部的數據或系統進行修改的模式,進入該模式可以升級新的Android
OTA: Over-the-Air Technology,即空中下載技術,是 Android 系統提供的標準軟件升級方式。 它功能強大,提供了完全升級、增量升級模式,可以通過 SD 卡升級,也可以通過網絡升級。不管是哪種方式,都有幾個過程:生成升級包、下載升級包、安裝升級包。
RecoveryFs:是Recovery對應掛載的文件系統,該文件系統內部實現了一個工具類,它提供了幾個重要的API供Android應用層使用,用於實現OTA包校驗、升級以及恢復出廠設置(格式化數據和緩存)。
System:即是根文件系統,是進入正常啓動模式時掛載的系統,即Android正常開機所進入的Android系統
Bootloader:Bootloader是嵌入式系統在加電後執行的第一段代碼,在它完成CPU和相關硬件的初始化之後,再將操作系統映像或固化的嵌入式應用程序裝在到內存中然後跳轉到操作系統所在的空間,啓動操作系統運行。
Misc: Android存放啓動控制信息塊(Bootloader Control Block),從代碼上看,就是一個結構體。

應用層進入Recovery升級模式流程:

在應用層升級下載升級包後,將升級包拷貝到/cache/下,然後往 /cache/recovery/command 寫入ota升級包存放路徑“--update_package=/cache/update_ota.zip”,最後通過reboot recovery即進入recovery升級模式,開始安裝升級包。

在Recovery安裝升級包流程:

1. get_args(&argc, &argv) 

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函數核心代碼如下:

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) 
install_package函數所實現的功能爲:調用really_install_package函數進行ota升級,並將升級結果寫入到文件install_file(/cache/recovery/last_install)中。當升級完成後進入main system,可以通過此文件讀取升級結果,並給出用戶相應的提示:比如升級成功或失敗。
 

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包檢查時大致會分三步:

檢驗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");

因此,根據上面的升級腳本,可以知道,升級包的大致升級流程如下:

判斷是不是升級包是否適用於該設備,如果不適用,則停止升級,否則繼續。
顯示進度條
格式化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包升級就正式完成了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章