系统升级系列八

update-binary程序介绍(如何解析和执行updater-script)

Posted by Cheson on January 4, 2019

1 updater程序代码

接系列七里的升级流程,最终会把update.zip这个升级包通过fork一个子进程的方式去启动升级包里的update-binary这个bin档去做升级,那么来看下这个bin档的代码

代码位于bootable/recovery/updater下,就几个文件,一个Android.mk,updater.c和updater.h,install.c和install.h,main方法再updater.c中,先来看下这个文件

1-1

int main(int argc, char** argv) {
    // Various things log information to stdout or stderr more or less
    
    // at random (though we've tried to standardize on stdout).  The
    
    // log file makes more sense if buffering is turned off so things
    
    // appear in the right order.  
    
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    if (argc != 5) {
        fprintf(stderr, "unexpected number of arguments (%d)\n", argc);
        return 1;
    }

    // updater的版本号,支持1、2、3
    
    char* version = argv[1];
    if ((version[0] != '1' && version[0] != '2' && version[0] != '3') ||
        version[1] != '\0') {
        // We support version 1, 2, or 3.
        
        fprintf(stderr, "wrong updater binary API; expected 1, 2, or 3; "
                        "got %s\n",
                argv[1]);
        return 2;
    }

    // Set up the pipe for sending commands back to the parent process.

    // 跟父进程通信的pipe管道
    
    int fd = atoi(argv[2]);
    FILE* cmd_pipe = fdopen(fd, "wb");
    setlinebuf(cmd_pipe);

    // Extract the script from the package.

    // 升级包路径
    
    char* package_data = argv[3];
    ZipArchive za;
    int err;
    err = mzOpenZipArchive(package_data, &za);
    if (err != 0) {
        fprintf(stderr, "failed to open package %s: %s\n",
                package_data, strerror(err));
        return 3;
    }

    // 系统类型
    
	unsigned int system_type = *(argv[4]);
	if(check_file_exist(&za,SCRIPT_NAMEB)){//updater-scriptB不存在的时候,系统类型赋值为B系统,去升级A系统
        
		system_type = BOOT_MODE_SYSTEM_B;
	}

    // 赋值升级脚本路径
    
	char script_path[128];
	memset(script_path,0x0,sizeof(script_path));
	if(system_type == BOOT_MODE_SYSTEM_A)
		strncpy(script_path,SCRIPT_NAMEB,strlen(SCRIPT_NAMEB));
	else
		strncpy(script_path,SCRIPT_NAME,strlen(SCRIPT_NAME));
    fprintf(stderr, "run script %s(%d)\n", script_path, system_type);

    // 找到升级脚本
    
    const ZipEntry* script_entry = mzFindZipEntry(&za, script_path);
    if (script_entry == NULL) {
        fprintf(stderr, "failed to find %s in %s\n", script_path, package_data);
        return 4;
    }

    // 分配一块内存来放升级脚本的内容
    
    char* script = malloc(script_entry->uncompLen+1);
    if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) {
        fprintf(stderr, "failed to read script from package\n");
        return 5;
    }
    script[script_entry->uncompLen] = '\0';

    // Configure edify's functions.
    
    // 注册内置函数、安装函数等,参考1-2
    
    // 1-2-1
    
    RegisterBuiltins();
    // 1-2-2
    
    RegisterInstallFunctions();
    // 1-2-3
    
    RegisterDeviceExtensions();
    // 1-2-4
    
    FinishRegistration();

    // Parse the script.

    // 这一段是用yy库来做语法错误分析,如果有错这边就出错返回6了
    
    // Expr是结构体,定义在bootable/recovery/edify/expr.h中,参考1-2-5
    
    Expr* root;
    int error_count = 0;
    yy_scan_string(script);
    // 参考1-2-6
    
    int error = yyparse(&root, &error_count);
    if (error != 0 || error_count > 0) {
        fprintf(stderr, "%d parse errors\n", error_count);
        return 6;
    }

    // selinux相关
    
    struct selinux_opt seopts[] = {
      { SELABEL_OPT_PATH, "/file_contexts" }
    };

    sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);

    if (!sehandle) {
        fprintf(cmd_pipe, "ui_print Warning: No file_contexts\n");
    }

    // Evaluate the parsed script.

    UpdaterInfo updater_info;
    updater_info.cmd_pipe = cmd_pipe;
    updater_info.package_zip = &za;
    updater_info.version = atoi(version);

    State state;
    state.cookie = &updater_info;
    state.script = script;
    state.errmsg = NULL;

    // 通过edify库里的expr.c中的Evaluate来执行每条命令
    
    char* result = Evaluate(&state, root);
    if (result == NULL) {
        if (state.errmsg == NULL) {
            fprintf(stderr, "script aborted (no error message)\n");
            fprintf(cmd_pipe, "ui_print script aborted (no error message)\n");
        } else {
            fprintf(stderr, "script aborted: %s\n", state.errmsg);
            char* line = strtok(state.errmsg, "\n");
            while (line) {
                fprintf(cmd_pipe, "ui_print %s\n", line);
                line = strtok(NULL, "\n");
            }
            fprintf(cmd_pipe, "ui_print\n");
        }
        free(state.errmsg);
        return 7;
    } else {
        fprintf(stderr, "script result was [%s]\n", result);
        free(result);
    }

    if (updater_info.package_zip) {
        mzCloseZipArchive(updater_info.package_zip);
    }
    free(script);

    return 0;
}

