1. 程式人生 > >Android Recovery OTA升級(二)—— Recovery原始碼解析

Android Recovery OTA升級(二)—— Recovery原始碼解析

目錄

概述

之前部落格裡一篇文章講解了OTA包的生成原理,這篇文章主要是從Recovery原始碼的角度介紹一下Recovery是如何使用OTA包進行系統升級的。

為了防止洩密,本文原始碼都是基於Android4.4.2_r1版本進行分析。

Recovery原始碼解析

Recovery原始碼的入口位置為:bootable/recovery/recovery.cpp檔案。下面我就來分析一下Recovery的原始碼。

static const char *CACHE_LOG_DIR = "/cache/recovery";
static const char *COMMAND_FILE = "/cache/recovery/command"
; static const char *INTENT_FILE = "/cache/recovery/intent"; static const char *LOG_FILE = "/cache/recovery/log";

註釋裡英文寫的很清楚:

The recovery tool communicates with the main system through /cache files.
/cache/recovery/command - INPUT - command line for tool, one arg per line
/cache/recovery/log - OUTPUT - combined log file from recovery run(s)
/cache/recovery/intent - OUTPUT - intent that was passed in

同時,程式碼裡還有一段對Recovery識別命令的註釋描述:

The arguments which may be supplied in the command file:
1. –send_intent=anystring - write the text out to recovery.intent
2. –update_package=path - verify install an OTA package file
3. –wipe_data - erase user data (and cache), then reboot
4. –wipe_cache - wipe cache (but not user data), then reboot

main函式

接下面,我們分析一下recovery.c的入口main函式。

輸出重定向

static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log"
int main(int argc, char **argv)
{
    time_t start = time(NULL);
    freopen(TEMPORARY_LOG_FILE, "a", stdout);
    setbuf(stdout, NULL);
    freopen(TEMPORARY_LOG_FILE, "a", stderr);
    setbuf(stderr, NULL);
    // 列印啟動recovery的時間
    printf("Starting recovery on %s", ctime(&start));
}

該部分的主要作用是:將標準輸出和錯誤輸出重定向到/tmp/recovery.log檔案中。

填充fstab結構體

struct fstab {
    int num_entries;
    struct fstab_rec *recs;
    char *fstab_filename;
};
struct fstab_rec {
    char *blk_device;
    char *mount_point;
    char *fs_type;
    unsigned long flags;
    char *fs_options;
    int fs_mgr_flags;
    char *key_loc;
    char *verity_loc;
    long long length;
    char *label;
    int partnum;
    int swap_prio;
    unsigned int zram_size;
};
typdef struct fstab_rec Volume;

void load_volume_table()
{
    int i;
    int ret;

    // 解析/etc/recovery.fstab配置檔案,填充fstab結構體
    fstab = fs_mgr_read_fstab("/etc/recovery.fstab");
    if (!fstab) {
        LOGE("failed to read /etc/recovery.fstab\n");
        return;
    }

    // 在fstab結構體中增加/tmp分割槽
    ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk", 0);
    if (ret < 0) {
        LOGE("failed to add /tmp entry to fstab\n");
        fs_mgr_free_fstab(fstab);
        fstab = NULL;
        return;
    }

    printf("recovery filesystem table\n");
    printf("=========================\n");
    for (i = 0; i < fstab->num_entries; i ++) {
        Volume* v = &fstab->recs[i];
        printf("  %d %s %s %s %lld\n", i, v->mount_point, v->fs_type,
                v->blk_device, v->length);
    }
    printf("\n");
}

int main(int argc, char **argv)
{
    load_volume_table();
}

該部分程式碼的主要作用是用recovery根目錄下的/etc/recovery.fstab中的分割槽內容和/tmp分割槽內容來填充了fstab結構體,並沒有真正的進行分割槽掛載。

掛載cache分割槽

原始碼分析如下:

typedef struct fstab_rec Volume;
#define LAST_LOG_FILE "/cache/recovery/last_log"

