Android系统升级 Recovery模式(01)Recovery模式启动

该系列文章总纲链接:专题分纲目录 Android系统升级 Recovery模式


本章关键点总结 & 说明:

Android系统升级 Recovery模式(01)Recovery模式启动_第1张图片

导图是不断迭代的,这里主要关注➕ recovery模式启动部分即可,主要从 更新包简介,mian函数解析开始解读,分析了main函数中关键的方法 获取参数getargs和执行菜单命令prompt_and_wait。

对于recovery模式,一般均采用第三方的方案,比如:在使用MTK平台时,我们这边一般会直接采用广升FOTA的服务商直接进行升级相关的工作。对于第三方的recovery代码,实际上也是参考android原生代码进行改动,核心原理不变,因此这里对recovery模式的代码研究还是采用 google 的原生recovery代码。

android 启动时会通过组合键 判定是否进入recovery模式,也可以通过android的RecoverySystem来进入。recovery模式下还是会启动bootloader、kernel,最后会通过bootargs来判定,如果进入recovery模式则加载recovery专属的rc文件,进而进入到recovery模式。

recovery模式主要是使用升级包进行升级,升级包解压后一般是这样

Android系统升级 Recovery模式(01)Recovery模式启动_第2张图片

包含的文件简要说明:

  1. boot.img:更新boot分区所需要的文件。这个boot.img主要包括kernel+ramdisk。
  2. system/  :内容在升级后会放在系统的system分区。主要用来更新系统的一些应用或则应用会用到的一些库
  3. recovery/ :中的recovery-from-boot.p是boot.img和recovery.img的补丁(patch),主要用来更新recovery分区,其中etc/目录下的install-recovery.sh是更新脚本。
  4. META-INF放的是更新包的签名文件和更新脚本,只有更新包的签名和设备签名匹配才能进行系统升级,包含三个关键文件。

 

最后META-INF的几个文件解读如下所示:

  1. CERT.RSA:与签名文件相关联的签名程序块文件,它存储了用于签名JAR文件的公共签名。
  2. CERT.SF:这是JAR文件的签名文件,其中前缀CERT代表签名者。
  3. MANIFEST.MF:manifest文件定义了与包的组成结构相关的数据。
  4. update-binary:二进制文件,相当于脚本解释器,能够识别updater-script中描述的操作。
  5. updater-script:脚本文件,具体描述了更新过程。我们可以根据具体情况编写该脚本来适应我们的具体需求。
  6. metadata:描述设备信息及环境变量的元数据。主要包括一些编译选项,签名公钥,时间戳以及设备型号等。

接下来我们从main函数启动开始分析,到如何传递参数,以及如何执行菜单命令 角度来逐步分析。

1 main函数启动分析