1-2

1-2-1

代码位于bootable/recovery/edify/expr.c

void RegisterBuiltins() {
    // 注册内置函数,这些都会出现在升级脚本updater-script中的命令,对应了后面注册的函数
    
    RegisterFunction("ifelse", IfElseFn);
    RegisterFunction("abort", AbortFn);
    RegisterFunction("assert", AssertFn);
    RegisterFunction("concat", ConcatFn);
    RegisterFunction("is_substring", SubstringFn);
    RegisterFunction("stdout", StdoutFn);
    RegisterFunction("sleep", SleepFn);

    RegisterFunction("less_than_int", LessThanIntFn);
    RegisterFunction("greater_than_int", GreaterThanIntFn);
}
void RegisterFunction(const char* name, Function fn) {
    // 将命令和对应的函数名放入一个表中做维护,后续根据此表来寻找对应的函数名调用就好了
    
    if (fn_entries >= fn_size) {
        fn_size = fn_size*2 + 1;
        fn_table = realloc(fn_table, fn_size * sizeof(NamedFunction));
    }
    fn_table[fn_entries].name = name;
    fn_table[fn_entries].fn = fn;
    ++fn_entries;
}
1-2-2

代码位于bootable/recovery/updater/install.c中

void RegisterInstallFunctions() {
    // 挂载分区
    
    RegisterFunction("mount", MountFn);
    // 判断分区是否挂载
    
    RegisterFunction("is_mounted", IsMountedFn);
    // 卸载分区
    
    RegisterFunction("unmount", UnmountFn);
    // 格式化分区
    
    RegisterFunction("format", FormatFn);
    // 显示升级进度
    
    RegisterFunction("show_progress", ShowProgressFn);
    // 设置升级进度
    
    RegisterFunction("set_progress", SetProgressFn);
    // 删除
    
    RegisterFunction("delete", DeleteFn);
    // 递归删除
    
    RegisterFunction("delete_recursive", DeleteFn);
    // 解压目录
    
    RegisterFunction("package_extract_dir", PackageExtractDirFn);
    // 解压文件
    
    RegisterFunction("package_extract_file", PackageExtractFileFn);
    // 创建符号链接
    
    RegisterFunction("symlink", SymlinkFn);

    // Maybe, at some future point, we can delete these functions? They have been
    
    // replaced by perm_set and perm_set_recursive.
    
    // 设置权限
    
    RegisterFunction("set_perm", SetPermFn);
    // 递归设置权限
    
    RegisterFunction("set_perm_recursive", SetPermFn);

    // Usage:
    
    //   set_metadata("filename", "key1", "value1", "key2", "value2", ...)
    
    // Example:
    
    //   set_metadata("/system/bin/netcfg", "uid", 0, "gid", 3003, "mode", 02750, "selabel", "u:object_r:system_file:s0", "capabilities", 0x0);
    
    // 设置用户、用户组、权限等等
    
    RegisterFunction("set_metadata", SetMetadataFn);

    // Usage:
    
    //   set_metadata_recursive("dirname", "key1", "value1", "key2", "value2", ...)
    
    // Example:
    
    //   set_metadata_recursive("/system", "uid", 0, "gid", 0, "fmode", 0644, "dmode", 0755, "selabel", "u:object_r:system_file:s0", "capabilities", 0x0);
    
    // 递归设置同上
    
    RegisterFunction("set_metadata_recursive", SetMetadataFn);

    // 获取系统属性
    
    RegisterFunction("getprop", GetPropFn);
    RegisterFunction("file_getprop", FileGetPropFn);
    // 写镜像文件
    
    RegisterFunction("write_raw_image", WriteRawImageFn);

    // 打patch
    
    RegisterFunction("apply_patch", ApplyPatchFn);
    RegisterFunction("apply_patch_check", ApplyPatchCheckFn);
    RegisterFunction("apply_patch_space", ApplyPatchSpaceFn);

    // 读取文件
    
    RegisterFunction("read_file", ReadFileFn);
    // 检查sha值
    
    RegisterFunction("sha1_check", Sha1CheckFn);
    // 重命名
    
    RegisterFunction("rename", RenameFn);

    // 擦除cache分区
    
    RegisterFunction("wipe_cache", WipeCacheFn);

    // 将内容打印到屏幕
    
    RegisterFunction("ui_print", UIPrintFn);

    // 执行命令
    
    RegisterFunction("run_program", RunProgramFn);
    RegisterFunction("write_dev", WriteDevNodeFn);
    // 检查升级包的md5
    
    RegisterFunction("verify_md5", VerifyMd5Fn);
    // 切A/B系统
    
    RegisterFunction("switch_system", SwitchSystemFn);
    // 获取usb目录
    
    RegisterFunction("get_usb_path", GetUsbPathFn);
    // dd操作,用来写分区
    
    RegisterFunction("dd", DDblockFn);
}
1-2-3