int ensure_path_mounted(const char* path)
{
    // 這裡是在fstab結構體中找到掛載點為/cache的fstab_recs
    Volume* v = volume_for_path(path);
    if (v == NULL) {
        LOGE("unknown volume for path [%s]\n", path);
        return -1;
    }

    // ramdisk型別的分割槽是一直掛載的。
    if (strcmp(v->fs_type, "ramdisk") == 0) {
        return 0;
    }

    int result;
    result = scan_mounted_volumes();
    const MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point);
    if (mv) {
        // 說明當前分割槽已經被掛載了
        return 0;
    }

    // 下面是具體的掛載過程
    mkdir(v->mount_point, 0755);
    if (strcmp(v->fs_type, "yaffs2") == 0) {
        // .......不用管這個yaffs2分割槽型別了,目前基本是ext4的。
    } else if (strcmp(v->fs_type, "ext4") == 0 || strcmp(v->fs_type, "vfat") == 0) {
        // ext4和vfat型別的分割槽呼叫mount函式進行掛載,掛載成功返回0,失敗返回-1
        result = mount(v->blk_device, v->mount_point, v->fs_type, MS_NOATIME | MS_NODEV | MS_NODIRATIME, "");
        if (result == 0) return 0;
        LOGE("failed to mount %s (%s)\n", v->mount_point, strerror(errno));
        return -1;
    }

    LOGE("unknown fs_type \"%s\" for %s\n", v->fs_type, v->mount_point);
    return -1;
}

#define LAST_LOG_FILE "/cache/recovery/last_log"
int main(int argc, char **argv)
{
    ensure_path_mounted(LAST_LOG_FILE);
    // 這個不用care了,就是重新命名了log
    rotate_last_logs(10);
}

從原始碼可以看出,ensure_path_mounted(LAST_LOG_FILE);程式碼的主要作用是保證/cache分割槽被掛載。

獲取Recovery命令引數

原始碼分析如下:

struct bootloader_message
{
    char command[32];
    char status[32];
    char recovery[768];

    char stage[32];
    char reverse[224];
};

int get_bootloader_message(struct bootloader_message *out) {
    Volume* v = volume_for_path("/misc");
    if (v == NULL) {
        return -1;
    }

    if (strcmp(v->fs_type, "mtd") == 0) {
        return get_bootloader_message_mtd(out, v);
    } else if (strcmp(v->fs_type, "emmc") == 0){
        return get_bootloader_message_block(out, v);
    }
    return -1;
}

static const char *COMMAND_FILE = "/cache/recovery/command";
static const int MAX_ARGS = 100;
static void get_args(int *argc, char ***argv)
{
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));

    // 首先從MISC分割槽中讀取BCB資料塊到boot變數中,可能存在為空的情況。
    get_bootloader_message(&boot);
    stage = strndup(boot.stage, sizeof(boot.stage));

    // 從/cache/recovery/command獲取引數,一般常用的ota升級做法。
    if (*argc < 1) {
        FILE *fp = fopen_path(COMMAND_FILE, "r");
        if (fp != NULL) {
            char *token;
            char *argv0 = (*argv)[0];
            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
            (*argv)[0] = argv0;

            char buf[MAX_ARG_LENGTH];
            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
                if (!fgets(buf, sizeof(buf), fp)) break;
                token = strtok(buf, "\r\n");
                if (token != NULL) {
                    (*argv)[*argc] = strdup(token);
                } else {
                    --*argc;
                }
            }
            check_and_fclose(fp, COMMAND_FILE);
        }
    }

    // ....省略部分程式碼,程式碼的作用就是將從/cache/recovery/command獲取的引數寫入到misc分割槽
}

int main(int argc, char **argv)
{
    get_args(&argc, &argv);
}

可以看到,對於ota升級來說,get_args函式的作用就是讀取/cache/recovery/command檔案,將引數存到argv二維陣列中。

分析recovery命令

原始碼如下:

int main(int argc, char **argv)
{
    const char *send_intent = NULL;
    const char *update_package = NULL;
    int wipe_data = 0, wipe_cache = 0, show_text = 0;
    bool just_exit = false;
    bool shutdown_after = false;

    int arg;
    while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
        switch(arg) {
        case 's': send_intent = optarg; break;
        case 'u': update_package = optarg; break;
        case 'w': wipe_data = wipe_cache = 1; break;
        case 'c': wipe_cache = 1; break;
        case 't': show_text = 1; break;
        case 'x': just_exit = true; break;
        case 'l': locale = optarg; break;
        case 'g':
            if (stage == NULL || stage == '\0') {
                char buffer[20] = "1/";
                strncat(buffer, optarg, sizeof(buffer)-3);
                stage = strdup(buffer);
            }
        case 'p': shutdown_after = true; break;
        case '?':
            LOGE("Invalid command argument\n");
            continue;
        }
    }
}

賦值結束後,就是UI和真正的OTA升級過程了。

install_package

原始碼如下:

// 解除安裝除了/tmp和/cache的其他分割槽
int setup_install_mounts() {
    if (fstab == NULL) {
        LOGE("can't set up install mounts: no fstab loaded\n");
        return -1;
    }

    for (int i = 0; i < fstab.num_entries; i ++) {
        Volume* v = fstab->recs + i;

        if (strcmp(v->mount_point, "/tmp") == 0 ||
            strcmp(v->mount_point, "/cache") == 0) {
            if (ensure_path_mounted(v->mount_point) != 0) return -1;
        } else {
            if (ensure_path_unmounted(v->mount_point) != 0) return -1;
        }
    }

    return 0;
}

// ota升級的真正實現,這裡去除掉跟UI顯示相關的邏輯
#define PUBLIC_KEYS_FILE "/res/keys"
static int really_install_package(const char *path, int* wipe_cache)
{
    // 確保zip包所在的目錄是掛載的
    if (ensure_path_mounted(path) != 0) {
        LOGE("Can't mount %s\n", path);
        return INSTALL_CORRUPT;
    }

    // 載入公鑰原始檔,根據公鑰對zip包進行校驗
    int numKeys;
    Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
    err = verify_file(path, loadedKeys, numKeys);

    // 開啟升級包,並將相關的資訊拷貝到一個臨時的ZipArchinve變數中。這一步並未對我們的update.zip包解壓。
    ZipArchive zip;
    err = mzOpenZipArchive(path, &zip);

    // 真正fota升級的過程
    return try_update_binary(path, &zip, wipe_cache);
}

int install_package(const char* path, int* wipe_cache, const char* install_file)
{
    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;
    if (setup_install_mounts() != 0) {
        result = INSTALL_ERROR;
    } else {
        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;
}

static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
int main(int argc, char **argv)
{
    if (update_package != NULL) {
        status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE);
        if (status == INSTALL_SUCCESS && wipe_cache) {
            if (erase_volume("/cache")) {
                LOGE("Cache wipe (requested by package) failed.");
            }
        }

        if (status != INSTALL_SUCCESS) {
            ui->Print("Installation aborted.\n");
        }
    }
}

接下來,分別講解一下really_install_package中具體函式實現。

load_keys——載入公鑰原檔案

load_keys原始碼實現如下:

#define RSANUMBYTES 256
#define RSANUMWORDS (RSANUMBYTES / sizeof(uint32_t))

typedef struct RSAPublicKey {
    int len;
    uint32_t n0inv;
    uint32_t n[RSANUMWORDS];
    uint32_t rr[RSANUMWORDS];
    int exponent;
} RSAPublicKey;

typedef struct Certificate {
    int hash_len;
    RSAPublicKey* public_key;
} Certificate;

#define SHA_DIGEST_SIZE 20
#define SHA256_DIGEST_SIZE 32

