三、嵌入式之虛擬機(jī)分析 (1) rt-thread的shell


1 簡介

1.1 說明

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

這里面所說的虛擬機(jī),其實(shí)也算是元編程的一種實(shí)現(xiàn)方式。

目前的虛擬機(jī)有以下幾種:
CLR, JVM, Parrot, LLVM。

要在小型單片機(jī)上運(yùn)行編譯器,我暫時(shí)想到兩個(gè)辦法:
(1)如果程序空間足夠裝下編譯器代碼,但內(nèi)存不夠,可以利用SD卡,將asmcode[]、mcode[]等存入SD卡(或其它存儲設(shè)備)。
(2)如果程序空間只能裝下虛擬機(jī)的代碼,則可以將這個(gè)編譯器的C代碼用“簡單C” 的語法重新寫一下,(在電腦上)編譯成機(jī)器碼存入SD卡中,讓單片機(jī)運(yùn)行虛擬機(jī),運(yùn)行SD卡中的代碼,就是說在虛擬機(jī)中運(yùn)行編譯器;

比較值得有參考價(jià)值的虛擬機(jī)分析:參考鏈接

而標(biāo)準(zhǔn)規(guī)范的java虛擬機(jī)文檔:鏈接

1.2 名詞

1.2.1 前綴表達(dá)式

也稱為前綴記法、波蘭式。稱作波蘭式是為了紀(jì)念其發(fā)明者波蘭數(shù)學(xué)家Jan Lukasiewicz。

1.2.2 中綴表達(dá)式

同類型的還有前綴表達(dá)式,后綴表達(dá)式。

中綴表達(dá)式是指運(yùn)算符在運(yùn)算數(shù)的中間,計(jì)算中綴表達(dá)式時(shí)需要用到兩個(gè)棧:數(shù)字棧和運(yùn)算符棧。在整個(gè)中綴表達(dá)式求值的過程中主要涉及到的模塊有:棧的相關(guān)操作、優(yōu)先級表的確立、輸入的待計(jì)算字符串拆分為數(shù)字和運(yùn)算符以及運(yùn)算處理等

有一個(gè)比較不錯(cuò)的寫法,鏈接

1.2.3 后綴表達(dá)式

后綴表達(dá)式(又稱為逆波蘭reverse polish)就是不需要括號就可以實(shí)現(xiàn)調(diào)整運(yùn)算順序的一種技巧。

1.2.3 二叉樹

可以通過不同遍歷方式組合成中輟表達(dá)式和后綴表達(dá)式。
二叉樹定義:每個(gè)節(jié)點(diǎn)最多有兩個(gè)子樹的樹結(jié)構(gòu)。

2 rt-thread的finsh shell

先從rt-thead的finsh shell開始研究。
rt-thread的github鏈接地址:下載鏈接

整體代碼邏輯:
在finsh_run_line中調(diào)用以下三個(gè)主要函數(shù),完成大體功能。

  • finsh_parser_run
    完成xxxx
  • finsh_compiler_run
  • finsh_vm_run

這樣的流程,在我的理解來看,就是一個(gè) 解析器(語法解析器),編譯器(解釋器),虛擬機(jī)執(zhí)行的過程。

對finsh_parser_run進(jìn)行分析:


  • 若在shell中輸入表達(dá)式: 1 + 1
  • a.執(zhí)行: proc_expr_statement
  • b.執(zhí)行: proc_expr
  • c.執(zhí)行:proc_assign_expr
  • d.執(zhí)行:proc_inclusive_or_expr,帶有后續(xù)處理
  • e.執(zhí)行:proc_exclusive_or_expr,帶有后續(xù)處理
  • f.執(zhí)行:proc_and_expr,有后續(xù)處理
  • g.執(zhí)行:proc_shift_expr,有后續(xù)處理
  • h.執(zhí)行:proc_additive_expr,有后續(xù)處理
  • i.執(zhí)行:proc_multiplicative_expr,有后續(xù)處理
  • j.執(zhí)行:proc_cast_expr,有后續(xù)處理
  • k.執(zhí)行:proc_unary_expr
  • l.執(zhí)行:proc_postfix_expr,調(diào)用該函數(shù)之前調(diào)用了finsh_token_replay。
  • m.執(zhí)行:proc_primary_expr,最終執(zhí)行到底。

