c语言尚未实现的虚拟函数,编译原理之学习 lua 1.1 笔记 (二) 函数调用与局部变量...
函數(過程)是程序中重要的抽象, 過程調用一般用棧實現. Lua 1.1 中尚未實現閉包(closure),
對于函數使用棧實現即已滿足需求了.在理論上, 在棧中要保存為實現調用以及返回調用處的足夠
信息, 這些信息當前是返回地址(return-address,?;分羔?base-pointer).
在虛擬機指令層次, 指令 CALLFUNC, RETCODE 用于函數調用的核心實現. 另有一些與調用參數, 局部
變量相關的指令, 稍后遇到的時候研究.
對于一個函數, 如例子 function f(a,b) ... end, 對其的調用代碼為:
PUSH f? -- 將函數對象 f 壓入堆棧.
PUSH MARK? --- 將特殊標記值 MARK 壓入堆棧, 作用下面描述.
PUSH a --- 計算得到 a 的代碼, 最終為將 a 的值壓入堆棧.
PUSH b --- 同 a 的情況, 此時棧中數據為: [f, MARK, a, b??)
CALLFUNC --- 實際產生調用.
指令 CALLFUNC 的執行如下:
case CALLFUNC:? --- 位于 opcode.c 的虛擬機執行函數 lua_execute() 中.
1. 從棧頂向上找 MARK 標記, 這一標記的前面是函數 f, 后面是第一個函數參數 a.
2. 從函數對象 f 中獲得該函數的代碼地址 new-pc;
把當前 pc?(也即返回地址)保存在函數對象 f 中. (注1)
設置 pc = new-pc,?此即實現執行地址的改變.
3. 將棧 base 指針保存在 MARK 的值中, 設置新的 base = MARK+1, 即指向棧中第一個
參數 a 的位置.
此后再執行的下一條指令就是函數的入口指令了.
上述的步驟, 所做工作就是保存 , 設置新的運行地址和基址指針. 與機器碼實現
函數調用幾乎是一致的. 略有不同的是由于調用者提供的參數數量可能是0個或多個(可變數量的), 所以
通過查找 MARK 方式找到, 同時 MARK 還用作保存 base 指針, 這種方式較為巧妙.
注1: 將返回地址保存到函數對象中, 這是一個"不好的"方式, 這樣函數就不能重入,遞歸了?
指令 RETCODE 的執行與 CALLFUNC 想對應:
case RETCODE:? --- 虛擬機函數 lua_execute() 中.
1. 由于當前 base 指針-1, -2 分別是保存了 base 的MARK, 和保存了返回地址的函數 f 對象,
故此從中恢復 pc, base 的值.
2. 復制/移動函數返回值 (函數的多返回值機制以后分析, 此處暫略)
此后下一條指令即返回到原調用處的下一條指令位置.
對函數產生調用的代碼, 在如下產生式中生成(代碼生成):
1. stat1 -> functioncall
2. expr -> functioncall
3. functioncall -> functionvalue {代碼塊1} '(' exprlist ')' {代碼塊2}
對上面的產生式3略作記錄:
1. functionvalue 產生計算函數 f 的值的代碼, 例如壓入全局變量 f 到堆棧中. PUSHGLOBAL?f
2. 代碼塊1: 產生代碼 PUSHMARK, 更新 ntemp 值(其用于跟蹤堆棧使用量)
3. exprlist 產生所有參數的計算和壓棧代碼, 如 PUSH a, PUSH b
4. 代碼塊2: 產生代碼 CALLFUNC, 更新 ntemp 值.
這里生成了調用函數 f 的前述代碼序列.
在前面提到過基址指針 base, 相當于 80x86 體系中的寄存器 BP, 用于尋址位于堆棧上的地址.
在 Lua 中, 函數的參數和局部變量存放在棧中, 并使用 base 指針尋址. 下面研究對局部變量訪問的
指令, 及其代碼生成.
在 Lua 中, 讀取局部變量的指令為 PUSHLOCAL(及其同系列的 PUSHLOCAL0~~9),
寫入局部變量的指令為 STORELOCAL(及其同系列的 STORELOCAL0~~9). 那些同系列的指令僅僅
是減少(優化)了指令大小, 語義是一致的, 因此只需要研究 PUSHLOCAL, STORELOCAL 即可.
case PUSHLOCAL:? --- 帶一個字節的指令立即數 i, 局部變量壓入棧中.
*top++ = stack[base + i];
case STORELOCAL: --- 帶一個字節的指令立即數 i, 棧頂值彈出存入局部變量.
stack[base + i] = *(--top);
PUSHLOCAL, STORELOCAL 都帶有一個字節的指令參數, 表示所訪問的局部變量的索引 i, 從 0 開始,
尋址到堆棧 stack[base + i] 位置. 由于只有一個字節, 也即限制最多只能有 256 個局部變量.
由于調用函數時 base 被自動維護, 因此每函數都有自己的局部變量. 又 base 指針從第一個參數開始,
因此參數實現上也是被當做局部變量看待的. 這隱含的幾個問題, 第一個是如果函數所需參數數量,
和實際調用者傳遞的參數數量不一致的問題.
舉例說明, 設函數聲明為 function f(a,b), 而調用者調用為 f(1,2,3), 或 f(4), 即函數參數多或少的情況.
在函數的入口代碼中, 會產生一條 ADJUST n?指令, 其中 n 是函數聲明時的參數數量, 該指令執行如下:
case ADJUST:
在語義上:
1. 如果調用者提供的參數不足 n 個, 則不足的部分以 NIL 值填充.
2. 如果調用者提供的參數多于 n 個, 則多出的被裁剪掉.
最終設置棧頂指針 top = base + n (即多出的參數被裁剪, 如果有的話)
這一指令在產生式 function -> FUNCTION NAME '(' parlist ')' block END 中生成.
隱含的第二個問題是, 由于多出的參數被裁剪掉了, 這樣表示無法提供 f(args, ...) 后面可變參數語義的實現.
預計 lua 以后的版本會使用某種方法實現.
局部變量的聲明, 例如 local x, y, 相關產生式為:
stat1 -> LOCAL localdeclist decinit
localdeclist -> NAME {代碼塊1} | localdeclist ',' NAME
其中代碼塊1:
localvar[nlocalvar]=lua_findsymbol($1);?? --- 查找 $1(即NAME), 并加入到局部變量表中.
$$ = 1;? --- 已聲明的局部變量數.
在函數中聲明的局部變量被放置在 localvar[] 表中, 值是 lua_findsymbol() 的返回值, 即到符號表
symtab 的索引.
在產生式 var -> NAME 中, 第一篇研究全局變量的文章中也有遇見, 其中代碼塊為:
Word s = lua_findsymbol(NAME) --- 查找 NAME 在符號表中的索引.
int local = lua_localname(s)? ---? 在 localvar[] 表中查找是否有 s, 如果有則表示這是個局部
--- 變量.
if (是局部變量)?$$ =?-(local+1) --- 是一個負數, 從而與全局變量的 正的索引 區分開.
在 lua_pushvar() 為訪問 var 生成代碼時, 前一篇文章研究全局變量時也碰到,
為全局變量生成代碼為: PUSHGLOBAL idx-of-symtab
為局部變量生成代碼為: PUSHLOCAL idx-of-localvar, 也即表示在 stack[base+idx]
在產生式 varlist1 -> var ... 中, 記錄 var.$$ 到 varbuffer[] 表中, 在為其生成寫入指令時,
根據是正數生成 全局變量的(STOREGLOBAL), 是負數生成局部變量訪問指令 (STORELOCAL).
生成指令的時候, 如果 idx 在 0~9, 則產生較短的指令 PUSHLOCAL0~9, STORELOCAL0~9.
函數的返回值也是放在棧頂的, 是在 RETCODE 指令中設置好返回給調用者的, 由于 lua 支持多賦值,多返回值, 將它們單獨放一個地方再研究也許更合適一些.
總結
以上是生活随笔為你收集整理的c语言尚未实现的虚拟函数,编译原理之学习 lua 1.1 笔记 (二) 函数调用与局部变量...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数字人民币能否匿名使用 其实匿名性
- 下一篇: bmp文件数字水印c语言,[求助]C语言