lua 多条件_深入Lua:虚拟机指令集

Lua如何执行代码

这一篇稍微深入一点,大概说说Lua虚拟机的指令集。试想一下原生语言是如何跑起来的?

  1. 编译器将程序编译成平台相关的机器码。
  2. 然后CPU一条条的执行里面的指令。
  3. 指令需要的操作数放在内存中:可能在线程相关的栈里,也可能在进程相关的堆里,不管在哪里它都是一个内存地址,用间接或直接的方式从该地址取数据就是。

脚本语言本质上也差不多是这样的逻辑,只不过执行这些指令的不是CPU,而是一个程序,它模拟CPU一条条的执行指令,所以也被称为虚拟机。

Lua的代码可以以文本的形式保存(也就是普通的脚本文件),在加载的时候才预编译为字节码;也可以预先编译为字节码文件(用luac),这样加载进来之后节省了预编译的过程,可以提高加载速度。有了字节码,虚拟机就可以执行指令。

那么指令到底有什么内容呢?说起来似乎也很简单:就是指明要执行什么操作,以及这些操作需要的操作数从哪里获取。

Register-based和Stack-based虚拟机

Lua 4.0之前的虚拟机是Stack-based的执行方式,到5.0之后才改为Register-based

基于栈的虚拟机在执行时,指令只包含操作码,操作数需要从栈中取出,运算完毕后再将结果压回栈,它的指令格式相对更简单,每条指令占用的空间也更小,但是完成相同的运算往往需要更多的指令。

基于寄存器的虚拟机一条指令可以同时包含操作码和数据,因此可以完成的事情更多。操作码指定了要执行的操作,数据则因操作码而异,有可能是一个寄存器地址,也可能是纯粹的运算数值。这里所说的“寄存器”实际上是存在于栈上的元素,而地址是函数栈基址的一个偏移。

下面同样的代码分别翻译成Lua4和Lua5的字节码,从中可以看出两者的差别:

-- Lua 4.0
local a,t,i         1: PUSHNIL 3       -- 向栈压入3个nil,分别代码a, t, i
a=a+i               2: GETLOCAL 0      -- 从栈中取a
                    3: GETLOCAL 2      -- 从栈中取i
                    4: ADD             -- a + i
                    5: SETLOCAL 0      -- 将结果存入a的栈位置
a=a+1               6: GETLOCAL 0      -- 从栈中取a
                    7: ADDI 1          -- a + 1
                    8: SETLOCAL 0      -- 将结果存入a的栈位置
a=t[i]              9: GETLOCAL 1      -- 从栈中取t
                    10: GETINDEXED 2   -- t[i]
                    11: SETLOCAL 0     -- 将结果存入a的栈位置
-- Lua 5.0
local a,t,i  1: LOADNIL 0 2 0    -- R(0)..R(2) := nil,R为寄存器
a=a+i        2: ADD 0 0 2        -- R(0) := RK(0) + RK(2) RK为寄存器或常量,这里是寄存器
a=a+1        3: ADD 0 0 250      -- R(0) := RK(0) + RK(250) RK(250)表示第0个常量,后面描述
a=t[i]       4: GETTABLE 0 1 2   -- R(0) := R(1)[RK(2)]

Lua5.3指令格式

在Lua中一条指令的大小为4个字节,其中最低6位为操作码(OpCode),由此可知Lua最多支持64条指令,不过现在(Lua5.3)只用到47条。操作码后面的内容因指令而异,可能有如下划分:

 OP(6)   A(8)    C(9)     B(9)
|-----|-------|--------|--------|
0                               31


 OP(6)   A(8)        Bx(18)
|-----|-------|-----------------|
0                               31


 OP(6)   A(8)        sBx(18)
|-----|-------|-----------------|
0                               31


 OP(6)         Ax(26)
|-----|-------------------------|
0                               31

总结起来有A, B, C, Bx, sBx, Ax这些部分,其他都是无符号数;sBx表示有符号数,它需要从原始的数值减去一个偏移才能得到真实值,这个偏移就是有符号的最大值,比如sBx的0表示-max,2*max表示max。