int main(int argc, char **argv) {
    time_t start = time(NULL);

    redirect_stdio(TEMPORARY_LOG_FILE);
    if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
        adb_main();//如果参数中有adb,作为adbd的daemon启动
        return 0;
    }
    //读取/etc/recovery.fstab文件,保存了recovery模式下分区情况(名称+参数)
    load_volume_table();
    ensure_path_mounted(LAST_LOG_FILE);
    rotate_last_logs(KEEP_LOG_COUNT);
    //获得启动参数(按照优先级,分别是recovery->misc->/cache/recovery/command中命令)
    get_args(&argc, &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; //擦除cache
        case 't': show_text = 1; break;//指示升级时是否显示UI
        case 'x': just_exit = true; break; //退出
        case 'l': locale = optarg; break; //指定locale
        //...
        case '?':
            LOGE("Invalid command argument\n");
            continue;
        }
    }
    //...

    //Device很多函数是空函数,设计上留给厂商实现。
    Device* device = make_device();
    ui = device->GetUI();
    gCurrentUI = ui;

    ui->SetLocale(locale);
    ui->Init();

    int st_cur, st_max;
    if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) {
        ui->SetStage(st_cur, st_max);
    }

    ui->SetBackground(RecoveryUI::NONE);
    if (show_text) ui->ShowText(true);
    //SElinux相关

    device->StartRecovery();//空函数

    printf("Command:");
    for (arg = 0; arg < argc; arg++) {
        printf(" \"%s\"", argv[arg]);
    }
    printf("\n");

    if (update_package) {//预处理更新命令
        if (strncmp(update_package, "CACHE:", 6) == 0) {
            int len = strlen(update_package) + 10;
            char* modified_path = (char*)malloc(len);
            strlcpy(modified_path, "/cache/", len);
            strlcat(modified_path, update_package+6, len);
            printf("(replacing path \"%s\" with \"%s\")\n",
                   update_package, modified_path);
            update_package = modified_path;
        }
    }
    printf("\n");

    property_list(print_property, NULL);
    property_get("ro.build.display.id", recovery_version, "");
    printf("\n");

    int status = INSTALL_SUCCESS;

    if (update_package != NULL) {
        //如果更新系统
        status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
        if (status == INSTALL_SUCCESS && wipe_cache) {
            //更新成功,启动并擦出cache
            if (erase_volume("/cache")) {
                LOGE("Cache wipe (requested by package) failed.");
            }
        }
        if (status != INSTALL_SUCCESS) {
            ui->Print("Installation aborted.\n");
            char buffer[PROPERTY_VALUE_MAX+1];
            property_get("ro.build.fingerprint", buffer, "");
            if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
                ui->ShowText(true);//屏幕上打印失败版本信息
            }
        }
    } else if (wipe_data) {//擦除数据
        if (device->WipeData()) status = INSTALL_ERROR;
        if (erase_volume("/data")) status = INSTALL_ERROR;
        if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
        if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR;
        if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
    } else if (wipe_cache) {//擦除cache
        if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
        if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
    } else if (!just_exit) {//只是退出
        status = INSTALL_NONE;  // No command specified
        ui->SetBackground(RecoveryUI::NO_COMMAND);
    }

    if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
        copy_logs();
        //执行命令错误,屏幕上显示标志
        ui->SetBackground(RecoveryUI::ERROR);
    }
    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
    if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
        //进入菜单模式
        Device::BuiltinAction temp = prompt_and_wait(device, status);
        if (temp != Device::NO_ACTION) after = temp;
    }
    
    // Save logs and clean up before rebooting or shutting down.
    finish_recovery(send_intent);
    //根据after参数设置,关闭/重启系统
    switch (after) {
        case Device::SHUTDOWN:
            ui->Print("Shutting down...\n");
            property_set(ANDROID_RB_PROPERTY, "shutdown,");
            break;

        case Device::REBOOT_BOOTLOADER:
            ui->Print("Rebooting to bootloader...\n");
            property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
            break;
        default:
            ui->Print("Rebooting...\n");
            property_set(ANDROID_RB_PROPERTY, "reboot,");
            break;
    }
    sleep(5); // should reboot before this finishes
    return EXIT_SUCCESS;
}

main函数并不复杂,主要实现了几个关键功能:

  1. 加载recovery.fstab分区表
  2. 获取启动参数
  3. UI初始化 ( 厂商接口)
  4. 解析并执行相关命令(升级/擦除数据 /擦除cache/退出)
  5. 显示菜单(如果失败,显示失败的版本信息)
  6. 退出recovery,根据参数,关闭/重启系统

2 如何传递参数

2.1 关键结构体bootloader_message

Bootloader和reovery模块以及主系统分区通信主要是通过misc来完成的,misc的数据结构是bootloader_message,定义如下:

struct bootloader_message {
    char command[32];//bootloader 启动时读取改数据,决定是否进入recovery模式
    char status[32]; //由recovery或者bootloader进行更新,标识升级的结果;
    /* 由Android系统进行写入,recovery从中读取信息,以recovrery开头,
    后面是recovery中执行的命令,命令以\n分割 */
    char recovery[768];

    // The 'recovery' field used to be 1024 bytes.  It has only ever
    // been used to store the recovery command line, so 768 bytes
    // should be plenty.  We carve off the last 256 bytes to store the
    // stage string (for multistage packages) and possible future
    // expansion.
    char stage[32];
    char reserved[224];
};

command命令详细说明:

  1. 如果command的值是boot-recovery,则进入recovery模式
  2. 如果command的值是update-radia /update-hboot,则进入firmware模式,由bootloader更新
  3. 如果command的值是NULL,则进入主系统,正常启动。

recovery命令详细说明:存放的是recovery模块启动时的参数。

2.2 getargs获取参数(misc分区相关)

代码如下:

static void get_args(int *argc, char ***argv) {
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    //读取misc分区命令到boot变量中
    get_bootloader_message(&boot);  // this may fail, leaving a zeroed structure
    stage = strndup(boot.stage, sizeof(boot.stage));
    //...
    if (*argc <= 1) {//如果命令没有传递参数
        boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
        const char *arg = strtok(boot.recovery, "\n");
        //从misc分区中读取命令建立启动参数,这样argc和argv就会有新的值。
        if (arg != NULL && !strcmp(arg, "recovery")) {
            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
            (*argv)[0] = strdup(arg);
            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
                if ((arg = strtok(NULL, "\n")) == NULL) break;
                (*argv)[*argc] = strdup(arg);
            }
        } else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
        }
    }

    if (*argc <= 1) {//如果从misc分区中没有读到命令
        //COMMAND_FILE 为/cache/recovery/command
        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;  // use the same program name

            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);  // Strip newline.
                } else {
                    --*argc;
                }
            }
            check_and_fclose(fp, COMMAND_FILE);
        }
    }
    //把启动参数放到boot对象中
    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
    int i;
    for (i = 1; i < *argc; ++i) {
        strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
        strlcat(boot.recovery, "\n", sizeof(boot.recovery));
    }
    set_bootloader_message(&boot);
}