執(zhí)行到proc_primary_expr后,能識別出1。也就是說,執(zhí)行到分支:finsh_token_type_value_int。同時(shí)往數(shù)組global_node_table添加一個(gè)finsh_node。

然后:

  1. 返回到proc_postfix_expr,其中將該函數(shù)返回值賦值給struct finsh_node* postfix;該值應(yīng)該就是前面識別出來的結(jié)點(diǎn)1。隨后調(diào)用next_token,經(jīng)過一系列處理,token的值為:finsh_token_type_add。然后該函數(shù)仍舊執(zhí)行了finsh_token_replay,返回了上面提到的postfix。
  2. 返回到proc_unary_expr,仍舊繼續(xù)返回。返回到,proc_cast_expr。
  3. proc_cast_expr后,返回的仍然是1的結(jié)點(diǎn)。
  4. 后面一直返回到proc_additive_expr,其調(diào)用完成proc_multiplicative_expr后,執(zhí)行next_token,而token的值為finsh_token_type_add。mul是1的結(jié)點(diǎn)。mul_new返回值為 proc_multiplicative_expr的結(jié)果。這一次調(diào)用proc_multiplicative_expr,執(zhí)行到proc_cast_expr,搜尋next_token,這個(gè)時(shí)候找到另外一個(gè)1結(jié)點(diǎn)。最終mul_new就是另外一個(gè)1結(jié)點(diǎn)了。這個(gè)結(jié)點(diǎn)就是global_node_table創(chuàng)建的第二個(gè)值了。
  5. 返回到proc_additive_expr,執(zhí)行make_sys_node,創(chuàng)建FINSH_NODE_SYS_ADD結(jié)點(diǎn)。后執(zhí)行賦值,node->child(+)=先找到的結(jié)點(diǎn)1,結(jié)點(diǎn)1->sibling=結(jié)點(diǎn)1。最后返回FINSH_NODE_SYS_ADD結(jié)點(diǎn)。
  6. 最后返回到self->root = node;那么self->root就是前面提到的FINSH_NODE_SYS_ADD結(jié)點(diǎn)。

  • 對finsh_parser_run進(jìn)行分析:

  • a.執(zhí)行finsh_type_check
  • b.清除text_segment,finsh_vm_stack。設(shè)置finsh_compile_sp,finsh_compile_pc。
  • c.獲取sibling node,調(diào)用finsh_compile,傳參數(shù)為root node,如果有sibling,則彈出棧sibling node。

重點(diǎn)在分析finsh_compile上:

  • a.判斷node->child是否為空,否則編譯child結(jié)點(diǎn)。這里使用的遞歸的操作方法。
  • b.判斷node->node_type,最先判斷的child的node_type,為FINSH_NODE_VALUE_INT。接著執(zhí)行,
finsh_code_byte(FINSH_OP_LD_DWORD);
finsh_code_dword(node->value.long_value);
  • c.遞歸獲取到child->sibling結(jié)點(diǎn)。編譯sibling結(jié)點(diǎn)。
finsh_code_byte(FINSH_OP_LD_DWORD);
finsh_code_dword(node->value.long_value);
  • d.判斷的是root->node_type,是FINSH_NODE_SYS_ADD。執(zhí)行:
finsh_code_byte(FINSH_OP_ADD_BYTE);

從這里看,仍然是一種后綴表達(dá)式的形式。

  • 對finsh_vm_run進(jìn)行分析:

    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中,數(shù)據(jù)存儲的內(nèi)容為:
0x24
4字節(jié)數(shù)值,低字節(jié)在前,高字節(jié)在后。
0x24
4字節(jié)數(shù)值,低字節(jié)在前,高字節(jié)在后。
0x03