那么这些A,B。。。的数值含义到底是什么呢?它们是和OP相关的,不同的OP代表不同的含义,假设A, B。。抽象为X,可以归纳为下面几种:

  • R(X) 表示第X个寄存器,寄存器存在于和函数关联的栈里面:当Lua进入一个函数时,它会从线程的栈中(每个Lua线程有一个栈)预分配足够的空间用于容纳函数参数,本地变量和临时变量,这些我们统称为“寄存器”。第X个寄存器就是以栈帧基址偏移的元素值。关于函数栈帧的内容则是另外的主题了。
  • Kst(X) 表示第X个常量,常量就是函数中的那些字面量,比如function local s = "aa" endaa这个就是一个常量。Lua在编译函数时会生成一个Proto结构,Proto当中就有一个常量数组,而Kst(X)就是该数组中的第X的值。
  • RK(X) 可能是寄存器索引也可能是常量索引,通过测试X的最高位决定。
  • UpValue(X) 表示第X个upvalue,当Lua执行一个函数时(function...end),会创建一个新的闭包对象,对象里面有一个upvalues数组,用于引用其外部的本地变量。UpValue(X)即是从这个数组去取值。
  • KPROTO(X) 表示第几个函数原型,上述闭包对象还有一个指向Proto对象的引用,而Proto还有一个Propto数组,表示函数里面的内嵌函数列表,KPROTO(X)即表示第几个函数原型。

现在来看指令列表应该就好理解多了,不过每条指令还是有很多细节在里面,在文末会给出Lua5.3的字节码参考手册,有兴趣直接跳过去查阅

typedef enum {
/*----------------------------------------------------------------------
name        args    description
------------------------------------------------------------------------*/
// 在寄存器间拷贝值
OP_MOVE,/*  A B R(A) := R(B)                    */

// 加载常量给寄存器
OP_LOADK,/* A Bx    R(A) := Kst(Bx)                 */

// 加载常量到寄存器,常量索引从下一条指令OP_EXTRAARG获取,
// 也就是下一行指令必定是OP_EXTRAARG
OP_LOADKX,/*    A   R(A) := Kst(extra arg)              */

// 设置布尔值B给R(A),如果C为true则跳过下一条指令
OP_LOADBOOL,/*  A B C   R(A) := (Bool)B; if (C) pc++            */

// 加载nil给寄存器
OP_LOADNIL,/*   A B R(A), R(A+1), ..., R(A+B) := nil        */

// 取第B个Upvalue给寄存器
OP_GETUPVAL,/*  A B R(A) := UpValue[B]              */

// 从第B个Upvalue取出表,然后以RK(C)为Key取表的值给寄存器
OP_GETTABUP,/*  A B C   R(A) := UpValue[B][RK(C)]           */

// 从第B个寄存器取出表,然后以RK(C)为Key取表的值给寄存器
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)                */

// 新建一个表,表的数组和哈希初始大小分别是B,C
OP_NEWTABLE,/*  A B C   R(A) := {} (size = B,C)             */

// 这是为了支持面向对象的成员函数调用的写法,如:self:func()
// 首先将R(B)表设置给R(A+1),相当于函数的第1个参数
// 接着从R(B)表取出函数给R(A),这样函数也准备好了。
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)              */

// 连接操作,从R(B)一直连到R(C)
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_JMP配合
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++        */

// 条件测试 R(A)的布尔值如果不等于C,则跳过下一条指令
// 否则执行下一条指令是OP_JMP
OP_TEST,/*  A C if not (R(A) <=> C) then pc++           */

// 条件测试和赋值 R(B)的布尔值如果不等于C,则跳过下一条指令
// 否则R(B)赋值给R(A),然后执行下一条指令是OP_JMP
OP_TESTSET,/*   A B C   if (R(B) <=> C) then R(A) := R(B) else pc++ */

// 函数调用语句,如果B>0,则参数是从R(A+1)往后B-1个。
// 如果B==0,则参数个数是从R(A+1)到栈顶
// 关于该指令的详细信息,请见参考资料
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)  */

// 这两个指令实现数值的for循环
// 关于该指令的详细信息,请见参考资料
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               */

// 这两个指令实现泛型的for循环
// 关于该指令的详细信息,请见参考资料
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;

通过luac可以查看一个脚本文件的字节码,比如下面函数:

local function max(a, b)
    local m = a
    if b > a then
        m = b
    end
    return m
end

反编译字节码得到:

$luac -l test.lua

function <test.lua:2,8> (6 instructions at 0x2138cb0)
2 params, 3 slots, 0 upvalues, 3 locals, 0 constants, 0 functions
        1       [3]     MOVE            2 0 
        2       [4]     LT              0 0 1 
        3       [4]     JMP             0 1     ; to 5
        4       [5]     MOVE            2 1
        5       [7]     RETURN          2 2
        6       [8]     RETURN          0 1

如果有兴趣的可以对着参考手册慢慢研究。但我觉得相对于OpCodes来说,Lua的对象模型,函数执行框架,垃圾回收这些似乎更有价值。这些留待后面分解。

参考资料

  • The Implementation of Lua 5.0
  • Lua 5.3 Bytecode Reference
  • Lua源代码: lopcodes.c|h,lvm.c|h等等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值