LuaJIT Bytecode介绍

一、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的不同类型。如ADDVNADDNV:前者表示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的运算结果为trueJMP会跳转到指定的目标处,否则就会继续执行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,用于求操作数AD的关系运算结果。当comparison指令的运算结果为true时,JMP的跳转目标是对应的是if语句之外的语句或者else块中的语句。比如source code内容为:if(a > b) then stat1; end;,则对应的指令如下:

ISGE     b   a
JMP      target
stat1
...

test和copy指令:

test和copy指令用于检查一个boolean变量在上下文中的计算结果,如果计算结果是nilfalse,指令认为值为false,其他任何计算结果指令都认为是true

copy指令ISTC A D:如果Dtrue,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,如果Dtrue,执行后面紧跟的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^cCAT则是将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中,那么就称f2f1的内嵌(inner)函数,f1f2的外包(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,即三色标记的白色不能赋值给黑色这一问题。
  • USETSUSETNUSETP同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设置位123这种类似于数组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结果作为哈希表的大小。
  • GGETGSET指令,可以理解为是在当前函数执行环境的全局符号表中获取或存放对象,_G就是全局符号表。
  • TGETSTSETS等指令是以C作为B的index存取值,下面例子中的指令TGETS 0 0 5 ; "sort"的意思是:在名字是table的table中取出key = sort的value。
  • TSETM指令,一般在有变参的函数中使用,solt(A-1)中存放的肯定是个table,如下图所示,TSETM需要做table[val] = solt Atable[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-58do...end块的字节码,是从这里创建要给add函数的闭包,再去调用函数的。

  • FNEW:根据slot 0中存放的函数原型,创建一个闭包并存放到slot 0中。
  • GSET:将slot 0中存放的闭包放到全局符号表中,key为add。
  • KSHORT:将常量111222存放到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, ITERLLOOP主要用来做热点计数,当执行次数达到设定值(src/lj_jit.h中的hotloop值)之后,会触发JIT模式。 JFORI, JFORL, JITERL and JLOOP指令条件表达式结果为true,会进行JIT模式热点跟踪。
  • 3)IFORL, IITERL and ILOOP用于将无法再JIT模式下编译的循环列入黑名单,则在解释执行的模式下不会对其热点技术,直接进行解释执行即可。
  • 4) JFORL, JITERL and JLOOP指令的热点数(trance number)存在D操作数中。
  • 5)*FORL指令首先会执行idx = idx + step (改变循环计数器)。*FOR*指令会检查循环结束条件表达式的值,即idx <= stop (if step >= 0) or idx >= stop (if step < 0),如果为trueext 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运行时栈,如图所示:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值