所以,虛擬機(jī)執(zhí)行的為:

  • a.執(zhí)行OP_ld_dword,獲取4字節(jié)數(shù)據(jù),然后放到棧上。同時(shí)pc指針前進(jìn)。
  • b.繼續(xù)執(zhí)行a步驟操作。
  • c.執(zhí)行OP_add_dword,最終調(diào)用OP_BIN_DWORD,將棧中數(shù)據(jù)進(jìn)行處理,同時(shí)出棧指針遞減,也就是出棧數(shù)據(jù)。

自此,按照例子 1 + 1的整個(gè)路徑分析完成。

那么,三個(gè)函數(shù)做的事情,總結(jié)如下:

  • finsh_parser_run主要是將字符串,一個(gè)個(gè)分拆,也就是編譯原理中會說的詞法分析。然后形成一棵樹。
  • finsh_compiler_run主要是將樹中的數(shù)據(jù)提取出來,放到虛擬機(jī)中的代碼段中。這里并沒有區(qū)分?jǐn)?shù)據(jù)區(qū)和代碼區(qū),一股腦的放在一起。
  • finsh_vm_run主要是將代碼段中的數(shù)據(jù),根據(jù)操作碼,提取出數(shù)據(jù),放到棧中。這里面用到了后綴表達(dá)式,先提取出兩個(gè)數(shù),然后第三個(gè)數(shù)是+,-,*,/等等的操作碼,計(jì)算完成后,可以出一個(gè)棧的數(shù)據(jù),然后繼續(xù)處理。

接著再來看看輸入一個(gè)函數(shù),是怎樣的執(zhí)行流程。比如hello():
我先說明一點(diǎn),輸入函數(shù),函數(shù)的參數(shù)個(gè)數(shù),rt-thread的shell并沒有做檢測,這應(yīng)該是一個(gè)失誤,是一個(gè)很不好的設(shè)計(jì)。

    1. finsh_parser_run中調(diào)用token_run中,獲取到current_token為finsh_token_type_identifier。后執(zhí)行到proc_primary_expr,執(zhí)行finsh_node_new_id(),會先調(diào)用finsh_var_lookup(),沒找到就調(diào)用finsh_sysvar_lookup(),仍沒找到,就調(diào)用finsh_syscall_lookup(),當(dāng)然按照例子上來看,最終是找到了。最后會調(diào)用finsh_node_allocate(),分配結(jié)點(diǎn),node_type為FINSH_NODE_ID。也就是下面這幾個(gè):
global_node_table[i].node_type = FINSH_NODE_ID;
node->id.syscall = xxx;
node->idtype = FINSH_IDTYPE_SYSCALL;
  1. 執(zhí)行到函數(shù)proc_postfix_expr(),調(diào)用完proc_primary_expr()后,執(zhí)行next_token(),進(jìn)入分支finsh_token_type_left_paren,接著就去尋找右括號。我們不看找不到右括號或者帶參數(shù)情況,只看找到了右括號后的執(zhí)行路徑。
    3.調(diào)用make_sys_node,這個(gè)時(shí)候,添加結(jié)點(diǎn)FINSH_NODE_SYS_FUNC,并把上面的FINSH_NODE_ID添加進(jìn)來。也就是說,root為FINSH_NODE_SYS_FUNC結(jié)點(diǎn),child為FINSH_NODE_ID,sibling為空。
    4.調(diào)用finsh_compile,進(jìn)入分支FINSH_NODE_ID,判斷child是否為空,若不為空,則調(diào)用
                    finsh_code_byte(FINSH_OP_LD_DWORD);
                    finsh_code_dword((long)node->id.syscall->func);

在代碼段中形成的結(jié)構(gòu)為:
0x24
函數(shù)地址
接著執(zhí)行FINSH_NODE_SYS_FUNC,調(diào)用:

                    finsh_code_byte(FINSH_OP_SYSCALL);
                    finsh_code_byte(parameters);

其中參數(shù)個(gè)數(shù)為0
在代碼段中存儲的數(shù)據(jù)為:
0x2C
0

  • 執(zhí)行虛擬機(jī)函數(shù)finsh_vm_run(),調(diào)用OP_ld_dword(),將函數(shù)地址入棧,finsh_sp->long_value=函數(shù)地址。然后調(diào)用OP_call(),獲取參數(shù)個(gè)數(shù),當(dāng)然為0。后面就是相關(guān)的函數(shù)調(diào)用了。

