豆瓣读书:https://book.douban.com/subject/30348061/
    image.png


    最近看了一本《自己动手构造编译系统:编译、汇编与链接》感觉收获还是挺大的,
    就顺手搜了一下,看看还有没有类似的书籍可以读一下,就找到了这本书。

    之前读过一本《深入 Java 虚拟机》算是对于我有关 “虚拟机” 技术的启蒙读物了。
    虽然学的时候一知半解,但自此埋下了好奇的种子。

    现在看的这本书《自己动手实现 Lua》看起来更接地气一些,
    作者带领我们用 go 语言从零实现了 Lua 虚拟机(chunk 的解释器),以及编译器和标准库。

    • 准备
    • Lua 虚拟机 和 Lua API
    • Lua 语法和编译器
    • Lua 标准库

    如果对虚拟机技术比较感兴趣的话,可以重点读第二部分,
    作者解释了二进制 chunk 的构成,虚拟机所支持的指令集,然后一点点把整个虚拟机实现,
    支持了以下这些语言特性:算术运算,表操作,函数调用(闭包),FFI 调 Go,元编程,异常处理。

    在读这本书之前,我留意了几个重要的点:

    • Lua 从 1.0 版本开始就内置了 虚拟机
    • Lua 二进制 chunk 的格式(包括 虚拟机指令)并没有标准化,属于官方内部实现细节
    • Lua 5.0 之前用的是基于栈的虚拟机(栈机),从 5.0 开始改成基于寄存器的虚拟机(本书介绍)
    • JVM 用的是变长指令集,而 Lua 用的是定长指令集(4字节[32 bit])(6bit 操作码/ 26bit 操作数)
    • Lua 5.3 总共包含了 47 条指令
    • MOVE 指令支持的寄存器数最多为 255 个,所以 Lua 编译器会把函数局部变量限制在 200 个以内

    我来重点介绍一下作者在 第二部分(Lua 虚拟机 和 Lua API)的行文思路。

    虽然是基于寄存器的虚拟机,但是指令的计算过程,却跟栈机的原理很相似,
    即,在计算之前先把操作数压栈,计算完后弹栈,再把结果压栈。
    (用寄存器机作为底层实现,是因为这样可以减少指令集,但会增加指令长度)

    这个 “栈” 其实就是 Lua 虚拟机最基本的组成部分了,它里面可以加入基本的数据类型,还可以加入表,
    甚至函数调用帧也是基于 “栈” 这个数据结构来实现的。

    作者从 Lua 栈开始,不断给它增加 API,最后形成的就是 Lua API。
    每个 Lua 解释器有一个 lua_State 实例,Lua API 体现为一系列操作 lua_State 结构体的函数。

    • Lua 栈(luaStack)(虚拟栈)
      • 【字段:slots, top】
      • 【方法:check, push, pop, get, set …】
    • Lua State(封装了整个 Lua 解释器状态)
      • 【字段:stack】
      • 【方法:GetTop, CheckStack, Pop, Copy, PushValue, Replace, Insert, Remove, SetTop, TypeName …】
    • 扩展 Lua State 接口(算术运算)
      • 【方法:Arith, Compare, Len, Concat】
    • LuaVM(Lua API 是对用户提供的,Lua VM 是为了内部实现,要扩展 Lua State 又不影响 Lua API)
      • 用 Lua State 模拟寄存器
      • 【字段:LuaState】
      • 【方法:PC, AddPC, Fetch, GetConst, GetRK】
    • 扩展 Lua State(表相关的操作)
      • luaTable【字段:arr, _map】,并且 Lua 栈(寄存器)中可以直接保存一个 luaTable
      • 关于表的操作:从寄存器中拿到表, 拿到索引,进行操作,将结果压栈
      • 【方法:NewTable, CreateTable, GetTable, SetField, GetField】

    以上就是一个简单的寄存器虚拟机的实现了,可以实现多数寄存器指令的操作。

    但为了实现函数调用指令(以及 FFI,元编程 等语言特性),还需要进行以下扩展:

    • 扩展 Lua 栈(luaStack)
      • 【字段:prev, closure, varargs, pc】
    • 让 luaState 充当函数调用栈
      • 【方法:pushLuaStack, popLuaStack】
      • 【方法:Load, Call】
        • Load:加载二进制 chunk,把主函数原型实例化为闭包推入栈顶
        • Call:把被调函数推入栈顶,参数入栈,函数执行完后弹栈,把返回值压栈

    如此这般,后面 FFI 部分(Lua 调 Go),闭包,元编程,异常处理,就更容易理解了。


    总之,本书是一本很值得读的参考书,读起来能对虚拟机的实现细节有直观的认识,
    作者在介绍的时候也很细致,美中不足的是字里行间代码片段比较多,而且没有整体的认识。
    如果每个章节能有一个概念的整体关系图就好了。

    茶余饭后值得阅读消遣一下。