这个没想到是个挺有意思的知识点,参考RegisterDeviceExtensions原来这么有用

1-2-4

代码位于bootable/recovery/edify/expr.c

void FinishRegistration() {
    qsort(fn_table, fn_entries, sizeof(NamedFunction), fn_entry_compare);
}

结束注册后只做了一个动作,就是将存放注册函数的表通过qsort来做快速排序,qsort是c语言的库函数,详解可以参考qsort详解,通常qsort会和bsearch来搭配使用,后面会在查找注册函数时见到bsearch函数的使用。

1-2-5

代码位于bootable/recovery/edify/expr.h

struct Expr {
    Function fn;//注册函数
    
    char* name;//升级脚本中的命令名称?
    
    int argc;//参数个数
    
    Expr** argv;//参数
    
    int start, end;
};
1-2-6

这里简单来介绍下升级脚本是如何被解析成各个在C语言里可执行的函数的,首先涉及到的是LEX(Lexical Analyzar)和YACC(Yet Another Compiler Compiler),参考Yacc 与 Lex 快速入门,学过编译原理的应该懂词法分析器是个什么东西。在这里通过Lex编程,生成一段词法分析代码bootable/recovery/edifyparser.y,来看一下它的核心部分,这里的参数一眼看去就跟1-2-5中的Expr结构体非常吻合了,分配一个Expr的内存,然后fn就是Function类型(这里代表的就是预置的处理函数),后面就是一一对应了。

expr:  STRING {
    $$ = malloc(sizeof(Expr));
    $$->fn = Literal;
    $$->name = $1;
    $$->argc = 0;
    $$->argv = NULL;
    $$->start = @$.start;
    $$->end = @$.end;
}
|  '(' expr ')'                      { $$ = $2; $$->start=@$.start; $$->end=@$.end; }
|  expr ';'                          { $$ = $1; $$->start=@1.start; $$->end=@1.end; }
|  expr ';' expr                     { $$ = Build(SequenceFn, @$, 2, $1, $3); }
|  error ';' expr                    { $$ = $3; $$->start=@$.start; $$->end=@$.end; }
|  expr '+' expr                     { $$ = Build(ConcatFn, @$, 2, $1, $3); }
|  expr EQ expr                      { $$ = Build(EqualityFn, @$, 2, $1, $3); }
|  expr NE expr                      { $$ = Build(InequalityFn, @$, 2, $1, $3); }
|  expr AND expr                     { $$ = Build(LogicalAndFn, @$, 2, $1, $3); }
|  expr OR expr                      { $$ = Build(LogicalOrFn, @$, 2, $1, $3); }
|  '!' expr                          { $$ = Build(LogicalNotFn, @$, 1, $2); }
|  IF expr THEN expr ENDIF           { $$ = Build(IfElseFn, @$, 2, $2, $4); }
|  IF expr THEN expr ELSE expr ENDIF { $$ = Build(IfElseFn, @$, 3, $2, $4, $6); }
| STRING '(' arglist ')' {
    $$ = malloc(sizeof(Expr));
    $$->fn = FindFunction($1);
    if ($$->fn == NULL) {
        char buffer[256];
        snprintf(buffer, sizeof(buffer), "unknown function \"%s\"", $1);
        yyerror(root, error_count, buffer);
        YYERROR;
    }
    $$->name = $1;
    $$->argc = $3.argc;
    $$->argv = $3.argv;
    $$->start = @$.start;
    $$->end = @$.end;
}
;

