C 语言:
对应的Pcode:
现在来对照着 C 语言中的函数定义和调用来说明这组命令。
FUNC 和 ENDFUNC 分别为函数开始和结尾,FUNC 后的函数名以 @ 开始,这是为了不与系统命令冲突,因为在 C 语言中有可能会定义一个名为 add 或 push 等和系统命令同名的函数。函数名后接一个冒号。
函数体内开始的第一个命令为 arg ,这是声明函数参数的,注意此命令不能和 FUNC 行写在同一行。如果函数没有参数,则此命令可以去掉。声明了函数参数,函数内部就可以根据参数名来引用函数调用者传递进来的参数了。
函数调用的时候,在函数名前加 “$” 就可以了,函数的参数通过栈传递,先 从左向右 将参数压入栈中(再次强调,是 从左向右 ,也是为了更接近于源文件的阅读顺序),再调用函数。
Pcode 虚拟机会将所有用 FUNC 和 ENDFUNC 定义的函数名、函数入口地址及函数参数等相关信息记录在其函数表(func_table)中,当遇到以 $ 开头的命令时,它根据 $ 后面的函数名在函数表中查找,若查找到,则会根据函数信息进行函数调用,若没查找到,则会出错终止。
函数用 ret 命令向调用者返回值,有以下形式:
函数返回时,会将调用者入栈的参数出栈,并在清栈后将返回值压入栈顶。
在以上Pcode程序的第一行添加 “var a, b, c” ,并在 $sum 后面添加 “exit 0” ,之后存为 ,和 pysim.py 文件一起都放在终端的当前目录,并在终端输入:
终端显示如下,注意模拟器自动在 ENDFUNC 的后面加了一句 ret ,这样对于函数体内不写任何 ret 的程序,程序运行到此处也会返回的。图4.7 函数调用执行过程1
再敲3下回车,使程序运行到 $sum 这一行,可以看出此时栈上已经分配并绑定了 3 个变量 a, b, c ,函数的参数 1 和 2 也都压入到栈上了。见下图:图4.8 函数调用执行过程2
下面就要开始调用函数了,让我们再敲 1 下回车,看看发生了什么:图4.9 函数调用执行过程3
可以看到,code 区中,eip 已经跳到 sum 函数内的第1条命令 push a 那里了;而栈区中,栈顶向下增长了一个单元,栈顶单元里多了一个 (RetInfo) ,调用者压入的两个参数 1 和 2 被绑定了变量 a 和 b,而原来绑定的三个变量 a, b, c 消失了。
(RetInfo) 里面有什么?原来绑定的变量呢,到哪去了?
我们先把这两个问题放一放,先一步一步运行函数内的命令,到 ret ~ 这一行停下来,见下图:图4.10 函数调用执行过程4
可以看出,此时 a + b 的结果已经计算出来并放到栈顶了,让我们再敲一下回车,执行一下 ret ~ 这条命令,看看发生了什么:图4.11 函数调用单步执行过程5
可以看到,eip 跳回到了 $sum 后面的 exit 0 这一行,栈顶指针向上退回了 3 个单元,新的栈顶元素变成了 3 。我们把图4.11和图4.8对比一下可以看出,eip 移动到了下一条命令,压入的两个参数 1 和 2 出栈了,而 sum(1 , 2) 则被压入了栈顶,这个 $sum 命令和 add 命令的效果是完全一样的。