美文网首页
三、嵌入式之虚拟机分析 (1) rt-thread的shell

三、嵌入式之虚拟机分析 (1) rt-thread的shell

作者: wit_yuan | 来源:发表于2017-05-14 16:08 被阅读0次

    1 简介

    1.1 说明

    little c 解释器
    http://blog.csdn.net/redraiment/article/details/4693952
    编译器:https://www.amobbs.com/thread-5536737-1-1.html

    这里面所说的虚拟机,其实也算是元编程的一种实现方式。

    目前的虚拟机有以下几种:
    CLR, JVM, Parrot, LLVM。

    要在小型单片机上运行编译器,我暂时想到两个办法:
    (1)如果程序空间足够装下编译器代码,但内存不够,可以利用SD卡,将asmcode[]、mcode[]等存入SD卡(或其它存储设备)。
    (2)如果程序空间只能装下虚拟机的代码,则可以将这个编译器的C代码用“简单C” 的语法重新写一下,(在电脑上)编译成机器码存入SD卡中,让单片机运行虚拟机,运行SD卡中的代码,就是说在虚拟机中运行编译器;

    比较值得有参考价值的虚拟机分析:参考链接

    而标准规范的java虚拟机文档:链接

    1.2 名词

    1.2.1 前缀表达式

    也称为前缀记法、波兰式。称作波兰式是为了纪念其发明者波兰数学家Jan Lukasiewicz。

    1.2.2 中缀表达式

    同类型的还有前缀表达式,后缀表达式。

    中缀表达式是指运算符在运算数的中间,计算中缀表达式时需要用到两个栈:数字栈和运算符栈。在整个中缀表达式求值的过程中主要涉及到的模块有:栈的相关操作、优先级表的确立、输入的待计算字符串拆分为数字和运算符以及运算处理等

    有一个比较不错的写法,链接

    1.2.3 后缀表达式

    后缀表达式(又称为逆波兰reverse polish)就是不需要括号就可以实现调整运算顺序的一种技巧。

    1.2.3 二叉树

    可以通过不同遍历方式组合成中辍表达式和后缀表达式。
    二叉树定义:每个节点最多有两个子树的树结构。

    2 rt-thread的finsh shell

    先从rt-thead的finsh shell开始研究。
    rt-thread的github链接地址:下载链接

    整体代码逻辑:
    在finsh_run_line中调用以下三个主要函数,完成大体功能。

    • finsh_parser_run
      完成xxxx
    • finsh_compiler_run
    • finsh_vm_run

    这样的流程,在我的理解来看,就是一个 解析器(语法解析器),编译器(解释器),虚拟机执行的过程。

    对finsh_parser_run进行分析:


    • 若在shell中输入表达式: 1 + 1
    • a.执行: proc_expr_statement
    • b.执行: proc_expr
    • c.执行:proc_assign_expr
    • d.执行:proc_inclusive_or_expr,带有后续处理
    • e.执行:proc_exclusive_or_expr,带有后续处理
    • f.执行:proc_and_expr,有后续处理
    • g.执行:proc_shift_expr,有后续处理
    • h.执行:proc_additive_expr,有后续处理
    • i.执行:proc_multiplicative_expr,有后续处理
    • j.执行:proc_cast_expr,有后续处理
    • k.执行:proc_unary_expr
    • l.执行:proc_postfix_expr,调用该函数之前调用了finsh_token_replay。
    • m.执行:proc_primary_expr,最终执行到底。

    执行到proc_primary_expr后,能识别出1。也就是说,执行到分支:finsh_token_type_value_int。同时往数组global_node_table添加一个finsh_node。

    然后:

    1. 返回到proc_postfix_expr,其中将该函数返回值赋值给struct finsh_node* postfix;该值应该就是前面识别出来的结点1。随后调用next_token,经过一系列处理,token的值为:finsh_token_type_add。然后该函数仍旧执行了finsh_token_replay,返回了上面提到的postfix。
    2. 返回到proc_unary_expr,仍旧继续返回。返回到,proc_cast_expr。
    3. proc_cast_expr后,返回的仍然是1的结点。
    4. 后面一直返回到proc_additive_expr,其调用完成proc_multiplicative_expr后,执行next_token,而token的值为finsh_token_type_add。mul是1的结点。mul_new返回值为 proc_multiplicative_expr的结果。这一次调用proc_multiplicative_expr,执行到proc_cast_expr,搜寻next_token,这个时候找到另外一个1结点。最终mul_new就是另外一个1结点了。这个结点就是global_node_table创建的第二个值了。
    5. 返回到proc_additive_expr,执行make_sys_node,创建FINSH_NODE_SYS_ADD结点。后执行赋值,node->child(+)=先找到的结点1,结点1->sibling=结点1。最后返回FINSH_NODE_SYS_ADD结点。
    6. 最后返回到self->root = node;那么self->root就是前面提到的FINSH_NODE_SYS_ADD结点。

    • 对finsh_parser_run进行分析:

    • a.执行finsh_type_check
    • b.清除text_segment,finsh_vm_stack。设置finsh_compile_sp,finsh_compile_pc。
    • c.获取sibling node,调用finsh_compile,传参数为root node,如果有sibling,则弹出栈sibling node。

    重点在分析finsh_compile上:

    • a.判断node->child是否为空,否则编译child结点。这里使用的递归的操作方法。
    • b.判断node->node_type,最先判断的child的node_type,为FINSH_NODE_VALUE_INT。接着执行,
    finsh_code_byte(FINSH_OP_LD_DWORD);
    finsh_code_dword(node->value.long_value);
    
    • c.递归获取到child->sibling结点。编译sibling结点。
    finsh_code_byte(FINSH_OP_LD_DWORD);
    finsh_code_dword(node->value.long_value);
    
    • d.判断的是root->node_type,是FINSH_NODE_SYS_ADD。执行:
    finsh_code_byte(FINSH_OP_ADD_BYTE);
    

    从这里看,仍然是一种后缀表达式的形式。

    • 对finsh_vm_run进行分析:

        finsh_sp = &finsh_vm_stack[0];
    
        /* set pc to the beginning of text segment */
        finsh_pc = &text_segment[0];
    
        while ((finsh_pc - &text_segment[0] >= 0) &&
            (finsh_pc - &text_segment[0] < FINSH_TEXT_MAX))
        {
            /* get op */
            op = *finsh_pc++;
    
            /* call op function */
            op_table[op]();
        }
    

    在前面说的compile中,数据存储的内容为:
    0x24
    4字节数值,低字节在前,高字节在后。
    0x24
    4字节数值,低字节在前,高字节在后。
    0x03

    所以,虚拟机执行的为:

    • a.执行OP_ld_dword,获取4字节数据,然后放到栈上。同时pc指针前进。
    • b.继续执行a步骤操作。
    • c.执行OP_add_dword,最终调用OP_BIN_DWORD,将栈中数据进行处理,同时出栈指针递减,也就是出栈数据。

    自此,按照例子 1 + 1的整个路径分析完成。

    那么,三个函数做的事情,总结如下:

    • finsh_parser_run主要是将字符串,一个个分拆,也就是编译原理中会说的词法分析。然后形成一棵树。
    • finsh_compiler_run主要是将树中的数据提取出来,放到虚拟机中的代码段中。这里并没有区分数据区和代码区,一股脑的放在一起。
    • finsh_vm_run主要是将代码段中的数据,根据操作码,提取出数据,放到栈中。这里面用到了后缀表达式,先提取出两个数,然后第三个数是+,-,*,/等等的操作码,计算完成后,可以出一个栈的数据,然后继续处理。

    接着再来看看输入一个函数,是怎样的执行流程。比如hello():
    我先说明一点,输入函数,函数的参数个数,rt-thread的shell并没有做检测,这应该是一个失误,是一个很不好的设计。

      1. finsh_parser_run中调用token_run中,获取到current_token为finsh_token_type_identifier。后执行到proc_primary_expr,执行finsh_node_new_id(),会先调用finsh_var_lookup(),没找到就调用finsh_sysvar_lookup(),仍没找到,就调用finsh_syscall_lookup(),当然按照例子上来看,最终是找到了。最后会调用finsh_node_allocate(),分配结点,node_type为FINSH_NODE_ID。也就是下面这几个:
    global_node_table[i].node_type = FINSH_NODE_ID;
    node->id.syscall = xxx;
    node->idtype = FINSH_IDTYPE_SYSCALL;
    
    1. 执行到函数proc_postfix_expr(),调用完proc_primary_expr()后,执行next_token(),进入分支finsh_token_type_left_paren,接着就去寻找右括号。我们不看找不到右括号或者带参数情况,只看找到了右括号后的执行路径。
      3.调用make_sys_node,这个时候,添加结点FINSH_NODE_SYS_FUNC,并把上面的FINSH_NODE_ID添加进来。也就是说,root为FINSH_NODE_SYS_FUNC结点,child为FINSH_NODE_ID,sibling为空。
      4.调用finsh_compile,进入分支FINSH_NODE_ID,判断child是否为空,若不为空,则调用
                        finsh_code_byte(FINSH_OP_LD_DWORD);
                        finsh_code_dword((long)node->id.syscall->func);
    

    在代码段中形成的结构为:
    0x24
    函数地址
    接着执行FINSH_NODE_SYS_FUNC,调用:

                        finsh_code_byte(FINSH_OP_SYSCALL);
                        finsh_code_byte(parameters);
    

    其中参数个数为0
    在代码段中存储的数据为:
    0x2C
    0

    • 执行虚拟机函数finsh_vm_run(),调用OP_ld_dword(),将函数地址入栈,finsh_sp->long_value=函数地址。然后调用OP_call(),获取参数个数,当然为0。后面就是相关的函数调用了。

    好了,rt-thread的finsh shell就讲到这里。实话说,其调用关系比较层叠。看起来每个函数调用关系分开,写得很好。但是,目前很多的特性,支持力度还是有限的。

    后想了想,对于我们常见的说法,函数参数列表先入栈的是最后一个参数,这里可以验证一下:

    找到函数proc_param_list,这个就是处理参数列表的。
    最终调用到proc_cast_expr() -->proc_type()---> 找到第一个参数(我们先分析参数全为数字),则token为finsh_token_type_value_int---->递归调用proc_cast_expr()--->proc_unary_expr()--->proc_postfix_expr()--->proc_primary_expr()--->finsh_node_new_int()。
    然后递归返回,node->data_type = finsh_type_unknown;node->node_type = FINSH_NODE_VALUE_INT;
    当然在proc_postfix_expr()中调用的next_token,会设置token==finsh_token_type_comma。--->proc_cast_expr()--->next_token()--->FINSH_NODE_VALUE_INT。etc

        while (token == finsh_token_type_comma )
        {
            finsh_node_sibling(assign) = proc_assign_expr(self);
    
            if (finsh_node_sibling(assign) != NULL) assign = finsh_node_sibling(assign);
            else finsh_error_set(FINSH_ERROR_EXPECT_OPERATOR);
    
            next_token(token, &(self->token));
        }
    

    从这个循环代码中,可以发现是一直在建立siblings。后面finsh_compile被调用,然后递归生效,最后一个参数被加入到栈中。

    所以,很明显咯,是递归导致的栈序改变。

    还可以看看rt-thread的shell的函数调用,一层一层的,其实是按照操作符的优先级来的。

    2.1 一些扩展知识

    在写完一些内容之后,发现finsh shell有一些命名技巧,这就涉及到一些知识。我也是边看就搜咯。

    2.1.1 siblings

    这个在二叉树上有介绍,表示的是兄弟结点。二叉树有完美二叉树、完全二叉树和完满二叉树。

    写到这里,我只好表示,我的树知识欠缺,只好另外开一个栏,学习树了。
    链接:链接地址

    2.2 分析开始

    分析之初,这几个结构体比较重要:

    3 picoC

    源码的下载地址为:下载链接

    从其README.md上看,其说明了picoC是一个小的C解释器脚本。它最初是作为一个无人机的飞行系统板上的脚本语言。核心的C源代码是大约3500行。并不打算成为一个完整的ISO C实现。picoc已经在x86-32,x86-64,PowerPC,arm,UltraSPARC,HP-PA和Blackfin处理器上测试过,并很容易地移植到新的目标。

    picoc代码上看,基本有如下几块:lex词法解析,table一个基本数据结构(用于存放变量),是个字符串hash表,heap管理内存分配(包括了stack frame的实现), type做类型管理(基本型别和程序自定义的struct,typedef等),expression做表达式解析,variable变量管理分配释放栈帧。

    2.1 编译

    在NIX/Linux/POSIX主机上:

    • make
    • make test

    之后,可以执行./picoc。
    可以有三种执行方式:

    图1

    2.2 移植

    1. platform_XXX.c 包含一些支持性的函数。编译器这样才能在你的平台上运行。例如如何向控制台输出字符。
    2. platform_library.c包含一些函数库,这些库是提供给用户程序的。
    3. 有clibrary.c,包含用户库函数,比如printf,这些是平台无关的。

    移植系统,需要在platform.h中设置相应的includes和defines。在platform_XXX.c中编写I/o函数。在platform_library.c 中编写用户函数。在picoc.c中编写一些main函数内容。

    platform.h默认设置为unix主机(UNIX_HOST)。

    2.3 协议

    picoc遵循"New BSD License".

    相关文章

      网友评论

          本文标题:三、嵌入式之虚拟机分析 (1) rt-thread的shell

          本文链接:https://www.haomeiwen.com/subject/gcjqxxtx.html