文章目录
- 一、Introduction
- 二、Comparison、Test and Copy、JMP
- 三、Unary and Binary ops
- 四、Upvalue and function ops
- 五、Table ops
- 六、Calls and vararg handling
- 七、Return
- 八、Loops and branches
- 九、Function headers
一、Introduction
关于Bytecode介绍的官方文档:http://wiki.luajit.org/Bytecode-2.0
但是最近发现作者将Bytecode和SSA IR的介绍的文档删除了,也不知道是为什么。虽然那些文档写的不太详细,除非看它们在LuaJIT中的具体实现代码,否则很难看懂。这篇文章我尽量说详细点,会结合Bytecode在代码中的具体定义,希望对需要的同学有所帮助。
在LuaJIT的源码中,关于Bytecode的指令格式的定义,在src/lj_bc.h
中。
在使用LuaJIT时,可以加上参数-bl
可以列出所执行lua脚本生成的ByteCode,如:luajit -bl test.lua
指令格式:
ByteCode指令占4个byte(32bit),有两种格式:一种是有ABC三个操作数,一种是只有AD两个操作数。A一般是dst操作数,占8位;B、C为src操作数,占8位;D一般是src操作数,占16位。
/* Bytecode instruction format, 32 bit wide, fields of 8 or 16 bit:
**
** +----+----+----+----+
** | B | C | A | OP | Format ABC
** +----+----+----+----+
** | D | A | OP | Format AD
** +--------------------
** MSB LSB
**
** In-memory instructions are always stored in host byte order.
*/
假设0xbbccaa1e
是一条ByteCode编码,则:op = 0x1e
(src/lj_bc.h中有一个枚举类型BCOp,从中可以查看具体值对应的opcode),A = 0xaa,B = 0xbb,C = 0xcc
;或者op = 0x1e,A = 0xaa,D = bbcc
。
在src/lj_bc.h
定义了get/set指令操作数和操作码的的宏定义。
/* Macros to get instruction fields. */
#define bc_op(i) ((BCOp)((i)&0xff))
#define bc_a(i) ((BCReg)(((i)>>8)&0xff))
#define bc_b(i) ((BCReg)((i)>>24))
#define bc_c(i) ((BCReg)(((i)>>16)&0xff))
#define bc_d(i) ((BCReg)((i)>>16))
#define bc_j(i) ((ptrdiff_t)bc_d(i)-BCBIAS_J)
/* Macros to set instruction fields. */
#define setbc_byte(p, x, ofs) \
((uint8_t *)(p))[LJ_ENDIAN_SELECT(ofs, 3-ofs)] = (uint8_t)(x)
#define setbc_op(p, x) setbc_byte(p, (x), 0)
#define setbc_a(p, x) setbc_byte(p, (x), 1)
#define setbc_b(p, x) setbc_byte(p, (x), 3)
#define setbc_c(p, x) setbc_byte(p, (x), 2)
#define setbc_d(p, x) \
((uint16_t *)(p))[LJ_ENDIAN_SELECT(1, 0)] = (uint16_t)(x)
指令定义:
关于指令的定义,在下面的这个结构中有说明,#define BCDEF(_)
这个宏中定义了每个指令格式和其操作数类型,可以查阅。
/* Bytecode opcode numbers. */
typedef enum {
#define BCENUM(name, ma, mb, mc, mt) BC_##name,
BCDEF(BCENUM)
#undef BCENUM
BC__MAX
} BCOp;
指令名后面会跟个后缀,用于区分操作数B、C或D的不同类型。如ADDVN
和ADDNV
:前者表示dst = V + N
,即操作数B是一个变量,操作数C是一个常量;后者表示dst = N + V
,即操作数B是一个常量,操作数C是一个变量。这种后缀代表含义如下:
/*
** The opcode name suffixes specify the type for RB/RC or RD:
** V = variable slot
** S = string const
** N = number const
** P = primitive type (~itype)
** B = unsigned byte literal
** M = multiple args/results
*/
V对应的操作数类型是变量,LuaJIT用一个Stack维护VM的运行时,variable slot表示的是变量在Stack中的index,关于var stack的介绍请阅读《LuaJIT 栈帧布局(stack frames layout)》。
N和S表示对应操作数类型是number和string。string操作数的中的值肯定是string在其存放结构中的index;number如果是位数小于16位的整数,则操作数的值是number字面值,否则就是number值存放结构中的index。其实string和number存放在同一个结构中,我把它叫做常量数组,同Stack一样都是很重要的结构,关于常量数组的介绍请阅读《LuaJIT 常量数组(constant array)》。
P表示操作数是一个primitive类型,B就是一个字节的字面值,M表示多个args/results.
二、Comparison、Test and Copy、JMP
Comparison、test及copy指令后面会紧跟一条JMP
跳转指令,如果comparison或test的运算结果为true
,JMP
会跳转到指定的目标处,否则就会继续执行JMP
之后的指令。
Comparison and JMP:
comparison指令总共有12
条,如下是#define BCDEF(_)
中的定义
/* Comparison ops. ORDER OPR. */ \
_(ISLT, var, ___, var, lt) \ // A < D
_(ISGE, var, ___, var, lt) \ // A >= D
_(ISLE, var, ___, var, le) \
_(ISGT, var, ___, var, le) \
\
_(ISEQV, var, ___, var, eq) \
_(ISNEV, var, ___, var, eq) \
_(ISEQS, var, ___, str, eq) \
_(ISNES, var, ___, str, eq) \
_(ISEQN, var, ___, num, eq) \
_(ISNEN, var, ___, num, eq) \
_(ISEQP, var, ___, pri, eq) \
_(ISNEP, var, ___, pri, eq) \
/* JMP. */ \
_(JMP, rbase, ___, jump, ___) \
comparison指令语法为OP A D
,用于求操作数A
、D
的关系运算结果。当comparison指令的运算结果为true
时,JMP的跳转目标是对应的是if
语句之外的语句或者else
块中的语句。比如source code内容为:if(a > b) then stat1; end;
,则对应的指令如下:
ISGE b a
JMP target
stat1
...
test和copy指令:
test和copy指令用于检查一个boolean
变量在上下文中的计算结果,如果计算结果是nil
或false
,指令认为值为false
,其他任何计算结果指令都认为是true
。
copy指令ISTC A D
:如果D
为true
,copy D
的值到A
中,执行后面紧跟的JMP
指令跳转到指定的目标处,否则不跳转继续执行JMP
之后的指令。指令ISFC A D
则是在D为false时,先copy再jmp。
/* copy ops. */ \
_(ISTC, dst, ___, var, ___) \ // if D==true,则A=D且执行JMP
_(ISFC, dst, ___, var, ___) \ // if D==false,则A=D且执行JMP
test指令IST D
,如果D
为true
,执行后面紧跟的JMP
指令跳转到指定的目标处,否则不跳转继续执行JMP
之后的指令。指令ISTYPE A D
,A是个变量,D是类型值,如果A的类型值(itype
,后面会专门写一片文章讲TValue结构,会涉及到这个知识)和D的值相等,则啥也不干,否则将A的类型转换为D的类型。ISNUM
与ISTYPE类似。
/* Unary test ops. */ \
_(IST, ___, ___, var, ___) \ // if D == true, 则执行JMP
_(ISF, ___, ___, var, ___) \ // if D == false, 则执行JMP
_(ISTYPE, var, ___, lit, ___) \
_(ISNUM, var, ___, lit, ___) \
三、Unary and Binary ops
一元运算指令有4
条。MOV A D
将D中的值复制到A中;NOT A D
对D中的值做按位逻辑非运算,运算结果放入A;UNM是对D中的值取反;LEN
是求字符串或table的长度。
/* Unary ops. */ \
_(MOV, dst, ___, var, ___) \
_(NOT, dst, ___, var, ___) \
_(UNM, dst, ___, var, unm) \
_(LEN, dst, ___, var, len) \
二元运算指令有17
条,上文介绍指令定义 中有使用过这部分的例子。基本的运算是加减乘除取余
;POW a b c => a = b^c
;CAT
则是将B和C连接,结果放入A。
/* Binary ops. ORDER OPR. VV last, POW must be next. */ \
_(ADDVN, dst, var, num, add) \
_(SUBVN, dst, var, num, sub) \
_(MULVN, dst, var, num, mul) \
_(DIVVN, dst, var, num, div) \
_(MODVN, dst, var, num, mod) \
\
_(ADDNV, dst, var, num, add) \
_(SUBNV, dst, var, num, sub) \
_(MULNV, dst, var, num, mul) \
_(DIVNV, dst, var, num, div) \
_(MODNV, dst, var, num, mod) \
\
_(ADDVV, dst, var, var, add) \
_(SUBVV, dst, var, var, sub) \
_(MULVV, dst, var, var, mul) \
_(DIVVV, dst, var, var, div) \
_(MODVV, dst, var, var, mod) \
\
_(POW, dst, var, var, pow) \
_(CAT, dst, rbase, rbase, concat) \
四、Upvalue and function ops
这里介绍Lua中有关于函数操作相关的两个概念,知道这两个概念再去理解相关指令就很好理解:一个是外部局部变量,也称为upvalue,另一个是closure,翻译过来叫闭包。
外部局部变量(upvalue):
Lua 中的函数是一阶类型值(first-class value),定义函数就象创建普通类型值一样(只不过函数类型值的数据主要是一条条指令而已),所以在函数体中仍然可以定义函数。假设函数f2
定义在函数f1
中,那么就称f2
为f1
的内嵌(inner)函数,f1
为f2
的外包(enclosing)函数,外包和内嵌都具有传递性,即f2
的内嵌必然是f1
的内嵌,而f1
的外包也一定是f2
的外包。内嵌函数可以访问外包函数已经创建的所有局部变量,这种特性便是所谓的词法定界(lexical scoping),而这些局部变量则称为该内嵌函数的外部局部变量(external local variable)或者upvalue(是变量而不是值)。
19 function f1(n)
20 local function f2()
21 print(n)
22 end
23 return f2
24 end
25
26 do
27 g1 = f1(1024)
28 g1()
29 end
-- BYTECODE -- test_while.lua:20-22
0001 GGET 0 0 ; "print"
0002 UGET 2 0 ; n
0003 CALL 0 1 2
0004 RET0 0 1
-- BYTECODE -- test_while.lua:19-24
0001 FNEW 1 0 ; test_while.lua:20
0002 UCLO 0 => 0003
0003 => RET1 1 2
-- BYTECODE -- test_while.lua:0-30
0001 FNEW 0 0 ; test_while.lua:19
0002 GSET 0 1 ; "f1"
0003 GGET 0 1 ; "f1"
0004 KSHORT 2 1024
0005 CALL 0 2 2
0006 GSET 0 2 ; "g1"
0007 GGET 0 2 ; "g1"
0008 CALL 0 1 1
0009 RET0 0 1
函数f1
的参数n
是函数f2
的upvalue,当执行完g1 = f1(1024)
后,局部变量n
的生命周期本该结束,但因为它已经成了内嵌函数f2
的upvalue,并且它又被赋给了变量g1
,所以它仍然能以某种形式继续“存活”下来,从而令g1()
打印出正确的值。
闭包(closure):
Lua解析一个函数编译生成字节码时,会为它生成一个原型(prototype),其中包含了函数体对应的ByteCode指令、函数用到的常量值(数字字面量,文本字符串等等)和一些调试信息。在运行时,每当Lua执行一个形如function...end
这样的表达式时,它就会根据原型创建一个新的数据对象(ByteCode的FNEW
指令),其中包含了相应函数原型的引用、环境(environment,用来查找全局变量的表)的引用以及一个由所有upvalue引用组成的数组,而这个数据对象就称为闭包。由此可见,函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。
这部分操作的指令总共有7
条,除了FNEW
之外其余都是以U
开头。
/* Upvalue and function ops. */ \
_(UGET, dst, ___, uv, ___) \
_(USETV, uv, ___, var, ___) \
_(USETS, uv, ___, str, ___) \
_(USETN, uv, ___, num, ___) \
_(USETP, uv, ___, pri, ___) \
_(UCLO, rbase, ___, jump, ___) \
_(FNEW, dst, ___, func, gc) \
对上面这几条指令逐一解释:
-
UGET dst uv
指令:将upvale值uv复制到solt dst中。注意按照指令格式与dst(对应A)和uv(对应D)对位,后面也是。 -
USETV uv var
指令:将solt var中的值复制到uv,这一条指令同上一条指令不一样,需要考虑GC,即三色标记的白色不能赋值给黑色这一问题。 -
USETS
、USETN
和USETP
同USETV一样,只是类型不同而已。 -
UCLO rbase jump
指令:close栈上满足条件slots ≥ rbase的所有upvalue值,upvalue有两种状态,open和close。 -
FNEW dst func
指令:根据函数原型func创建一个函数闭包,放进solt dst中。该指令一般在函数调用时会使用,如调用一个自定义lua函数之前,需要创建函数的闭包。
五、Table ops
Lua中的table是一个关联数组,除了nil
以外的任何值都可以做为key
,大小不固定,可动态扩容。
这里说一下关联数组和哈希表的区别:哈希表在存值时,必须使用key
值,你可以将key
设置位1
、2
、3
这种类似于数组index的形式,但必须有key
,如array = {"123" = "Lua", "456" = "Tutorial"}
;关联数组则同时兼具了普通数组和哈希表的功能,普通数组可以在存值时不用index(key),只要有value,会有默认的index,如array = {"Lua", "Tutorial"}
。
这部分指令看着多,其实也挺好理解的。
/* Table ops. */ \
_(TNEW, dst, ___, lit, gc) \
_(TDUP, dst, ___, tab, gc) \
_(GGET, dst, ___, str, index) \
_(GSET, var, ___, str, newindex) \
_(TGETV, dst, var, var, index) \
_(TGETS, dst, var, str, index) \
_(TGETB, dst, var, lit, index) \
_(TGETR, dst, var, var, index) \
_(TSETV, var, var, var, newindex) \
_(TSETS, var, var, str, newindex) \
_(TSETB, var, var, lit, newindex) \
_(TSETM, base, ___, num, newindex) \
_(TSETR, var, var, var, newindex) \
配合下面给出的解释和例子,对上面这几条指令逐一解释:
-
TNEW
指令的D
操作数占16
位,低11
位存放数组size,也就是要将table当作普通数组使用;高5
位存放一个叫做hsize的值,也就是要将table当作哈希表使用,用2^hsize
结果作为哈希表的大小。 -
GGET
和GSET
指令,可以理解为是在当前函数执行环境的全局符号表中获取或存放对象,_G
就是全局符号表。 -
TGETS
、TSETS
等指令是以C
作为B
的index存取值,下面例子中的指令TGETS 0 0 5 ; "sort"
的意思是:在名字是table的table中取出key = sort
的value。 TSETM
指令,一般在有变参的函数中使用,solt(A-1)
中存放的肯定是个table,如下图所示,TSETM
需要做table[val] = solt A
、table[val + 1] = solt(A + 1)
、table[val] = solt(A + 2)
……直到MULTRES
为零的时候,赋值结束。
这是一个对table操作的例子:
do
fruits = {"banana","orange","apple","grapes"}
for k,v in ipairs(fruits) do
print(k,v)
end
table.sort(fruits)
for k,v in ipairs(fruits) do
print(k,v)
end
end
0001 TDUP 0 0
0002 GSET 0 1 ; "fruits"
0003 GGET 0 2 ; "ipairs"
0004 GGET 2 1 ; "fruits"
0005 CALL 0 4 2
0006 JMP 3 => 0011
0007 => GGET 5 3 ; "print"
0008 MOV 7 3
0009 MOV 8 4
0010 CALL 5 1 3
0011 => ITERC 3 3 3
0012 ITERL 3 => 0007
0013 GGET 0 4 ; "table"
0014 TGETS 0 0 5 ; "sort"
0015 GGET 2 1 ; "fruits"
0016 CALL 0 1 2
0017 GGET 0 2 ; "ipairs"
0018 GGET 2 1 ; "fruits"
0019 CALL 0 4 2
0020 JMP 3 => 0025
0021 => GGET 5 3 ; "print"
0022 MOV 7 3
0023 MOV 8 4
0024 CALL 5 1 3
0025 => ITERC 3 3 3
0026 ITERL 3 => 0021
0027 RET0 0 1
六、Calls and vararg handling
函数调用相关指令总共有8
条,有多参数的,迭代器相关的以及变参的,主要理解CALL指令,在再理解其他指令就很好理解。
/* Calls and vararg handling. T = tail call. */ \
_(CALLM, base, lit, lit, call) \
_(CALL, base, lit, lit, call) \
_(CALLMT, base, ___, lit, call) \
_(CALLT, base, ___, lit, call) \
_(ITERC, base, lit, lit, call) \
_(ITERN, base, lit, lit, call) \
_(VARG, base, lit, lit, ___) \
_(ISNEXT, base, ___, jump, ___) \
对下例中使用到的指令做出如下解释:
43 function add(a, b)
44 return a + b;
45 end
46 do
47 local a = 111;
48 local b = 222;
49 local c = add(a, b);
50 --print(c);
51 end
-- BYTECODE -- test_while.lua:43-45
0001 ADDVV 2 0 1
0002 RET1 2 2
-- BYTECODE -- test_while.lua:0-58
0001 FNEW 0 0 ; test_while.lua:43
0002 GSET 0 1 ; "add"
0003 KSHORT 0 111
0004 KSHORT 1 222
0005 GGET 2 1 ; "add"
0006 MOV 4 0
0007 MOV 5 1
0008 CALL 2 2 3
0009 RET0 0 1
第一个Bytecode lua:43-45
为函数add
的原型,在原型中参数参数默认依次放在slot 0、1...
中。
-
ADDVV
:slot 2 = slot 0 + slot 1。 -
RET1
:第一个参数是存放返回值的slot number,第二个参数是返回值个数+1
。
第二个Bytecode lua:0-58
为do...end
块的字节码,是从这里创建要给add
函数的闭包,再去调用函数的。
-
FNEW
:根据slot 0中存放的函数原型,创建一个闭包并存放到slot 0中。 -
GSET
:将slot 0中存放的闭包放到全局符号表中,key为add。 -
KSHORT
:将常量111
、222
存放到slot 0、slot 1中。 -
GGET
:以key=add,取出全局符号表中存放给的闭包存放到slot 2。 -
MOV
:copy slot 0 to slot 4,copy slot 1 to slot 5。 -
CALL
:第一个参数为被调函数闭包slot number,第二个参数为返回值个数+1
的常数值,第三个参数为被调函数参数个数+1
的常数值。 -
RET0
:这是虚拟机自己补的一条指令,把do...end
当作一个函数来处理,编译器前端基本上都是这么去处理,把source最外层的一个块当作中间码的一个函数去处理。RET0
只改变控制流程,返回值个数为0
。
这里再说几个官方文档中的一些不好理解的点。如CALL
指令的A, ..., A+B-2 = A(A+1, ..., A+C-1)
,现在的LuaJIT版本已经这种结构,而是A, ..., A+B-2 = A(A+2, ..., A+C)
,其中solt A存放callee函数,B表示返回值个数+1
,C表示参数个数+1
,结构var stack如图所示:
比如说call 2 2 3
,调用前statck上A处(slot 2)存放被调函数的闭包,实参是从A+2开始,一直到A+C(A+3);调用结束后,参数个数为2-1=1个,第一个返回值存放在statck上A处(slot 2)。
七、Return
RET
比较好理解,总共有4
条,主要是将被掉函数的返回结果传到主调函数中。
/* Returns. */ \
_(RETM, base, ___, lit, ___) \
_(RET, rbase, ___, lit, ___) \
_(RET0, rbase, ___, lit, ___) \
_(RET1, rbase, ___, lit, ___) \
如RET A D(return A, ..., A+D-2)
,A
是存放第一个返回值的slot number,D
是返回值个数+1
。从slot A连续开始,一直到 slot A+D-2
都是要返回的值。
八、Loops and branches
Lua中有4种循环方式,3种基本的循环语句,1种用于迭代器的循环。根据下图种给出的例子对照阅读。
- 1)for循环:
for i=start,stop,step do body end
=>set start,stop,step FORI body FORL
- 2)while循环:
while cond do body end
=>inverse-cond-JMP LOOP body JMP
- 3)repeat循环:
repeat body until cond
=>LOOP body cond-JMP
- 4)迭代器:
for vars... in iter,state,ctl do body end
=>set iter,state,ctl JMP body ITERC ITERL
1 do
2 local a = 5;
3 local sum = 0;
4 while (a > 1) do
5 sum = sum + a;
6 a = a - 1;
7 end
8
9 for i = 1, 5, 1
10 do sum = sum + i;
11 end
12
13 repeat
14 a = a + 1;
15 sum = sum + a;
16 until(a > 5)
17
18 fruits = { "banana","orange","apple","grapes"}
19 for k,v in ipairs(fruits) do
20 print(k,v);
21 end
22 end
-- BYTECODE -- test_while.lua:0-64
0001 KSHORT 0 5
0002 KSHORT 1 0
0003 => KSHORT 2 1
0004 ISGE 2 0
0005 JMP 2 => 0010
0006 LOOP 2 => 0010
0007 ADDVV 1 1 0
0008 SUBVN 0 0 0 ; 1
0009 JMP 2 => 0003
0010 => KSHORT 2 1
0011 KSHORT 3 5
0012 KSHORT 4 1
0013 FORI 2 => 0016
0014 => ADDVV 1 1 5
0015 FORL 2 => 0014
0016 => LOOP 2 => 0022
0017 ADDVN 0 0 0 ; 1
0018 ADDVV 1 1 0
0019 KSHORT 2 5
0020 ISGE 2 0
0021 JMP 2 => 0016
0022 => TDUP 2 0
0023 GSET 2 1 ; "fruits"
0024 GGET 2 2 ; "ipairs"
0025 GGET 4 1 ; "fruits"
0026 CALL 2 4 2
0027 JMP 5 => 0032
0028 => GGET 7 3 ; "print"
0029 MOV 9 5
0030 MOV 10 6
0031 CALL 7 1 3
0032 => ITERC 5 3 3
0033 ITERL 5 => 0028
0034 RET0 0 1
循环和分支跳转相关的指令总共有12
条,这部分相对比较复杂,因为会涉及到切换JIT模式的热点跟踪和计数。
/* Loops and branches. I/J = interp/JIT, I/C/L = init/call/loop. */ \
_(FORI, base, ___, jump, ___) \
_(JFORI, base, ___, jump, ___) \
\
_(FORL, base, ___, jump, ___) \
_(IFORL, base, ___, jump, ___) \
_(JFORL, base, ___, lit, ___) \
\
_(ITERL, base, ___, jump, ___) \
_(IITERL, base, ___, jump, ___) \
_(JITERL, base, ___, lit, ___) \
\
_(LOOP, rbase, ___, jump, ___) \
_(ILOOP, rbase, ___, jump, ___) \
_(JLOOP, rbase, ___, lit, ___) \
\
_(JMP, rbase, ___, jump, ___) \
对上面几条指令作出简单解释。
- 1)
JMP
指令的A
操作数中存放第一个unused slot(没有被使用的slot)。 - 2)
FORL
,ITERL
和LOOP
主要用来做热点计数,当执行次数达到设定值(src/lj_jit.h
中的hotloop
值)之后,会触发JIT模式。JFORI
,JFORL
,JITERL
andJLOOP
指令条件表达式结果为true
,会进行JIT模式热点跟踪。 - 3)
IFORL
,IITERL
andILOOP
用于将无法再JIT模式下编译的循环列入黑名单,则在解释执行的模式下不会对其热点技术,直接进行解释执行即可。 - 4)
JFORL
,JITERL
andJLOOP
指令的热点数(trance number)存在D
操作数中。 - 5)
*FORL
指令首先会执行idx = idx + step
(改变循环计数器)。*FOR*
指令会检查循环结束条件表达式的值,即idx <= stop (if step >= 0)
oridx >= stop (if step < 0)
,如果为true
,ext idx = idx
,并记录热点数值;否则退出循环,继续执行*FORL
之后的指令。 - 6)
*ITERL
检查slot A中迭代器返回的值是否为非nil
,如果true
,将这个值复制到slot A-1
,并记录热点数值。
九、Function headers
在VM执行每个函数之前,都会执行这部分BC。函数调用需要增加Lua Stack,所以检查Stack是否够用是这部分的工作。还需要检查函数参数是否匹配,比如有的形参个数是3个,实参个数只有两个,这时候就要给缺少的那个形参的位置填充一个值(nil)。
/* Function headers. I/J = interp/JIT, F/V/C = fixarg/vararg/C func. */ \
_(FUNCF, rbase, ___, ___, ___) \
_(IFUNCF, rbase, ___, ___, ___) \
_(JFUNCF, rbase, ___, lit, ___) \
_(FUNCV, rbase, ___, ___, ___) \
_(IFUNCV, rbase, ___, ___, ___) \
_(JFUNCV, rbase, ___, lit, ___) \
_(FUNCC, rbase, ___, ___, ___) \
_(FUNCCW, rbase, ___, ___, ___)
函数的热调用也是在这一块触发的,还有这一部分的作用主要是配合call指令,改变var stack的结构,为callee VM函数构建VM运行时栈,如图所示: