Android(安卓)Recovery OTA升级(二)—— Recovery源码解析
目录
- 目录
- 概述
- Recovery源码解析
- main函数
- 输出重定向
- 填充fstab结构体
- 挂载cache分区
- 获取Recovery命令参数
- 分析recovery命令
- install_package
- load_keys加载公钥原文件
- verify_file对升级包进行签名校验
- mzOpenZipArchive-打开升级包获取相关信息
- try_update_binary-ota真正执行的地方
概述
之前博客里一篇文章讲解了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 32Certificate* 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 1int 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函数了。
更多相关文章
- 生命周期组件 Lifecycle 源码解析(一)
- 【Android(安卓)Native Code开发系列】二 Ubuntu系统安装及软硬
- Android的开机流程
- Android(安卓)Activity 知识点全面总结
- 孤岛能源安卓游戏源码
- Android(安卓)app security安全问题总结
- Android(安卓)Material Design 组件集合Demo,附源码地址 :CimoGall
- android 源码 来电流程 详解
- Android(安卓)adb shell操作时出现“ XXX ... Read-only file sy