|
一直想探寻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[FINSH_HISTORY_LINES][FINSH_CMD_SIZE];
- #endif
- struct finsh_parser parser;
- char line[FINSH_CMD_SIZE];
- 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_entry- finsh_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[shell->line_position] = ch;
- ch = 0;
- if (shell->echo_mode)
- rt_kprintf("%c", shell->line[shell->line_position]);
- shell->line_position ++;
- shell->use_history = 0;
复制代码 当输完命令行,最后敲击回车,便会执行以下语句- /* handle end of line, break */
- if (ch == '\r' || ch == '\n')
- {
- /* change to ';' and break */
- shell->line[shell->line_position] = ';';
-
- 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.h- struct 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[128];
- 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_token- enum 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], 0, sizeof(text_segment));
- memset(&finsh_vm_stack[0], 0, sizeof(finsh_vm_stack[0]));
- /* reset compile stack pointer and pc */
- finsh_compile_sp = &finsh_vm_stack[0];
- finsh_compile_pc = &text_segment[0];
- /* 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[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]();
- }
- }
复制代码 设定好finsh_sp和finsh_pc后 便从虚拟机中一条一条的读取指令。
|
阿莫论坛20周年了!感谢大家的支持与爱护!!
如果天空是黑暗的,那就摸黑生存;
如果发出声音是危险的,那就保持沉默;
如果自觉无力发光,那就蜷伏于牆角。
但是,不要习惯了黑暗就为黑暗辩护;
也不要为自己的苟且而得意;
不要嘲讽那些比自己更勇敢的人。
我们可以卑微如尘土,但不可扭曲如蛆虫。
|