Instruction Analysis of Lua5.3 Virtual Machine (1) Overview

Keywords: Mac C

Instruction Analysis of Lua5.3 Virtual Machine (1) Overview

Summary

Lua VM uses Register based VM. Instructions access operands in registers that have been allocated.
Add a b C adds register b to the value in register c, and the result is in register a. Standard three-address instruction, each instruction has strong expressive ability, and effectively reduces the memory assignment operation.

#if LUAI_BITSINT >= 32
typedef unsigned int Instruction;
#else
typedef unsigned long Instruction;
#endif

OpCode

Lua's instructions are represented by a 32-bit unsigned integer, of which the lower six bits represent the opcode, so it supports up to 2 ^ 6 = 64 instructions. All instructions are defined as follows:

typedef enum {
/*----------------------------------------------------------------------
name        args    description
------------------------------------------------------------------------*/
OP_MOVE,/*  A B R(A) := R(B)                    */
OP_LOADK,/* A Bx    R(A) := Kst(Bx)                 */
OP_LOADKX,/*    A   R(A) := Kst(extra arg)              */
OP_LOADBOOL,/*  A B C   R(A) := (Bool)B; if (C) pc++            */
OP_LOADNIL,/*   A B R(A), R(A+1), ..., R(A+B) := nil        */
OP_GETUPVAL,/*  A B R(A) := UpValue[B]              */

OP_GETTABUP,/*  A B C   R(A) := UpValue[B][RK(C)]           */
OP_GETTABLE,/*  A B C   R(A) := R(B)[RK(C)]             */

OP_SETTABUP,/*  A B C   UpValue[A][RK(B)] := RK(C)          */
OP_SETUPVAL,/*  A B UpValue[B] := R(A)              */
OP_SETTABLE,/*  A B C   R(A)[RK(B)] := RK(C)                */

OP_NEWTABLE,/*  A B C   R(A) := {} (size = B,C)             */

OP_SELF,/*  A B C   R(A+1) := R(B); R(A) := R(B)[RK(C)]     */

OP_ADD,/*   A B C   R(A) := RK(B) + RK(C)               */
OP_SUB,/*   A B C   R(A) := RK(B) - RK(C)               */
OP_MUL,/*   A B C   R(A) := RK(B) * RK(C)               */
OP_MOD,/*   A B C   R(A) := RK(B) % RK(C)               */
OP_POW,/*   A B C   R(A) := RK(B) ^ RK(C)               */
OP_DIV,/*   A B C   R(A) := RK(B) / RK(C)               */
OP_IDIV,/*  A B C   R(A) := RK(B) // RK(C)              */
OP_BAND,/*  A B C   R(A) := RK(B) & RK(C)               */
OP_BOR,/*   A B C   R(A) := RK(B) | RK(C)               */
OP_BXOR,/*  A B C   R(A) := RK(B) ~ RK(C)               */
OP_SHL,/*   A B C   R(A) := RK(B) << RK(C)              */
OP_SHR,/*   A B C   R(A) := RK(B) >> RK(C)              */
OP_UNM,/*   A B R(A) := -R(B)                   */
OP_BNOT,/*  A B R(A) := ~R(B)                   */
OP_NOT,/*   A B R(A) := not R(B)                */
OP_LEN,/*   A B R(A) := length of R(B)              */

OP_CONCAT,/*    A B C   R(A) := R(B).. ... ..R(C)           */

OP_JMP,/*   A sBx   pc+=sBx; if (A) close all upvalues >= R(A - 1)  */
OP_EQ,/*    A B C   if ((RK(B) == RK(C)) ~= A) then pc++        */
OP_LT,/*    A B C   if ((RK(B) <  RK(C)) ~= A) then pc++        */
OP_LE,/*    A B C   if ((RK(B) <= RK(C)) ~= A) then pc++        */

OP_TEST,/*  A C if not (R(A) <=> C) then pc++           */
OP_TESTSET,/*   A B C   if (R(B) <=> C) then R(A) := R(B) else pc++ */

OP_CALL,/*  A B C   R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
OP_TAILCALL,/*  A B C   return R(A)(R(A+1), ... ,R(A+B-1))      */
OP_RETURN,/*    A B return R(A), ... ,R(A+B-2)  (see note)  */

OP_FORLOOP,/*   A sBx   R(A)+=R(A+2);
            if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/*   A sBx   R(A)-=R(A+2); pc+=sBx               */

OP_TFORCALL,/*  A C R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));  */
OP_TFORLOOP,/*  A sBx   if R(A+1) ~= nil then { R(A)=R(A+1); pc += sBx }*/

OP_SETLIST,/*   A B C   R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B    */

OP_CLOSURE,/*   A Bx    R(A) := closure(KPROTO[Bx])         */

OP_VARARG,/*    A B R(A), R(A+1), ..., R(A+B-2) = vararg        */

OP_EXTRAARG/*   Ax  extra (larger) argument for previous opcode */
} OpCode;