在后面赋值部分可以看到给fn赋值时调了FindFunction函数,来看下这个函数的定义,见bootable/recovery/edify/expr.c

Function FindFunction(const char* name) {
    NamedFunction key;
    key.name = name;
    NamedFunction* nf = bsearch(&key, fn_table, fn_entries,
                                sizeof(NamedFunction), fn_entry_compare);
    if (nf == NULL) {
        return NULL;
    }
    return nf->fn;
}

这里就跟1-2-1中的函数注册对应起来了,通过bsearch来查找指定名称对应的处理函数

1-3

从以上就看完了updater.c的main函数的流程,而后面的控制就交给了Evaluate函数去挨条执行升级脚本中的命令,我们拿一个非常规的升级脚本来做示例,这个脚本是我手动做的,为了去做不影响系统的情况下清掉讯飞日志并替换配置的一个功能,原来OTA升级还能做这种这么不常规的事对吧。

ui_print("Mount System partion...");
show_progress(0.100000, 0);
assert(run_program("/system/bin/busybox", "mkdir", "-p", "/data/update_s/system"));
mount("ext4", "EMMC", "/dev/block/mmcblk0p9", "/data/update_s/system");
assert(run_program("/system/bin/busybox", "mkdir", "-p", "/data/update_s/ivres"));
mount("ext4", "EMMC", "/dev/block/mmcblk0p12", "/data/update_s/ivres");
show_progress(0.200000, 0);

ui_print("Skip verifying current system...");
show_progress(0.300000, 0);

# ---- start making changes here ----
ui_print("No files to delete...");
show_progress(0.500000, 0);
ui_print("No system files to patch...");

show_progress(0.600000, 0);
# ---- add new files
ui_print("Unpacking new files...");
package_extract_dir("data/update_s/system", "/data/update_s/system");
show_progress(0.700000, 0);

# ---- add permissions
ui_print("Symlinks and permissions...");
set_metadata("/data/update_s/system/etc/iflytek_res", "uid", 0, "gid", 2000, "mode", 0755, "capabilities", 0x0, "selabel", "u:object_r:system_file:s0");

# --- repair iflytek res
ui_print("Repair iflytek res...");
#run_program("/system/bin/busybox", "rm", "-rf", "/data/update_s/ivres/iflytek");
#assert(run_program("/data/update_s/system/etc/iflytek_res"));
assert(run_program("/system/bin/busybox", "rm", "/data/update_s/ivres/iflytek/res/Active/TTSRes/isstts_log.cfg"));
assert(run_program("/system/bin/busybox", "mv", "/data/update_s/system/etc/isstts_log.cfg", "/data/update_s/ivres/iflytek/res/Active/TTSRes/isstts_log.cfg"));
assert(run_program("/system/bin/busybox", "rm", "/data/update_s/system/etc/iflytek_res"));
show_progress(0.900000, 0);

# ---- ready to switch system
run_program("/system/bin/busybox", "umount", "/data/update_s/system");
run_program("/system/bin/busybox", "rm", "-rf", "/data/update_s/system");
run_program("/system/bin/busybox", "umount", "/data/update_s/ivres");
run_program("/system/bin/busybox", "rm", "-rf", "/data/update_s/ivres");
switch_system(2);

Evaluate函数的定义见1-3-1,从1-2-6中的分析可以知道,这里第一次调用expr->fn会调用到ui_print对应的处理函数,跟升级流程相关的处理函数都在bootable/recovery/updater/install.c中定义,ui_print对应的是

Value* UIPrintFn(const char* name, State* state, int argc, Expr* argv[]) {
    fprintf(stderr, "cdq: UIPrintFn %s[%d]\n", name, argc);
    char** args = ReadVarArgs(state, argc, argv);
    if (args == NULL) {
        return NULL;
    }

    int size = 0;
    int i;
    for (i = 0; i < argc; ++i) {
        size += strlen(args[i]);
    }
    char* buffer = malloc(size+1);
    size = 0;
    for (i = 0; i < argc; ++i) {
        strcpy(buffer+size, args[i]);
        size += strlen(args[i]);
        free(args[i]);
    }
    free(args);
    buffer[size] = '\0';

    char* line = strtok(buffer, "\n");
    while (line) {
        fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe,
                "ui_print %s\n", line);
        line = strtok(NULL, "\n");
    }
    fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe, "ui_print\n");

    return StringValue(buffer);
}

首先去读所带的参数,从argc可以表明,ui_print命令带了一个参数,来看ReadVarArgs