Certificate* load_keys(const char *filename, int *numKeys)
{
    Certificate *out = NULL;
    *numKeys = 0;

    // 開啟recovery根目錄下的/res/keys檔案
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        goto exit;
    }

    {
        int i;
        bool done = false;
        while (! done) {
            ++ *numKeys;
            out = (Certificate *)realloc(out, *numKeys * sizeof(Certificate));
            Certificate *cert = out + (*numKeys - 1);
            cert->public_key = (RSAPublicKey*)malloc(sizeof(RSAPublicKey));

            // 分析第一個字元,獲取版本和長度
            char start_char;
            if (fscanf(f, "%c", &start_char) != 1) goto exit;
            if (start_char == '{') {
                cert->public_key->exponent = 3;
                cert->hash_len = SHA_DIGEST_SIZE;
            } else if (start_char == 'v') {
                int version;
                if (fscanf(f, "%d {", version) != 1) goto exit;
                switch (version) {
                    case 2:
                        cert->public_key->exponent = 65537;
                        cert->hash_len = SHA_DIGEST_SIZE;
                        break;
                    case 3:
                        cert->public_key->exponent = 3;
                        cert->hash_len = SHA256_DIGEST_SIZE;
                        break;
                    case 4:
                        cert->public_key->exponent = 65537;
                        cert->hash_len = SHA256_DIGEST_SIZE;
                        break;
                    default:
                        goto exit;
                }
            }

            RSAPublicKey *key = cert->public_key;
            if (fscanf(f, " %i , 0x%x , { %u", &(key->len), &(key->n0inv), &(key->n[0])) != 3) goto exit;
            // 依次讀取key->len個數字到key->n[i]中
            for (i = 0; i < key->len; i ++) {
                if (fscanf(f, " , %u", &(key->n[i])) != 1) goto exit;
            }
            // 再次讀取(key->len + 1)個數字到key->rr陣列中
            if (fscanf(f, " } , { %u", &(key->rr[0])) != 1) goto exit;
            for (i = 0; i < key->len; i ++) {
                if (fscanf(f, " , %u", &(key->rr[i])) != 1) goto exit;
            }
            fscanf(f, " } } ");

            switch (fgetc(f)) {
                case ',':
                    // 還有需要load的key
                    break;
                case EOF:
                    done = true;
                    break;
                default:
                    goto exit;
            }
        }        
    }

    fclose(f);
    return out;

exit:
    if (f) fclose(f);
    free(out);
    *numKeys = 0;
    return NULL;
}

原始碼還是很簡單的,就是解析/res/keys檔案,將該檔案裡面的校驗key儲存到Certificate* loadedKeys中。

verify_file——對升級包進行簽名校驗

verify_file函式的原始碼如下:

#define VERIFY_SUCCESS 0
#define VERIFY_FAILURE 1

int verify_file(const char* path, const Certificate* pKeys, unsigned int numKeys) {
    FILE *f = fopen(path, "rb");
    if (f == NULL) {
        return VERIFY_FAILURE;
    }

// 從檔案最後6個字元開始校驗,並獲取comment_size和signature_start等資訊
#define FOOTER_SIZE 6
    if (fseek(f, -FOOTER_SIZE, SEEK_END) != 0) {
        fclose(f);
        return VERIFY_FAILURE;
    }

    unsigned char footer[FOOTER_SIZE];
    if (fread(footer, 1, FOOTER_SIZE, f) != FOOTER_SIZE) {
        fclose(f);
        return VERIFY_FAILURE;
    }
    if (footer[2] != 0xff || footer[3] != 0xff) {
        fclose(f);
        return VERIFY_FAILURE;
    }
    size_t comment_size = footer[4] + (footer[5] << 8);
    size_t signature_start = footer[0] + (footer[1] << 8);
    if (signature_start - FOOTER_SIZE < RSANUMBYTES) {
        fclose(f);
        return VERIFY_FAILURE;
    }

// 校驗從檔案末尾開始的倒數eocd_size個字元
#define EOCD_HEADER_SIZE 22
    size_t eocd_size = comment_size + EOCD_HEADER_SIZE;
    if (fseek(f, -eocd_size, SEEK_END) != 0) {
        fclose(f);
        return VERIFY_FAILURE;
    }

    // ... 省略,感興趣的自己看原始碼

#define BUFFER_SIZE 4096
    // ... rsa校驗,感興趣的自己看原始碼
}

verify_file函式校驗失敗的話,ota過程會直接返回校驗失敗的錯誤,不再進行ota升級流程。

mzOpenZipArchive-開啟升級包,獲取相關資訊

mzOpenZipArchive函式的作用是:開啟升級包,並將相關的資訊拷貝到一個臨時的ZipArchinve變數中。具體實現程式碼如下:

typedef struct MemMapping {
    void *addr;
    size_t length;

    void* baseAddr;
    size_t baseLength;
} MemMapping;

int mzOpenZipArchive(const char* fileName, ZipArchive* pArchive)
{
    MemMapping map;
    int err;

    map.addr = NULL;
    memset(pArchive, 0, sizeof(*pArchive));

    pArchive->fd = open(fileName, O_RDONLY, 0);
    if (pArchive->fd < 0) {
        goto bail;
    }

    // 呼叫mmap將zip檔案對映到內容中,起始地址儲存到map->baseAddr和map->addr,對映長度儲存在map->baseLength和map->length中。
    if (sysMapFileInShmem(pArchive->fd, &map) != 0) {
        goto bail;
    }

    // 解析map中的內容到pArhchive中(其中,map是zip包檔案在記憶體中的對映)
    if (!parseZipArchive(pArchive, &map)) {
        goto bail;
    }
    sysCopyMap(&pArchive->map, &map);
    map->addr = NULL;
bail:
    // 資源回收
}

try_update_binary-ota真正執行的地方

try_update_binary才是recovery對update.zip真正開始進行升級的地方。原始碼如下:

static int try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache)
{
    // 將zip包META-INF/com/google/android/update-binary檔案內容儲存到binary_entry結構體中
    const ZipEntry* binary_entry = mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);

    // 這裡是將binary_entry結構體的內容儲存到/tmp/update_binary檔案中
    // 其實說白了,就是將ota zip包中的META-INF/com/google/android/update-binary複製到recovery的/tmp/update_binary檔案中
    const char* binary = "/tmp/update_binary";
    unlink(binary);
    int fd = creat(binary, 0755);
    bool ok = mzExtractZipEntryToFile(META-INF/com/google/android/update-binaryzip, binary_entry, fd);
    close(fd);
    mzCloseZipArchive(zip);

    // 建立管道,用於子程序和父程序的通訊
    int pipefd[2];
    pipe(pipefd);

    // 
    const char** args = (const char**)malloc(sizeof(char*) * 5);
    args[0] = binary;
    args[1] = EXPAND(RECOVERY_API_VERSION);
    char* temp = (char*)malloc(10);
    sprintf(temp, "%s", pipefd[1]);
    args[2] = temp;
    args[3] = (char *)path;
    args[4] = NULL;

    pid_t pid = fork();
    // 子程序執行update_binary,進行刷機操作
    if (pid == 0) {
        // 子程序傳送訊息,所以關閉receive fd.
        close(pipefd[0]);
        // 執行binary,即執行/tmp/update_binary,需要去研究update.c的原始碼
        execv(binary, (char* const*)args);
    }

    // 父程序用來接收訊息,所以關閉send fd.
    close(pipefd[1]);


    char buffer[1024];
    FILE* from_child = fdopen(pipefd[0], "r");
    while (fgets(buffer, sizeof(buffer), from_child) != NULL) {
        char* command = strtok(buffer, " \n");
        if (command == NULL) {
            continue;
        } else if (strcmp(command, "progress") == 0) {
            // 父程序顯示UI進度
        } else if (strcmp(command, "set_progress") == 0) {
            // 父程序設定UI進度
        } else if (strcmp(command, "ui_print") == 0) {
            // 父程序列印資訊
        } else if (strcmp(command, "wipe_cache") == 0) {
            // 清除cache分割槽
        } else if (strcmp(command, "special_factory_reset") == 0) {
            // 清除data和cache分割槽
        } else if (strcmp(command, "clear_display") == 0) {
            // 清除UI顯示
        }
    }
    fclose(from_child);

    int status;
    waitpid(pid, &status, 0);
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        return INSTALL_ERROR;
    }

    return INSTALL_SUCCESS;
}

需要注意的是:execv(binary,args)的作用就是去執行binary程式,這個程式的實質就是去解析update.zip包中的updater-script指令碼中的命令並執行。由此,Recovery服務就進入了實際安裝update.zip包的過程。繼續分析,則需要分析updater.c檔案的main函數了。