Android 8.0 recovery 流程分析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ShuttleCheng/article/details/79186756

这里主要分析non A/B模式下的recovery流程
A/B模式下的recovery在boot中
后续会不断补充,如果有疏漏或者错误的地方,请指出,共同学习,谢谢!

一、流程分析

首先列出recovery流程的几个重要点,接着会详细分析

  1. 加载recovery.fstab分区表
  2. 解析传入的参数
  3. recovery界面相关的设置
  4. 执行命令
  5. 如果没有命令,等待用户输入
  6. 结束recovery

1.加载分区表

首先看recovery.cpp的main函数,后面会解释为什么

[recovery.cpp]
int main(int argc, char **argv) {
    //因为这个时候还没启动logcat,这里应该是设置将log打印到屏幕上和recovery.log中
    android::base::InitLogging(argv, &UiLogger);
    //下面是加载pmsg log文件
    // Take last pmsg contents and rewrite it to the current pmsg session.
    static const char filter[] = "recovery/";
    // Do we need to rotate?
    //这里应该和重命名log文件有关
    bool doRotate = false;

    __android_log_pmsg_file_read(
        LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter,
        logbasename, &doRotate);
    // Take action to refresh pmsg contents
    __android_log_pmsg_file_read(
        LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter,
        logrotate, &doRotate);
    //看注释,这里是启动迷你版的adbd,为了使用adb sideload命令
    if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
        minadbd_main();
        return 0;
    }
    //猜测这里是把log输出重定向到/tmp/recovery.log
    redirect_stdio(TEMPORARY_LOG_FILE);
    //加载recovery.fstab并建立分区表信息,在etc/目录下,recovery模式连adb可以看到
    load_volume_table();
    //从上面建立的分区表信息中读取是否有cache分区,因为log等重要信息都存在cache分区里
    has_cache = volume_for_path(CACHE_ROOT) != nullptr;
    ..........
}

详细看下是如何加载分区表的

[roots.cpp]
void load_volume_table()
{
    int i;
    int ret;

    fstab = fs_mgr_read_fstab_default();
    if (!fstab) {
        LOG(ERROR) << "failed to read default fstab";
        return;
    }
    //将对应的信息加入到一条链表中
    ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
    if (ret < 0 ) {
        LOG(ERROR) << "failed to add /tmp entry to fstab";
        fs_mgr_free_fstab(fstab);
        fstab = NULL;
        return;
    }

    printf("recovery filesystem table\n");
    printf("=========================\n");
    //在last_log中打印分区表信息
    //打印的顺序是:
    //编号  |  挂载节点  |  文件系统类型  |  块设备  |  长度
    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");
}

跟踪fs_mgr_read_fstab_default

[fs_mgr_fstab.cpp]
struct fstab *fs_mgr_read_fstab_default()
{
    std::string hw;
    std::string default_fstab;

    //下面应该是去其它位置查询fstab文件,由于/sbin/recovery是有的,这里default_fstab是"/etc/recovery.fstab"
    //A/B和non A/B下fstab文件是不同的
    if (access("/sbin/recovery", F_OK) == 0) {
        default_fstab = "/etc/recovery.fstab";
    } else if (fs_mgr_get_boot_config("hardware", &hw)) {  // normal boot
        for (const char *prefix : {"/odm/etc/fstab.","/vendor/etc/fstab.", "/fstab."}) {
            default_fstab = prefix + hw;
            if (access(default_fstab.c_str(), F_OK) == 0) break;
        }
    } else {
        LWARNING << __FUNCTION__ << "(): failed to find device hardware name";
    }

    //fs_mgr_read_fstab_dt函数是读取/proc/device-tree/firmware/android/fstab文件
    //fs_mgr_read_fstab是解析/etc/recovery.fstab文件
    //in_place_merge将上面读取的结果合并
    struct fstab *fstab_dt = fs_mgr_read_fstab_dt();
    struct fstab *fstab = fs_mgr_read_fstab(default_fstab.c_str());
    return in_place_merge(fstab_dt, fstab);
}

2.解析参数

