原文:http://www.rt-thread.org/phpBB3/viewtopic.php?f=3&t=2865
一直想探寻rtt的finsh原理,最近终于下定决心跑一跑这段代码,若有不对之处还望多多指针。
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后 便从虚拟机中一条一条的读取指令。