Annotations illustrate how the instruction operates.

/*
** R(x) - register
** Kst(x) - constant (in constant table)
** RK(x) == if ISK(x) then Kst(INDEXK(x)) else R(x)
*/

R () means that this must be a register index (must operate on the Lua stack)
RK () indicates that this may be a register index or a constant index. RK can only use parameter B and parameter C (SIZE_B = SIZE_C = 9), where the parameter's highest bit-differentiated register index and constant index.

The following macros provide some operations:

/*
** Macros to operate RK indices
*/

/* this bit 1 means constant (0 means register) */
#define BITRK       (1 << (SIZE_B - 1))

/* test whether value is a constant */
#define ISK(x)      ((x) & BITRK)

/* gets the index of the constant */
#define INDEXK(r)   ((int)(r) & ~BITRK)

#define MAXINDEXRK  (BITRK - 1)

/* code a constant index as a RK value */
#define RKASK(x)    ((x) | BITRK)

Opcode Type

The following table collates the description information of the instructions:

  1. Each instruction affects an object, which is called A. It is represented by 8 bits. A is usually an index of a register, or it may be an operation on Upvalue.
  2. Generally speaking, there are two parameters that affect A. Each parameter is represented by 9 bits, called B and C respectively.
  3. Some instructions do not require two operation parameters, so B and C can be merged into an 18 bits integer Bx to accommodate a larger range.
  4. When the opcode involves a jump instruction, this parameter represents the jump offset. Forward jump requires a negative offset. Such instructions need to be distinguished by symbolic information and recorded as sBx. Among them, 0 is expressed as 2 ^ 17; 1 is expressed as 2 ^ 17 + 1; and -1 is expressed as 2 ^ 17 - 1.
  5. During runtime, Lua VM loads the required constants into registers (Lua stack), and then uses these registers to do the corresponding work. The opcode for loading constants is LOADK, which consists of two parameters A and Bx. This operation loads the constants referred to by Bx into the registers referred to by A. Bx is 18 bit s long, so LOADK can only be indexed to 2 ^ 18 constants. To expand the upper limit of index constants, LOADKX is provided, which places the constant index number in the next EXTRAARG instruction. The OP_EXTRAARG instruction uses 26 bit s other than 8 bits of opcode for parameter representation, which is called * Ax*.
    The bit size and offset of parameters A, B and C are defined by the following macros in Lua:

    /*
    ** size and position of opcode arguments.
    */

    define SIZE_C 9

    define SIZE_B 9

    define SIZE_Bx (SIZE_C + SIZE_B)

    define SIZE_A 8

    define SIZE_Ax (SIZE_C + SIZE_B + SIZE_A)

    define SIZE_OP 6

    define POS_OP 0

    define POS_A (POS_OP + SIZE_OP)

    define POS_C (POS_A + SIZE_A)

    define POS_B (POS_C + SIZE_C)

    define POS_Bx POS_C

    define POS_Ax POS_A

The parameters A, B and C are generally used to store the address (index) of instruction operation data, and the address (index) has three kinds:
1. Register idx
2. Constant scale idx
3. upvalue idx