好了,rt-thread的finsh shell就講到這里。實(shí)話說,其調(diào)用關(guān)系比較層疊??雌饋砻總€(gè)函數(shù)調(diào)用關(guān)系分開,寫得很好。但是,目前很多的特性,支持力度還是有限的。

后想了想,對于我們常見的說法,函數(shù)參數(shù)列表先入棧的是最后一個(gè)參數(shù),這里可以驗(yàn)證一下:

找到函數(shù)proc_param_list,這個(gè)就是處理參數(shù)列表的。
最終調(diào)用到proc_cast_expr() -->proc_type()---> 找到第一個(gè)參數(shù)(我們先分析參數(shù)全為數(shù)字),則token為finsh_token_type_value_int---->遞歸調(diào)用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;
當(dāng)然在proc_postfix_expr()中調(diào)用的next_token,會設(shè)置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));
    }

從這個(gè)循環(huán)代碼中,可以發(fā)現(xiàn)是一直在建立siblings。后面finsh_compile被調(diào)用,然后遞歸生效,最后一個(gè)參數(shù)被加入到棧中。

所以,很明顯咯,是遞歸導(dǎo)致的棧序改變。

還可以看看rt-thread的shell的函數(shù)調(diào)用,一層一層的,其實(shí)是按照操作符的優(yōu)先級來的。

2.1 一些擴(kuò)展知識

在寫完一些內(nèi)容之后,發(fā)現(xiàn)finsh shell有一些命名技巧,這就涉及到一些知識。我也是邊看就搜咯。

2.1.1 siblings

這個(gè)在二叉樹上有介紹,表示的是兄弟結(jié)點(diǎn)。二叉樹有完美二叉樹、完全二叉樹和完滿二叉樹。

寫到這里,我只好表示,我的樹知識欠缺,只好另外開一個(gè)欄,學(xué)習(xí)樹了。
鏈接:鏈接地址

2.2 分析開始

分析之初,這幾個(gè)結(jié)構(gòu)體比較重要:

3 picoC

源碼的下載地址為:下載鏈接

從其README.md上看,其說明了picoC是一個(gè)小的C解釋器腳本。它最初是作為一個(gè)無人機(jī)的飛行系統(tǒng)板上的腳本語言。核心的C源代碼是大約3500行。并不打算成為一個(gè)完整的ISO C實(shí)現(xiàn)。picoc已經(jīng)在x86-32,x86-64,PowerPC,arm,UltraSPARC,HP-PA和Blackfin處理器上測試過,并很容易地移植到新的目標(biāo)。

picoc代碼上看,基本有如下幾塊:lex詞法解析,table一個(gè)基本數(shù)據(jù)結(jié)構(gòu)(用于存放變量),是個(gè)字符串hash表,heap管理內(nèi)存分配(包括了stack frame的實(shí)現(xiàn)), type做類型管理(基本型別和程序自定義的struct,typedef等),expression做表達(dá)式解析,variable變量管理分配釋放棧幀。

2.1 編譯

在NIX/Linux/POSIX主機(jī)上:

  • make
  • make test

之后,可以執(zhí)行./picoc。
可以有三種執(zhí)行方式:

圖1

2.2 移植

  1. platform_XXX.c 包含一些支持性的函數(shù)。編譯器這樣才能在你的平臺上運(yùn)行。例如如何向控制臺輸出字符。
  2. platform_library.c包含一些函數(shù)庫,這些庫是提供給用戶程序的。
  3. 有clibrary.c,包含用戶庫函數(shù),比如printf,這些是平臺無關(guān)的。

移植系統(tǒng),需要在platform.h中設(shè)置相應(yīng)的includes和defines。在platform_XXX.c中編寫I/o函數(shù)。在platform_library.c 中編寫用戶函數(shù)。在picoc.c中編寫一些main函數(shù)內(nèi)容。

platform.h默認(rèn)設(shè)置為unix主機(jī)(UNIX_HOST)。

2.3 協(xié)議

picoc遵循"New BSD License".

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容