4.3 自定义函数命令
FUNC / ENDFUNC / arg / ret / $func_name 命令。这组命令用来定义函数,这是 Pcode 的最后一组命令,也是最为复杂的一组命令,还是用个简单的例子来说明这组命令吧。
C 语言:
- ...
- sum(1, 2);
- ...
- void sum(int a, int b) {
- return a + b;
- }
对应的Pcode:
- push 1
- push 2
- $sum
- FUNC @sum:
- arg a, b
- push a
- push b
- add
- ret ~
- ENDFUNC
现在来对照着 C 语言中的函数定义和调用来说明这组命令。
FUNC 和 ENDFUNC 分别为函数开始和结尾,FUNC 后的函数名以 @ 开始,这是为了不与系统命令冲突,因为在 C 语言中有可能会定义一个名为 add 或 push 等和系统命令同名的函数。函数名后接一个冒号。
函数体内开始的第一个命令为 arg ,这是声明函数参数的,注意此命令不能和 FUNC 行写在同一行。如果函数没有参数,则此命令可以去掉。声明了函数参数,函数内部就可以根据参数名来引用函数调用者传递进来的参数了。
函数调用的时候,在函数名前加 “$” 就可以了,函数的参数通过栈传递,先 从左向右 将参数压入栈中(再次强调,是 从左向右 ,也是为了更接近于源文件的阅读顺序),再调用函数。
Pcode 虚拟机会将所有用 FUNC 和 ENDFUNC 定义的函数名、函数入口地址及函数参数等相关信息记录在其函数表(func_table)中,当遇到以 $ 开头的命令时,它根据 $ 后面的函数名在函数表中查找,若查找到,则会根据函数信息进行函数调用,若没查找到,则会出错终止。
函数用 ret 命令向调用者返回值,有以下形式:
- ret ; 返回空值 “/”
- ret 1 ; 返回常数
- ret a ; 返回变量值
- ret ~ ; 取出栈顶元素,返回其值。
函数返回时,会将调用者入栈的参数出栈,并在清栈后将返回值压入栈顶。
下面让我们来一步一步的执行这个程序,看看各命令执行过程中 eip 和栈的变化,看看调用者如何向函数传递参数,函数又如何向调用者返回结果。
在以上Pcode程序的第一行添加 “var a, b, c” ,并在 $sum 后面添加 “exit 0” ,之后存为 pcode_2.asm ,和 pysim.py 文件一起都放在终端的当前目录,并在终端输入:
- $ python pysim.py pcode_2.asm -d
终端显示如下,注意模拟器自动在 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 命令的效果是完全一样的。
下面再详细的说明函数调用的整个过程中发生了什么事情,并解释前面的两个问题:(RetInfo) 里面有什么?原来绑定的变量到哪去了?
(1) 在函数调用之前,函数调用者按 从左向右 的顺序把函数的参数压入栈内。
(2) 在函数调用时,也就是 $sum 这条命令执行时, Pcode 虚拟机把:
- 函数的返回地址,也就是 $sum 下面那条命令的地址
- 虚拟机中当前的变量表(var_table)
- 函数的参数数量
这三个东西打包进 (RetInfo) ,并将其压入栈顶。注意这只是虚拟机,栈单元中可以放你想放的任何东西。
之后,虚拟机新建一个空的变量表,再根据 arg 后面的参数名称,把这些参数名称按顺序绑定到调用者压入栈内的几个单元上,并在新的变量表中记录下这些参数名及绑定的地址,再将此变量表设为当前变量表。这就是为什么图 4.6 中,原来的 a, b, c 不见了,而新的 a, b 则绑定到 (RetInfo) 上面的两个单元上。
最后,虚拟机跳转到函数内的第一条命令,开始执行函数过程。
(3) 当函数调用完毕后,也就是 ret 命令执行的时,虚拟机首先根据 ret 命令的型式计算出函数的返回值。
之后虚拟机从不停地将栈顶指针向上退回,直到遇到一个 (RetInfo) ,这时虚拟机将其出栈并解包,得到函数的返回地址、虚拟机的上一个变量表(也是函数调用者的变量表)、以及函数参数的数量。
然后虚拟机根据参数数量清栈(把调用者入栈的参数出栈),并将返回值放入栈顶。
再删掉当前变量表,将上一个变量表恢复为当前变量表,所以图 4.11 中,参数 a, b 都不见了,而原来的 a, b, c 又回来了。
最后,虚拟机跳转到函数的返回地址,开始执行 $sum 后面的命令,整个函数调用过程完毕。