这里继续分析上面提到的关键点

@1 get_bootloader_message的实现如下:

int get_bootloader_message(struct bootloader_message *out) {
    Volume* v = volume_for_path("/misc");//打开misc分区
    //...
    if (strcmp(v->fs_type, "mtd") == 0) {//mtd格式
        return get_bootloader_message_mtd(out, v);
    } else if (strcmp(v->fs_type, "emmc") == 0) {//emmc格式
        return get_bootloader_message_block(out, v);
    }
    LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
    return -1;
}

该方法主要是从misc分区读取数据。

@2 set_bootloader_message的实现如下:

int set_bootloader_message(const struct bootloader_message *in) {
    Volume* v = volume_for_path("/misc");
    //...
    if (strcmp(v->fs_type, "mtd") == 0) {
        return set_bootloader_message_mtd(in, v);
    } else if (strcmp(v->fs_type, "emmc") == 0) {
        return set_bootloader_message_block(in, v);
    }
    LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
    return -1;
}

这里把参数回写到misc分区,这样做是为了防止升级过程中发生崩溃,重启仍然可以从misc分区读取到更新的命令,继续进行更新操作,这也是为什么getargs要从几个地方读取启动参数的原因。

@3 finish_recovery函数

如果recovery模式下正常退出,则会清理掉misc分区中的内容,关键代码内容如下:

static void finish_recovery(const char *send_intent) {
    //...
    // Reset to normal system boot so recovery won't cycle indefinitely.
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    set_bootloader_message(&boot);
    //...
    sync();  // For good measure.
}

这里就是向misc分区中写入0。

3 执行菜单命令

执行菜单关键的函数是prompt_and_wait,这里打印屏幕菜单并接收用户输入,函数代码如下:

static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
    const char* const* headers = prepend_title(device->GetMenuHeaders());
    for (;;) {
        finish_recovery(NULL);
        //根据命令执行,修改UI背景
        switch (status) {
            case INSTALL_SUCCESS:
            case INSTALL_NONE:
                ui->SetBackground(RecoveryUI::NO_COMMAND);
                break;

            case INSTALL_ERROR:
            case INSTALL_CORRUPT:
                ui->SetBackground(RecoveryUI::ERROR);
                break;
        }
        ui->SetProgressType(RecoveryUI::EMPTY);
        //等待用户输入
        int chosen_item = get_menu_selection(headers, device->GetMenuItems(), 0, 0, device);
        //用户选择权 交给device对象处理
        Device::BuiltinAction chosen_action = device->InvokeMenuItem(chosen_item);

        int wipe_cache = 0;
        //处理菜单命令
        switch (chosen_action) {
            case Device::NO_ACTION:
                break;

            case Device::REBOOT:
            case Device::SHUTDOWN:
            case Device::REBOOT_BOOTLOADER:
                return chosen_action;

            case Device::WIPE_DATA:
                wipe_data(ui->IsTextVisible(), device);
                if (!ui->IsTextVisible()) return Device::NO_ACTION;
                break;

            case Device::WIPE_CACHE:
                ui->Print("\n-- Wiping cache...\n");
                erase_volume("/cache");
                ui->Print("Cache wipe complete.\n");
                if (!ui->IsTextVisible()) return Device::NO_ACTION;
                break;

            case Device::APPLY_EXT: {//sdcard卡上更新
                ensure_path_mounted(SDCARD_ROOT);
                char* path = browse_directory(SDCARD_ROOT, device);
                if (path == NULL) {
                    ui->Print("\n-- No package file selected.\n", path);
                    break;
                }
                //...
                break;
            }
            //...
            case Device::APPLY_ADB_SIDELOAD://启动adbd,也是卡刷入口
                status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);
                //...
                break;
        }
    }
}

整个函数的逻辑是:在屏幕上打印菜单,之后等待用户输入(用户只能通过音量+ 音量-来上下选择,power键确认),用户输入后,根据用户输入的命令,做不同的处理,命令与处理的对应关系如下:

命令 处理方式
REBOOT 重启
WIPE_DATA 擦除data分区,这就是恢复出厂设置所进行的操作,清除手机上所有的用户数据,包括cache分区
WIPE_CACHE 仅擦出cache分区下的内容
APPLY_EXT 通过UI在sdcard上选择一个文件进行更新操作
APPLY_CACHE 同上,只是路径变成了cache
APPLY_ADB_SIDELOAD 启动adbd(注意:这里adbd只是一个mini版本),让用户通过adb连接来执行sideload命令上传,更新文件到/tmp/update.zip,然后再执行更新操作。

你可能感兴趣的:(android)