char** ReadVarArgs(State* state, int argc, Expr* argv[]) {
    char** args = (char**)malloc(argc * sizeof(char*));
    int i = 0;
    for (i = 0; i < argc; ++i) {
        fprintf(stderr, "ReadVarArgs[%d]: %s[%d-%d-%d]\n", i, argv[i]->name, argv[i]->argc, argv[i]->start, argv[i]->end);
        args[i] = Evaluate(state, argv[i]);
        if (args[i] == NULL) {
            int j;
            for (j = 0; j < i; ++j) {
                free(args[j]);
            }
            free(args);
            return NULL;
        }
    }
    return args;
}

然后会把参数“Mount System partion…”再去跑一遍Evaluate,从1-2-6里看到fn的默认值是Literal

Value* Literal(const char* name, State* state, int argc, Expr* argv[]) {
    return StringValue(strdup(name));
}

所以这里的参数就是传入的字串,Value的data就是“Mount System partion…”本身。继续看ReadVarArgs,args[0]不是NULL,而且就只有一个参数,后面就直接返回args给到了UIPrintFn,在UIPrintFn后面将args里的参数根据换行符”\n”构造出了打印行(这里就只有一行输出),然后通过fprintf通过pipe输出到父进程里打印,最后再打印个换行符结束。

要理解整个脚本的命令是怎么执行的,先要搞清楚update-script脚本被yyparser解析成之后变成了一个什么样的数据结构。在1-2-1中可以看到yyparser解析了script之后存到了root指针里,root就是一个Expr的指针,而Expr的结构在expr.h中可以看到(参考1-2-5),这里关键是Expr结构体还会存一个Expr**的数据,就是一个Expr的指针数组,所以最后从root开始,整个数据就会交织成一个很复杂的二维数据。网上会有一些说法,例如

从Expr的定义中可以看到它有一个字段argv,这个字段是Expr指针的指针类型,它实际上会指向一个Expr指针的数组对象,表示Expr对象的所有下一级对象。通过这个字段,脚本解析后得到的所有命令都串接在一起,而且命令的执行函数还会调用Ecaluate()来继续执行argv中的Expr对象,因此,虽然Evaluate()中只调用了root对象的fn()函数,但是实际上会执行脚本中的所有命令。

比较宏观的描述了功能,但是真的理解了脚本执行的原理了吗?在1-3-2里来看下update-script脚本被解析后存成了什么样的数据格式。

大致清楚了脚本解析后的数据结构存储方式之后再来看Evaluate(&state, root),这个把root传下去之后,在1-3-1中调用了expr->fn怎么就能走到脚本的第一条指令对应的处理函数呢?这里还是个疑问,我自己对operator这个字串的作用还不太理解,这边也还没看懂是如何按顺序把所有指令串联起来跑一遍的,以后再说吧,目前不影响对整体功能的理解。

1-3-1

若修改这里,则先编译edify,会生成target StaticLib: libedify (out/target/product/xe1115h/obj/STATIC_LIBRARIES/libedify_intermediates/libedify.a),再编译updater,在Android.mk中有对libedify的依赖

char* Evaluate(State* state, Expr* expr) {
    fprintf(stderr, "cdq: Evaluate %s[%d,%d,%d]\n", expr->name, expr->argc, expr->start, expr->end);
    Value* v = expr->fn(expr->name, state, expr->argc, expr->argv);
    if (v == NULL) return NULL;
    if (v->type != VAL_STRING) {
        ErrorAbort(state, "expecting string, got value type %d", v->type);
        FreeValue(v);
        return NULL;
    }
    char* result = v->data;
    free(v);
    return result;
}
1-3-2
序号 指针 name argc start end
0 root->argv        
1 0->argv[0] (operator) 2 0 1940
2 0->argv[1] switch_system 1 1942 1958
3 2->argv[0] 2 0 1956 1957
4 1->argv[0] (operator) 2 0 1867
5 1->argv[1] run_program 4 1869 1940
6 5->argv[0] /system/bin/busybox 0 1881 1902
7 5->argv[1] rm 0 1904 1908
8 5->argv[2] -rf 0 1910 1915
9 5->argv[3] /data/update_s/ivres 0 1917 1939
10

从这个结果可以看出数据存储的规律就是以下几点:

1、root节点后面挂了Expr的数组的指针A,而A[0]存的是(operator),A[1]存的是一条升级命令,A[1]->expr[0]开始存的是这条指令的参数,有n个参数就存到A[1]->expr[n]

2、argc代表了这条指令所带参数的个数

3、start、end代表了这条指令/参数在脚本中的起止位置,(operator)的起止是从0到A[1]指令的start前2个字节,也就是说存了A[1]指令之前的整段