一. 简介
脚本语言,即动态语言,其相较于静态语言如C/C++/GO等,一大区别即不需要经过计算机的预处理、编译、汇编、链接,而是由虚拟机代为完成上述过程。虚拟机模拟真实的步骤,首先将脚本语言转化为特定的opcode(各脚本语言自行定义),接着放在虚拟机中逐个执行,模拟了CPU及内存的基本功能。Lua作为一门嵌入式语言,其虚拟机附着于宿主环境中而非单独存在,其核心问题主要包括:
- 如何分析源代码文件生成Opcode
- 如何执行Opcode指令
- 如何保存整个执行环境
本文将介绍主要流程和思路分析,对于详细细节如语法、词法分析,闭包加载等由于篇幅问题后续文章详谈,本文只是给出一个初步的整体脉络。
二. opcode的生成
从源码生成opcode,需要以下步骤:
- 初始化Lua虚拟机数据结构
- 读取Lua脚本文件内容
- 依次对Lua脚本文件进行词法分析、语法分析、语义分析,最后生成该文件的Lua虚拟机指令。注意以上的过程仅需要一次遍历,这是Lua解释器做的非常好的地方
Lua脚本的加载、编译和执行通过函数luaL_doFile()
进行,该函数实际是一个宏定义,如下所示:
1 | // lauxlib.h |
主要做了两件事:
- 调用
luaL_loadfile(L, fn)
生成opcode - 调用
lua_pcall()
执行
luaL_loadfile()
最终调用f_parser()
函数,对Lua
代码进行语法语义分析。
1 | // ldo.c |
实际解析根据文件类型选择通过luaU_undump()或者luaY_parser()
函数执行,保存在Closure
中并压入栈,进入后续的opcode执行过程。
三. opcode的执行
lua_pcall()
对栈中的opcode
进行实际的执行。
1 | (lapi.c) |
该函数主要流程为
- 获取需要调用的函数指针
c.func
- 如果不是连续调用则调用
luaD_pcall()
执行代码,否则调用luaD_call()
1 | // ldo.c |
实际需要关注的是luaD_call()
函数,其主要调用luaD_precall()
进行预处理,然后调用luaV_execute()
执行。
1 | /* |
luaD_precall()
函数主要逻辑在注释中已经说明的很清晰了。对于C函数直接调用,后续通过luaD_poscall()
返回上一层级,对于lua函数则使用luaV_execute()
进行实际调用工作。其中关键操作包括:
- 调用
next_ci()
从lua_State
的CallInfo
数组中得到一个新的CallInfo
结构体,设置它的func/base/top
指针 - 调用
setnilvalue()
给多余参数赋值为nil - 将
lua_State
的top/base
指针赋值为CallInfo
的值,供给luaV_execute()
执行
1 | /* |
luaV_execute()
中根据实际的字节码类型进行实际的操作,取的是在luaD_precall()
中压入的函数。
1 | void luaV_execute(lua_State *L) |
参考文献
《A No Frills Introduction to Lua 5.1 VM Instructions》
《The Implementation of Lua 5.0》