cmjs 发表于 2013-6-21 13:20:42

RT-Thread下finsh原理浅析


一直想探寻rtt的finsh原理,最近终于下定决心跑一跑这段代码,若有不对之处还望多多指针。我的QQ 277153007

​RT-Thread的Finsh Shell接口实际上是一个线程,入口在shell.c,入口函数为void finsh_thread_entry(void* parameter)该线程是典型的初始化---死循环结构{
   init();
   while(1)
   {
         ......
   }
}先初始化此shell的语法分析器parser
finsh_init(&shell->parser);shell是一个指向finsh_shell结构的变量,finsh_shell定义于shell.h 可以看做rt_device_t的派生类struct finsh_shell
{
    struct rt_semaphore rx_sem;
    enum input_stat stat;
    rt_uint8_t echo_mode:1;
    rt_uint8_t use_history:1;
#ifdef FINSH_USING_HISTORY
    rt_uint16_t current_history;
    rt_uint16_t history_count;
    char cmd_history;
#endif
    struct finsh_parser parser;
    char line;
    rt_uint8_t line_position;
    rt_device_t device;
};其中最重要的是一个finsh_parser的数据结构,这便是语法分析器struct finsh_parser
{
    u_char* parser_string;
    struct finsh_token token;
    struct finsh_node* root;
};其中 parser_string用于指向需要处理的字符串即在命令行中输入的字符串
      token表示一个词法单元处理器
      root是一个指向finsh_token的指针,用于指向后面语法树的根节点
PS    token的意思是 词法单元 比如 "12+14"‘12’是一个token   ‘+’是一个token   ‘14’是一个token (更多内容参见《编译原理》)

让我们再回到finsh线程入口函数 finsh_thread_entryfinsh_init(&shell->parser);此初始化函数调用的结果是此parser(语法分析器)所占用的内存清0

接下来 while死循环中if (rt_sem_take(&shell->rx_sem,RT_WAITING_FOREVER) != RT_EOK) continue;即永久地等待1个信号量,正常情况下当键盘有键按下时释放此信号量,然后此线程得到此信号量使程序继续运行。while (rt_device_read(shell->device, 0, &ch, 1) == 1)用ch储存键盘按下的键值#ifdef FINSH_USING_HISTORY
if (finsh_handle_history(shell, ch) == RT_TRUE) continue;
#endif如果开启了宏定义FINSH_USING_HISTORY,则表示输入的前几条命令会被记忆起来,存储深度的见shell
本文假设FINSH_USING_HISTORY未被开启

此线程会根据输入的字符不同而进入后面的几个if或者else if分支,只有当按下一些诸如回车等特殊按键时,才会进入那些分支;而当键盘按下普通字符时,执行的是以下程序:将输入字符依次存入shell->line数组中并回显到屏幕上。shell->line = ch;
ch = 0;
if (shell->echo_mode)
   rt_kprintf("%c", shell->line);
shell->line_position ++;
shell->use_history = 0;当输完命令行,最后敲击回车,便会执行以下语句/* handle end of line, break */
if (ch == '\r' || ch == '\n')
{            
    /* change to ';' and break */
    shell->line = ';';
         
    if (shell->line_position != 0)
      finsh_run_line(&shell->parser, shell->line);      
    else rt_kprintf("\n");

    rt_kprintf(FINSH_PROMPT);   
    memset(shell->line, 0, sizeof(shell->line));            
    shell->line_position = 0;
    break;
}上面的代码会在输入的字符串最后一个位置添一个‘;’然后运行 finsh_run_line(&shell->parser, shell->line),函数原型如下void finsh_run_line(struct finsh_parser* parser, const char *line)函数finsh_run_line主要完成三项工作:
1.分析输入的字符串,将其分割成一个一个的词法单元并构造成树形结构,每个节点为1个词法单元
2.编译语法树,生成中间代码并将其写入虚拟机所指定的内存
3.运行虚拟机的指令

首先来看第1项工作
在finsh_run_line中    调用finsh_parser_run(parser, (unsigned char*)line)运行语法分析器void finsh_parser_run(struct finsh_parser* self, const u_char* string)
{
    enum finsh_token_type token;
    struct finsh_node *node;
    node = NULL;
    /* init parser */
    self->parser_string = (u_char*)string;
    /* init token */
    finsh_token_init(&(self->token), self->parser_string);该函数定义一个finsh_token_type类型的token 具体类型的种类 可以阅览文件finsh_token.h,那里面涵盖了finsh-shell系统所有词法单元的类型
接着定义一个指向finsh_node类型的指针node。

第7行将输入的字符串复制到   self->parser_string所指向的地址中。
第9行初始化词法单元分析器所占用的内存空间 并使该token->line指向输入的字符串

介绍一下词法分析器的数据结构,它被定义在finsh.hstruct finsh_token
{
    char eof;
    char replay;

    int position;
    u_char current_token;

    union {
      char char_value;
      int int_value;
      long long_value;
    } value;
    u_char string;

    u_char* line;
};eof 用于记录词法单元是否结束,若置位则表示已经解析到该词法单元的最后1个字符
replay表示正在解析词法单元以后是否需要重新解析
position记录正在解析词法单元中的哪个位置
current用于记录当前解析词法单元的类型
value用于记录当前解析词法单元的值(当类型为数值类型时)
string用来存储id型的词法单元

回到函数finsh_parser_run/* get next token */
    next_token(token, &(self->token));这句话的意思是将获取下一个词法单元,并用token记录该词法单元的类型
举个例子   比如我在命令行输入的是abc+12
运行 next_token(token, &(self->token))的结果就是 得到一个词法单元abc 它的类型是identifier,即token=finsh_token_type_identifier
再运行 next_token(token, &(self->token))的结果是得到一个词法单元 +它的类型是 加号类型即token =finsh_token_type_add
以此类推

下面具体分析一下此流程
由宏定义#define next_token(token, lex)    (token) = finsh_token_token(lex)得知实际调用的函数是finsh_token_tokenenum finsh_token_type finsh_token_token(struct finsh_token* self)
{
    if ( self->replay )    self->replay = 0;
    else token_run(self);

    return (enum finsh_token_type)self->current_token;
}如果词法分析器的replay已经置位   需要再次解析则清空此位返回当前词法单元的类型
而若replay为0则表示可以继续往后处理   即调用token_run
在token_run里判别词法单元的种类 并更新current_token

接下来的的while循环   便是逐个提取词法单元   并将词法单元作为节点   构造语法树while (token != finsh_token_type_eof && token != finsh_token_type_bad)
    {
      switch (token)
      {
      case finsh_token_type_identifier:
            /* process expr_statement */
            finsh_token_replay(&(self->token));

            if (self->root != NULL)
            {
                finsh_node_sibling(node) = proc_expr_statement(self);
                if (finsh_node_sibling(node) != NULL)
                  node = finsh_node_sibling(node);
            }
            else
            {
                node = proc_expr_statement(self);
                self->root = node;
            }
            break;

      default:
            if (is_base_type(token) || token == finsh_token_type_unsigned)
            {
                /* variable decl */
                finsh_token_replay(&(self->token));   

                if (self->root != NULL)
                {
                  finsh_node_sibling(node) = proc_variable_decl(self);
                  if (finsh_node_sibling(node) != NULL)
                        node = finsh_node_sibling(node);
                }
                else
                {
                  node = proc_variable_decl(self);
                  self->root = node;
                }
            }
            else
            {
                /* process expr_statement */
                finsh_token_replay(&(self->token));

                if (self->root != NULL)
                {
                  finsh_node_sibling(node) = proc_expr_statement(self);
                  if (finsh_node_sibling(node) != NULL)
                        node = finsh_node_sibling(node);
                  else next_token(token, &(self->token));
                }
                else
                {
                  node = proc_expr_statement(self);
                  self->root = node;
                }
            }
            break;
      }
      /* get next token */
      next_token(token, &(self->token));
    }注意第54行的函数proc_expr_statement当我们追踪程序的时候会发现它会接着调用proc_assign_expr, proc_inclusive_or_expr,proc_exclusive_or_expr,proc_and_expr,proc_shift_expr,proc_additive_expr,proc_multiplicative_expr,proc_cast_expr,proc_unary_expr,proc_postfix_expr。
其实这个过程便是决定树形结构层次的过程,越往后的运算实际上优先级设定的越高   
举个例子finsh>> 8+5*2   从上面的调用顺序可以看到proc_additive_expr在proc_multiplicative_expr前面,表示乘法优先级更高,这也符合我们的习惯。

到此一棵完整的语法树就被构造出来了,下面来说函数finsh_run_line的第二项工作"编译语法树,生成中间代码并将其写入虚拟机所指定的内存“int finsh_compiler_run(struct finsh_node* node)
{
    struct finsh_node* sibling;

    /* type check */
    finsh_type_check(node, FINSH_NODE_VALUE);

    /* clean text segment and vm stack */
    memset(&text_segment, 0, sizeof(text_segment));
    memset(&finsh_vm_stack, 0, sizeof(finsh_vm_stack));

    /* reset compile stack pointer and pc */
    finsh_compile_sp = &finsh_vm_stack;
    finsh_compile_pc = &text_segment;

    /* compile node */
    sibling = node;
    while (sibling != NULL)
    {
      struct finsh_node* current_node;
      current_node = sibling;

      /* get sibling node */
      sibling = current_node->sibling;

      /* clean sibling node */
      current_node->sibling = NULL;
      finsh_compile(current_node);

      /* pop current value */
      if (sibling != NULL) finsh_code_byte(FINSH_OP_POP);
    }

    return 0;
}上面第8行到第14行清除虚拟机代码段和运行的栈所处的内存初始化其SP指针和PC指针。while循环中最关键的函数是finsh_compile,
这个函数从root节点开始递归地深入到语法树的叶子节点进行编译。

下面截取finsh_compile函数(文件finsh_compiler.c中)的一小部分稍加解释static int finsh_compile(struct finsh_node* node)
{
    if (node != NULL)
    {
      /* compile child node */
      if (finsh_node_child(node) != NULL)
            finsh_compile(finsh_node_child(node));

      /* compile current node */
      switch (node->node_type)
      {
      case FINSH_NODE_ID:
            {
                /* identifier::syscall */
                if (node->idtype & FINSH_IDTYPE_SYSCALL)
                {
                  /* load address */
                  finsh_code_byte(FINSH_OP_LD_DWORD);
                  finsh_code_dword((long)node->id.syscall->func);
                }第10行的switch语句根据结点类型的不同进入不同的分支。
此函数会用到finsh_compiler.c中3个常用的宏定义#define finsh_code_byte(x)    do { *finsh_compile_pc = (x); finsh_compile_pc ++; } while(0)
#define finsh_code_word(x)    do { FINSH_SET16(finsh_compile_pc, x); finsh_compile_pc +=2; } while(0)
#define finsh_code_dword(x) do { FINSH_SET32(finsh_compile_pc, x); finsh_compile_pc +=4; } while(0)这几个宏定义的作用是将x放入到finsh_compile_pc所指向的内存,finsh_compile_pc再向后移动。唯一不同的是移动的幅度,分别为字节,字和双字
举个例子假设 节点是个‘系统函数’节点 而 finsh_compile_pc=0x20000000所调用的系统函数地址是0x30000000
那么当执行完
finsh_code_byte(FINSH_OP_LD_DWORD)
finsh_code_dword((long)node->id.syscall->func)后
从0x20000000开始的内存会变为   24 00 00 00 00 30 ……

如此   完成语法树的编译,中间代码被存入虚拟机所占用内存,余下的工作便是运行虚拟机/* run virtual machine */
    if (finsh_errno() == 0)
    {
      char ch;
      finsh_vm_run();

      ch = (unsigned char)finsh_stack_bottom();
      if (ch > 0x20 && ch < 0x7e)
      {
            rt_kprintf("\t'%c', %d, 0x%08x\n",
                (unsigned char)finsh_stack_bottom(),
                (unsigned int)finsh_stack_bottom(),
                (unsigned int)finsh_stack_bottom());
      }
      else
      {
            rt_kprintf("\t%d, 0x%08x\n",
                (unsigned int)finsh_stack_bottom(),
                (unsigned int)finsh_stack_bottom());
      }
    }在函数finsh_vm_run中void finsh_vm_run()
{
    u_char op;

    /* if want to disassemble the bytecode, please define VM_DISASSEMBLE */
#ifdef VM_DISASSEMBLE
    void finsh_disassemble();
    finsh_disassemble();
#endif

    /* set sp(stack pointer) to the beginning of stack */
    finsh_sp = &finsh_vm_stack;

    /* set pc to the beginning of text segment */
    finsh_pc = &text_segment;

    while ((finsh_pc - &text_segment >= 0) &&
      (finsh_pc - &text_segment < FINSH_TEXT_MAX))
    {
      /* get op */
      op = *finsh_pc++;

      /* call op function */
      op_table();
    }
}​设定好finsh_sp和finsh_pc后便从虚拟机中一条一条的读取指令。


techbaby 发表于 2013-6-21 13:48:34

标记,楼主的解说不错!

gtnr 发表于 2013-6-21 15:00:12

{:handshake:}受益了。

ffxz 发表于 2013-6-22 22:54:16

这个帖分析得很好,不过还缺少语法分析这些代码的详细分析。不过这些也可以从finsh shell的代码中获得:
/* 这个是分析好的语法树情况
* the structure of abstract syntax tree:
* root____________
* |               \
* child__      sibling__
* |      \       |      \
* child siblingchild   sibling
*                        ...
*/

/* 规约推导范式 */
/* 这个是推导起点
start -> statement_expr | decl_variable
*/

/* 函数或变量的声明
process for function and variable declaration.
decl_variable -> type declaration_list ';'
declarator_list -> declarator_list ',' declarator
        | declarator
declarator -> identifier
        | identifier ASSIGN expr_assign
*/

/* 类型分析
type -> type_prefix type_basic | type_basic
type_prefix -> UNSIGNED
type_basic -> VOID
        | CHAR
        | SHORT
        | INT
        | STRING
*/

/*
identifier -> IDENTIFIER
*/

/* 表达式推导
statement_expr -> ';'
        | expr ';'
*/

/*
expr -> expr_assign
*/

/*
expr_assign -> expr_inclusive_or
        | expr_unary ASSIGN expr_assign
*/

/*
expr_inclusive_or -> expr_exclusive_or
        | expr_inclusive_or '|' expr_exclusive_or
*/

/*
expr_exclusive_or -> expr_and
        | expr_exclusive '^' expr_and
*/

/*
expr_and -> expr_shift
        | expr_and '&' expr_shift
*/

/*
expr_shift -> expr_additive
        | expr_shift '<<' expr_additive
        | expr_shift '>>' expr_additive
*/

/*
expr_additive -> expr_multiplicative
        | expr_additive SUB expr_multiplicative
        | expr_additive ADD expr_multiplicative
*/

/*
expr_multiplicative -> expr_cast
        | expr_multiplicative '*' expr_cast
        | expr_multiplicative '/' expr_cast
        | expr_multiplicative '%' expr_cast
*/

/*
20060313, add recast parse
expr_cast -> expr_unary
        | '(' type ')' expr_cast
*/

/*
20050921, add '*' and '&'
expr_unary -> expr_postfix
        | ADD expr_cast
        | INC expr_cast
        | SUB expr_cast
        | DEC expr_cast
        | '~' expr_cast
        | '*' expr_cast
        | '&' expr_cast
*/

/*
expr_postfix -> expr_primary
        | expr_postfix INC
        | expr_postfix DEC
        | expr_postfix '(' param_list ')'
*/

/*
expr_primary -> literal
        | '(' expr ')'
        | identifier
*/

/*
param_list -> empty
        | expr_assign
        | param_list ',' expr_assign
*/

这个范式是完全按照C语言的表达式来进行的,显然还有优化的空间,但是为了保持它的可读性,所以没有做进一步的优化。

enovo2468 发表于 2013-10-12 11:00:49

mark{:smile:}

huy666 发表于 2013-10-12 11:08:30

这种贴自要顶 建议加精

li3p 发表于 2013-12-5 22:56:33

受教良多,感谢!

Excellence 发表于 2013-12-6 09:01:55

不错。

xblandy 发表于 2013-12-6 09:44:34

RT-Thread下finsh原理浅析 MARK 好好学习

bobo89 发表于 2013-12-6 09:56:45

好东西啊!

DevLabs 发表于 2013-12-6 10:09:06

好文!mark下~

loveskangaroo 发表于 2014-1-1 11:14:28

mark。好东东

ffxz 发表于 2014-1-2 17:08:59

1.2.0的分支加入了msh功能,在裁剪finsh shell的前提下,可以把相应的flash & sram空间都节省下来

最近在看nRF51822,可以把msh塞进去了

hqgboy 发表于 2014-1-9 13:07:40

{:victory:}{:victory:}{:victory:}

1ongquan 发表于 2014-1-9 14:56:06

ffxz 发表于 2014-1-2 17:08
1.2.0的分支加入了msh功能,在裁剪finsh shell的前提下,可以把相应的flash & sram空间都节省下来

最近在 ...

懒问一个问题有没有例程把MCU虚拟化,完全执行 finsh脚本程序

just_be_fine 发表于 2014-3-6 22:38:45

LZ分析得很不错啊!

janho 发表于 2014-7-10 13:48:09

mark.........

litrtleway 发表于 2016-2-18 13:11:43

mark,finsh分析。感谢楼主!

claudling 发表于 2016-2-23 14:52:40

好文,必须mark

doushinide 发表于 2017-1-10 08:55:16

厉害了 谢谢分享

zhongsandaoren 发表于 2017-5-31 13:26:17

好吧,很复杂
页: [1]
查看完整版本: RT-Thread下finsh原理浅析