前言
这一节我们来深入解析与调用相关的指令,这些指令是:
- OP_CALL 调用
- OP_TAILCALL 尾调用
- OP_VARARG 可变参数
- OP_RETURN 返回
解析这些指令的过程中,最重要的是时刻跟踪栈的变化情况。
简单调用
- OP_CALL 的语法是:
R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))
。- R(A)为要调用的函数本身
- 如果B=1,表示没有参数,如果B>1,表示有B-1个参数,这些参数从寄存器R(A+1)开始。
- 函数调用完之后,如果C=1,表示没有返回值,如果C>1,表示有C-1个返回值,这些返回值会存到寄存器R(A)和它后面。从这里可以看出,本来存函数的R(A)最后被替换为返回值。
- OP_RETURN 函数返回指令,语法是:
return R(A), ... ,R(A+B-2)
- 如果B=1,表示没有返回值,如果B>1,表示有B-1个返因值,这些返回值就存在寄存器R(A)和它后面。这和OP_CALL是相呼应的。
有了上面两个指令,就可以进行函数调用,先看下面的Lua代码:
1
2 local function add(a, b)
3 return a + b
4 end
5
6 local function div(a, b)
7 return a // b, a % b
8 end
9
10 local function main()
11 local s = add(10, 20)
12 local d, v = div(s, 8)
13 print(d, v)
14 end
15
16 main()
通过luac查看上面几个函数的操作码,在每行操作码的最后,我加上了栈的内容,用<>
括起来,栈从函数对象开始,函数对象的后面为base, 操作码中的数字大多相对于base,比如0表示base自己,1表示base+1。
下面是main函数:
1 [11] GETUPVAL 0 0 ; add -- <main|add>
2 [11] LOADK 1 -1 ; 10 -- <main|add|10>
3 [11] LOADK 2 -2 ; 20 -- <main|add|10|20>
4 [11] CALL 0 3 2 -- <main|30>
5 [12] GETUPVAL 1 1 ; div -- <main|30|div>
6 [12] MOVE 2 0 -- <main|30|div|30>
7 [12] LOADK 3 -3 ; 8 -- <main|30|div|30|8>
8 [12] CALL 1 3 3 -- <main|30|3|6>
9 [13] GETTABUP 3 2 -4 ; -- <main|30|3|6|print>
10 [13] MOVE 4 1 -- <main|30|3|6|print|3>
11 [13] MOVE 5 2 -- <main|30|3|6|print|3|6>
12 [13] CALL 3 3 1 -- <main|30|3|6>
13 [14] RETURN 0 1 -- <>
下面是add函数:
1 [3] ADD 2 0 1 -- <add|10|20|30>
2 [3] RETURN 2 2 -- <30>
下面是div函数:
1 [7] IDIV 2 0 1 -- <div|30|8|3>
2 [7] MOD 3 0 1 -- <div|30|8|3|6>
3 [7] RETURN 2 3 -- <3|6>
一开始main函数的调用信息和栈是这样的:
执行了add函数之后,调用信息和栈变成这样:
add函数返回,再调用div之后,调用信息和栈变成这样:
函数返回结果作为函数参数
把上面的Lua代码修改一下,变成下面这样:
local function add(a, b)return a + b
endlocal function div(a, b)return a // b, a % b
endlocal function main()local r = add(div(10, 4))local s = "sum=" .. rprint(s)
endmain()
变化之处是div函数的返回结果,直接作为add的参数。这一改变使得VM不知道add会得到多少参数,只能借助于div返回的栈顶。
OP_CALL的B和C有另一种情况,B=0时,参数从R(A+1)一直到栈顶;C=0时,返回值从R(A)一直到栈顶。借助这两种情况就能实现上面的逻辑,main函数如下:
1 [11] GETUPVAL 0 0 ; add -- <main|add>
2 [11] GETUPVAL 1 1 ; div -- <main|add|div>
3 [11] LOADK 2 -1 ; 10 -- <main|add|div|10>
4 [11] LOADK 3 -2 ; 4 -- <main|add|div|10|4>
5 [11] CALL 1 3 0 -- <main|add|2|2>
6 [11] CALL 0 0 2 -- <main|4>
7 [12] LOADK 1 -3 ; -- <main|4|sum=>
8 [12] MOVE 2 0 -- <main|4|sum=|4>
9 [12] CONCAT 1 1 2 -- <main|4|sum=4>
10 [13] GETTABUP 2 2 -4 ; -- <main|4|sum=4|print>
11 [13] MOVE 3 1 -- <main|4|sum=4|print|sum=4>
12 [13] CALL 2 2 1 -- <main|4|sum=4>
13 [14] RETURN 0 1 -- <>
第5行CALL 1 3 0
,C=0,表示返回结果从R(1)一直到栈顶;第6行CALL 0 0 2
,B=0,表示add的参数从R(1)一直栈顶。
从VM代码看,调用div时,新的CallInfo的nresults等于-1,这表示函数的返回值为LUA_MULTRET
;在div返回时,moveresults
判断如果CallInfo的nresults等于-1,就返回函数的实际返回值,并且将L->top
调整到n个返回值之后。紧接着下一条指令是对add的调用,就能根据L->top
得到实际的参数。
可变参数
可变参数的指令是OP_VARARG:
- OP_VARARG 语法是
R(A), R(A+1), ..., R(A+B-1) = vararg
,如果B>0,表示从可变参数接受B-1个参数,如果可变参数不满B-1个,则后面自动填nil。如果B=0,则将传进函数的所有可变参数赋值给R(A)...。
看下面代码:
local function main(...)print(...)return ...
endmain(10, "ok", false)
main函数的操作码如下:
1 [16] GETTABUP 0 0 -1 ; -- <main|10|ok|false|print>
2 [16] VARARG 1 0 -- <main|10|ok|false|print|10|ok|false>
3 [16] CALL 0 0 1 -- <main|10|ok|false>
4 [17] VARARG 0 0 -- <main|10|ok|false|10|ok|false>
5 [17] RETURN 0 0 -- <10|ok|false>
这里要注意一点是main函数的栈base是从可变参数之后开始的,即false后面的寄存器为0。
第2行VARARG 1 0
, B=0,表示将所有可变参数保存到R(1)和后面的寄存器,然后设置好L->top
。
第3行调用print,B=0,所以参数就是从R(1)一直到L->top
。
第4行返回可变参数,B=0,表示将所有可变参数保存到R(0)和后面的寄存器,然后设置好L->top
第5行函数返回,B=0,表示将R(0)到L->top
作为返回值。
尾调用
尾调用使用OP_TAILCALL指令,它和OP_CALL的不同之处是,这个指令不会生成新的CallInfo,它会重用调用者的CallInfo,因为尾调用只能在最后一条返回语句产生,在那一刻调用者的CallInfo已经使用完毕,所以可以重用这个CallInfo。尾调用只能是Lua函数,具体可看lvm.c的OP_TAILCALL指令处理。
其他方面和OP_CALL的含义基本一致,下面是一个例子:
local function div(a, b)return a // b, a % b
endlocal function calc(a, b)return div(a, b)
endcalc(10, 3)
calc里面的div调用就是一个尾调用,calc的指令如下:
1 [7] GETUPVAL 2 0 ; div -- <calc|10|3|div>
2 [7] MOVE 3 0 -- <calc|10|3|div|10>
3 [7] MOVE 4 1 -- <calc|10|3|div|10|3>
4 [7] TAILCALL 2 3 0 -- <calc|10|3|3|1>
5 [7] RETURN 2 0 -- <3|1>
第4行的C=0,表示返回值为从R(2)一直到栈顶。第5行的B=0,表示从R(2)一直到栈顶作为返回值。结合上面的例子,能得到这种情况一般都是将上一个函数的返回值作为当前函数的返回值。