[recovery.cpp]
int main(int argc, char **argv) {
    ........
    //通过get_args方法获取传入的参数,顺便解析到args中,后面会在log中打出参数
    //get_args方法获取misc分区的command文件
    std::vector<std::string> args = get_args(argc, argv);
    std::vector<char*> args_to_parse(args.size());
    std::transform(args.cbegin(), args.cend(), args_to_parse.begin(),
                   [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
    //调用getopt_long解析参数,这是固定用法,可以网上搜索资料,大致意思就是根据OPTIONS,将参数转换成对应的字符
    //例如:传入的参数是"Command: "/sbin/recovery" "--update_package=/sdcard/update.zip" "--launch_app=update_launcher" "--requester=com.asus.UpdateLauncher" "--locale=zh_CN_#Hans""
    //其中包含update_package,那么getopt_long依次返回的结果就是"u" "l"
    while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", OPTIONS,
                              &option_index)) != -1) {
    }
    //加载客制化的东西,比如语言之类的
    // load_locale_from_cache不展开说了,大致过程就是从之前解析分区表得到的fstab中查询/cache/recovery/last_locale文件是否存在,如果存在就读取里面的值
    if (locale.empty()) {
        if (has_cache) {
            locale = load_locale_from_cache();
        }

        if (locale.empty()) {
            locale = DEFAULT_LOCALE;
        }
    }
    ........
}

获取参数比较重要,仔细看看:

//提前了解下bootloader_message结构体
struct bootloader_message {
    char command[32];
    char status[32];
    char recovery[768];

    char stage[32];

    char reserved[1184];
};


static std::vector<std::string> get_args(const int argc, char** const argv) {
  CHECK_GT(argc, 0);

  bootloader_message boot = {};
  std::string err;
  //通过函数从misc分区中去读BCB数据库到boot变量中
  if (!read_bootloader_message(&boot, &err)) {
    LOG(ERROR) << err;
    // If fails, leave a zeroed bootloader_message.
    boot = {};
  }
  stage = std::string(boot.stage);

  if (boot.command[0] != 0) {
    std::string boot_command = std::string(boot.command, sizeof(boot.command));
    LOG(INFO) << "Boot command: " << boot_command;
  }

  if (boot.status[0] != 0) {
    std::string boot_status = std::string(boot.status, sizeof(boot.status));
    LOG(INFO) << "Boot status: " << boot_status;
  }

  std::vector<std::string> args(argv, argv + argc);

  //如果传入参数为空,先看misc分区中的 bootloader_message是否有内容
  if (args.size() == 1) {
    boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
    std::string boot_recovery(boot.recovery);
    std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
    if (!tokens.empty() && tokens[0] == "recovery") {
      for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
        // Skip empty and '\0'-filled tokens.
        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
      }
      LOG(INFO) << "Got " << args.size() << " arguments from boot message";
    } else if (boot.recovery[0] != 0) {
      LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
    }
  }

  //如果参数为空,并且有cache分区,那么解析"/cache/recovery/command"中内容
  if (args.size() == 1 && has_cache) {
    std::string content;
    if (ensure_path_mounted(COMMAND_FILE) == 0 &&
        android::base::ReadFileToString(COMMAND_FILE, &content)) {
      std::vector<std::string> tokens = android::base::Split(content, "\n");
      // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
      // COMMAND_FILE doesn't use filename as the first argument).
      for (auto it = tokens.begin(); it != tokens.end(); it++) {
        // Skip empty and '\0'-filled tokens.
        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
      }
      LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
    }
  }
  //将得到的参数写入到misc分区中
  std::vector<std::string> options(args.cbegin() + 1, args.cend());
  if (!update_bootloader_message(options, &err)) {
    LOG(ERROR) << "Failed to set BCB message: " << err;
  }

  return args;
}

3.recovery界面设置

[recovery.cpp]
int main(int argc, char **argv) {
    ........
    //设置更新的类型,如果参数中没有--security,那么在安装界面会显示”正在安装系统更新”,否则显示“正在安装安全更新”
    //这里会调用SetSystemUpdateText 方法把显示哪种文字的选择存在installing_text中,后面解析具体命令的时候会调用GetCurrentText来显示
    ui->SetSystemUpdateText(security_update);
    //显示recovery的背景
    ui->SetBackground(RecoveryUI::NONE);
    //参数中没有show_text,此处为false
    //ShowText的意思是显示菜单,Android默认不显示的
    if (show_text) ui->ShowText(true);
    //下面设置selinux权限
    //相关文件:plat_file_contexts,nonplat_file_contexts,存放在recovery模式根目录下
    sehandle = selinux_android_file_context_handle();
    selinux_android_set_sehandle(sehandle);
    if (!sehandle) {
        ui->Print("Warning: No file_contexts\n");
    }
    //啥都没做
    device->StartRecovery();
    //把手机中的属性值打出来
    property_list(print_property, NULL);
    ........
}

4.执行命令