Personal understanding:
- Lua VM is based on register structure, that is to say, every segment of Lua chunk code can be regarded as a set of instructions translated into 256 registers (Opcode only 8 bit 2 ^ 8 = 256).
- When we write extensions for Lua in C language, C functions usually take parameters from lua-State and record them in local variables of C one by one, then use C code to operate on these values directly.
- The Lua median values are stored in three places: <1> There are local variables in the Lua register (that is, Lua's data stack). \ In the < 2 > Constant Scale, the storage constants are generally stored. \ <3> Some data that is neither constant nor register is stored in the upvalue Table or Table. It can be seen that the index of operation data stored in OpCode parameters A, B and C obtains values in three different storage locations (register, constant Table and upvalue Table) according to different instructions.
- Lua uses the stack of the current function as a register (Lua register = Lua stack), and the register idx starts at 0. The stack of the current function is equivalent to an array of registers, namely stack(n) = register(n).
- Prototype Proto of each function has a constant that belongs to the function and is used to store the constants used by the function in the compilation process. Constants can store data of nil, boolean, number, string type, and constant idx starts from 1.
- The prototype Proto of each function has an upvalue table for storing the upvalue used by the function during compilation. At runtime, when a closure is created through the OP-CLOSURE instruction, the upvalue table is initialized for the closure as described in Proto. Upvalue is also indexed by id. The idx of upvalue starts at 0.

       TTcs-Mac-mini:OpCode ttc$ cat tOP_VALUE.lua 
    local a = 100

    function foo()
        local b = 200
        a = 300
        c = b
    end
    TTcs-Mac-mini:OpCode ttc$ ./luac -l -l tOP_VALUE.lua

    main <tOP_VALUE.lua:0,0> (4 instructions at 0x7f92e44039b0)
    0+ params, 2 slots, 1 upvalue, 1 local, 2 constants, 1 function
        1   [1] LOADK       (iABx) [A]0 [K]-1   ; 100
        2   [7] CLOSURE     (iABx) [A]1 [U]0    ; 0x7f92e4403b70
        3   [3] SETTABUP    (iABC) [A]0 [ISK]256[B]-2[ISK]0[C]1 ; _ENV "foo"
        4   [7] RETURN      (iABC) [A]0 [ISK]0[B]1[ISK]0
    constants (2) for 0x7f92e44039b0:
        1(idx)  100
        2(idx)  "foo"
    locals (1) for 0x7f92e44039b0:
        0   a(name)      2(startpc)     5(endpc)
    upvalues (1) for 0x7f92e44039b0:
        0    _ENV(name)      1(instack)      0(idx)

    function <tOP_VALUE.lua:3,7> (5 instructions at 0x7f92e4403b70)
    0 params, 2 slots, 2 upvalues, 1 local, 3 constants, 0 functions
        1   [4] LOADK       (iABx) [A]0 [K]-1   ; 200
        2   [5] LOADK       (iABx) [A]1 [K]-2   ; 300
        3   [5] SETUPVAL    (iABC) [A]1 [ISK]0[B]0[ISK]0    ; a
        4   [6] SETTABUP    (iABC) [A]1 [ISK]256[B]-3[ISK]0[C]0 ; _ENV "c"
        5   [7] RETURN      (iABC) [A]0 [ISK]0[B]1[ISK]0
    constants (3) for 0x7f92e4403b70:
        1(idx)  200
        2(idx)  300
        3(idx)  "c"
    locals (1) for 0x7f92e4403b70:
        0   b(name)      2(startpc)     6(endpc)
    upvalues (2) for 0x7f92e4403b70:
        0    a(name)     1(instack)      0(idx)
        1    _ENV(name)      0(instack)      0(idx)
    TTcs-Mac-mini:OpCode ttc$ '' 

Lua code a is a local variable. In foo function, A is an upvalue and c is a global variable (_ENV table). It can be seen that the index starting number of stored values in three different places.

At present, Lua 5.3 uses 47 OpCodes, which are divided into four different mode s: iABC, iABx, iAsBx and iAx. Lua defines some instructions for all OpCodes with an array:

#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m))

LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {
/*       T  A    B       C     mode        opcode   */
  opmode(0, 1, OpArgR, OpArgN, iABC)        /* OP_MOVE */
 ,opmode(0, 1, OpArgK, OpArgN, iABx)        /* OP_LOADK */
 ,opmode(0, 1, OpArgN, OpArgN, iABx)        /* OP_LOADKX */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)        /* OP_LOADBOOL */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)        /* OP_LOADNIL */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)        /* OP_GETUPVAL */
 ,opmode(0, 1, OpArgU, OpArgK, iABC)        /* OP_GETTABUP */
 ,opmode(0, 1, OpArgR, OpArgK, iABC)        /* OP_GETTABLE */
 ,opmode(0, 0, OpArgK, OpArgK, iABC)        /* OP_SETTABUP */
 ,opmode(0, 0, OpArgU, OpArgN, iABC)        /* OP_SETUPVAL */
 ,opmode(0, 0, OpArgK, OpArgK, iABC)        /* OP_SETTABLE */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)        /* OP_NEWTABLE */
 ,opmode(0, 1, OpArgR, OpArgK, iABC)        /* OP_SELF */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_ADD */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_SUB */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_MUL */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_MOD */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_POW */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_DIV */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_IDIV */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_BAND */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_BOR */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_BXOR */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_SHL */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)        /* OP_SHR */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)        /* OP_UNM */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)        /* OP_BNOT */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)        /* OP_NOT */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)        /* OP_LEN */
 ,opmode(0, 1, OpArgR, OpArgR, iABC)        /* OP_CONCAT */
 ,opmode(0, 0, OpArgR, OpArgN, iAsBx)       /* OP_JMP */
 ,opmode(1, 0, OpArgK, OpArgK, iABC)        /* OP_EQ */
 ,opmode(1, 0, OpArgK, OpArgK, iABC)        /* OP_LT */
 ,opmode(1, 0, OpArgK, OpArgK, iABC)        /* OP_LE */
 ,opmode(1, 0, OpArgN, OpArgU, iABC)        /* OP_TEST */
 ,opmode(1, 1, OpArgR, OpArgU, iABC)        /* OP_TESTSET */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)        /* OP_CALL */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)        /* OP_TAILCALL */
 ,opmode(0, 0, OpArgU, OpArgN, iABC)        /* OP_RETURN */
 ,opmode(0, 1, OpArgR, OpArgN, iAsBx)       /* OP_FORLOOP */
 ,opmode(0, 1, OpArgR, OpArgN, iAsBx)       /* OP_FORPREP */
 ,opmode(0, 0, OpArgN, OpArgU, iABC)        /* OP_TFORCALL */
 ,opmode(0, 1, OpArgR, OpArgN, iAsBx)       /* OP_TFORLOOP */
 ,opmode(0, 0, OpArgU, OpArgU, iABC)        /* OP_SETLIST */
 ,opmode(0, 1, OpArgU, OpArgN, iABx)        /* OP_CLOSURE */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)        /* OP_VARARG */
 ,opmode(0, 0, OpArgU, OpArgU, iAx)     /* OP_EXTRAARG */
};