[recovery.cpp]
int main(int argc, char **argv) {
    ........
    //下面就是具体升级的地方,重点!
    int status = INSTALL_SUCCESS;
    if (update_package != NULL) {

        modified_flash = true;
        //首先判断电量是否OK,这里用的是系统提供的API
//is_battery_ok()方法的判断逻辑是:
1.手机非充电模式下,电量大于20%
2.手机充电模式下,注意,这里充电必须使用AC电源充电,USB连接电脑是不行的,这种情况下电量大于15%
在Android.mk中有”LOCAL_HAL_STATIC_LIBRARIES := libhealthd”这就是应用的库
recovery.cpp中包含”#include <healthd/BatteryMonitor.h>”
        if (!is_battery_ok()) {
            ui->Print("battery capacity is not enough for installing package, needed is %d%%\n",
                      BATTERY_OK_PERCENTAGE);
            // Log the error code to last_install when installation skips due to
            // low battery.
            log_failure_code(kLowBattery, update_package);
            status = INSTALL_SKIPPED;
        } else if (bootreason_in_blacklist()) { //这里是判断重启的原因,看看是否是非法的
            // Skip update-on-reboot when bootreason is kernel_panic or similar
            ui->Print("bootreason is in the blacklist; skip OTA installation\n");
            log_failure_code(kBootreasonInBlacklist, update_package);
            status = INSTALL_SKIPPED;
        } else {
            //真正执行升级的地方
            status = install_package(update_package, &should_wipe_cache,
                                     TEMPORARY_INSTALL_FILE, true, retry_count);
            //如果升级成功,并且主动要求清除cache,这里是通过升级脚本来判断的
            if (status == INSTALL_SUCCESS && should_wipe_cache) {
                wipe_cache(false, device);
            }
            if (status != INSTALL_SUCCESS) {
                ui->Print("Installation aborted.\n");
                //如果是IO出现错误,那么重新来一次
                if (status == INSTALL_RETRY && retry_count < EIO_RETRY_COUNT) {
                    copy_logs();
                    //调用set_retry_bootloader_message向bootloader_message写命令
                    set_retry_bootloader_message(retry_count, args);
                    // Print retry count on screen.
                    ui->Print("Retry attempt %d\n", retry_count);

                    // Reboot and retry the update
                    if (!reboot("reboot,recovery")) {
                        ui->Print("Reboot failed\n");
                    } else {
                        while (true) {
                            pause();
                        }
                    }
                }
                // If this is an eng or userdebug build, then automatically
                // turn the text display on if the script fails so the error
                // message is visible.
                if (is_ro_debuggable()) {
                    ui->ShowText(true);
                }
            }
        }
    } else if
    //下面是各种命令,比如清除cache,恢复工厂等,不细看
    ……
    }else if (!just_exit) {    //如果传入的参数中没有指定,那么进recovery就会走到这
        status = INSTALL_NONE;  // No command specified
        //在recovery背景显示”No commnad”这是根据图片no_command_text.png来显示的
        ui->SetBackground(RecoveryUI::NO_COMMAND);

        //Android默认user版本不显示选项,只显示机器人倒地和”No command”如果想要进入recovery模式,按power键1~2s,再短按音量上键即可
        //通常要把判断移除,直接显示选项
        if (is_ro_debuggable()) {
            ui->ShowText(true);
        }
    }
    ........
}

5.如果没有指定命令,等待用户输入

[recovery.cpp]
int main(int argc, char **argv) {
    ........
    //如果升级错误,显示”Error!”,通常测试就会提供错误的图片
    if (!sideload_auto_reboot && (status == INSTALL_ERROR || status == INSTALL_CORRUPT)) {
        //将log保存下来
        copy_logs();
        ui->SetBackground(RecoveryUI::ERROR);
    }
    //如果传入的参数中没有指定,那么稍后会自动重启,如果有那么关机
    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
    //当然,这只针对user版本,因为userdebug和eng版本在上面执行了ShowText,所以下面IsTextVisible得到的结果是true,就不会重启或关机
    if ((status != INSTALL_SUCCESS && status != INSTALL_SKIPPED && !sideload_auto_reboot) ||
            ui->IsTextVisible()) {
        //prompt_and_wait()函数是个死循环 开始显示recovery选项 并处理用户通过按键或者触摸屏的选项,如重启,升级等
        Device::BuiltinAction temp = prompt_and_wait(device, status);
        if (temp != Device::NO_ACTION) {
            after = temp;
        }
    }
    ........
}

看下log如何保存的:

[recovery.cpp]
static void copy_logs() {
    //如果啥都没有改,不需要存log
    if (!modified_flash) {
        return;
    }

    // Always write to pmsg, this allows the OTA logs to be caught in logcat -L
    //这里说是将log写入到pmsg中,下次通过logcat -L就可以查看log,为啥实验不行呢,提示”logcat read failure”
    copy_log_file_to_pmsg(TEMPORARY_LOG_FILE, LAST_LOG_FILE);
    copy_log_file_to_pmsg(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE);

    // We can do nothing for now if there's no /cache partition.
    if (!has_cache) {
        return;
    }
    //确保要存入的log路径是挂载的
    ensure_path_mounted(LAST_LOG_FILE);
    ensure_path_mounted(LAST_KMSG_FILE);
    //更改log的文件名,调整顺序
    //如果有多份log,last_log是最新的,last_log.1第二,last_log.2第三,依次往后,last_kmsg同样
    rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);

    //拷贝log文件到cache/recovery/目录
    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
    //保存kernel log, 这里面能看到selinux权限的问题,挺有用的
    save_kernel_log(LAST_KMSG_FILE);
    //下面是修改文件权限
    chmod(LOG_FILE, 0600);
    chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
    chmod(LAST_KMSG_FILE, 0600);
    chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
    chmod(LAST_LOG_FILE, 0640);
    chmod(LAST_INSTALL_FILE, 0644);
    sync();
}

6.结束recovery

[recovery.cpp]
int main(int argc, char **argv) {
    ........
    // Save logs and clean up before rebooting or shutting down.
    finish_recovery();
    //下面就是检测上面的after变量,没啥
    switch (after) {
        case Device::SHUTDOWN:
            ui->Print("Shutting down...\n");
            android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
            break;

        case Device::REBOOT_BOOTLOADER:
            ui->Print("Rebooting to bootloader...\n");
            android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
            break;

        default:
            ui->Print("Rebooting...\n");
            reboot("reboot,");
            break;
    }
    while (true) {
        pause();
    }
    // Should be unreachable.
    return EXIT_SUCCESS;
}

跟踪finish_recovery():

[recovery.cpp]
static void finish_recovery() {
    //保存locale log到last_locale中,不知道这有什么用
    if (!locale.empty() && has_cache) {
        LOG(INFO) << "Saving locale \"" << locale << "\"";

        FILE* fp = fopen_path(LOCALE_FILE, "w");
        if (!android::base::WriteStringToFd(locale, fileno(fp))) {
            PLOG(ERROR) << "Failed to save locale to " << LOCALE_FILE;
        }
        check_and_fclose(fp, LOCALE_FILE);
    }
    //拷贝log,上面已经介绍过
    copy_logs();

    //因为是正常退出recovery,所以需要把BSB清除掉,否则会循环进入recovery
    std::string err;
    if (!clear_bootloader_message(&err)) {
        LOG(ERROR) << "Failed to clear BCB message: " << err;
    }

    // 把"/cache/recovery/command"清除掉,不然也会导致重新进入recovery
    if (has_cache) {
        if (ensure_path_mounted(COMMAND_FILE) != 0 || (unlink(COMMAND_FILE) && errno != ENOENT)) {
            LOG(WARNING) << "Can't unlink " << COMMAND_FILE;
        }
        ensure_path_unmounted(CACHE_ROOT);
    }

    sync();  // For good measure.
}

二、recovery总结

1.启动recovery模式的三种方式

  1. adb reboot :
    这是通过调用/sbin/recovery来启动recovery
  2. 上层系统调用升级接口,恢复出厂等:
    这是通过写命令到”/cache/recovery/command”
    从log中可以看到”I:Got arguments from /cache/recovery/command”
    疑问:打出的log应该有命令的大小才对,可是log中并没有
  3. 通过组合键(电源键+音量下键):
    调用/sbin/recovery来启动recovery

2.知识点:

  1. 如果recovery有问题启动不了,那么会卡在开机log的第二帧上
  2. recovery模式下,在sbin/目录下有recovery,和adbd等文件
    这个recovery就是recovery.cpp编译生成的可执行文件(具体可以看recovery下的Android.mk)
    我们可以试着执行这个bin文件,并且带上参数
    执行后发现会重新进入recovery,在tmp/recovery.log中会显示我们的参数
    recovery会被recovery.img执行,这就是为什么我们会从main方法开始分析
    平时使用的adb就是PC端adb通过socket连接到sbin/adbd 执行的命令都是这个adbd执行的
展开阅读全文

没有更多推荐了,返回首页