Here, a macro opmode encapsulates the specific format of each OpCode, where:
1. T: (7bit) indicates that this is not a logic test-related instruction, which may involve a conditional jump, increasing the PC pointer by 1. (This tag is needed because virtually all of Lua's conditional branches are followed by a JMP instruction immediately after the branch instruction. Lua does not design opcode for Boolean operations alone. It allows all Boolean operations to appear as branch execution streams. Lua's And and Or keywords support short-circuit evaluation, so they are implemented in the form of branch jump in VM. Branch instructions And subsequent JMP jump instructions are integrated because 32-bit Instruction cannot be fully described before it is split into two instructions. This instruction can be used to detect whether it is a branch instruction or not. When encountering a JMP instruction, you can trace back to the previous instruction to determine whether it is a conditional jump. This is helpful for generating Lua's bytecode module.
2. A:(6bit) indicates whether the instruction will modify register A. This tag is used in the debug module to track the location of the instruction that finally changes the register content and to help generate debug info.
3. B: (4-5 bit) B arg mode.
4. C: (2-3 bit) C arg mode.
4. mode:(0-1 bit) OpCode format, these classified information for the output of luac decompiling bytecode, has no practical significance for Lua's runtime.

The Lua code provides the following set of macros to retrieve the corresponding Lua values based on the values in Opcode parameters A, B, and C (Lua VM performs a detailed analysis)

#define RA(i)   (base+GETARG_A(i))
/* to be used after possible stack reallocation */
#define RB(i)   check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i))
#define RC(i)   check_exp(getCMode(GET_OPCODE(i)) == OpArgR, base+GETARG_C(i))
#define RKB(i)  check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \
    ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
#define RKC(i)  check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \
    ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))
#define KBx(i)  \
  (k + (GETARG_Bx(i) != 0 ? GETARG_Bx(i) - 1 : GETARG_Ax(*ci->u.l.savedpc++)))

Posted by DjMikeWatt on Sun, 02 Jun 2019 15:30:37 -0700