[toc]
# 入口
入口 : luaL_dofile () doFile 中会对读入的代码进行检查,但是最后会走到 f_parser
函数,其原型如下:
# f_parser
1 | static void f_parser(lua_State* L, void* ud) { |
在 f_parser
中,通过 luaY_parser
函数指针进行实际的语法分析,并在最后输出词法 / 语义分析后的结果 Proto
,并将这个结果保存到比包中,然后申请闭包中的 Upvalue
,最后将闭包压栈,同时栈顶指针自增 在 doFile 后 如果成功会返回0
会进入 lua_pcall
# lua_pcall
其中 lua_pcall
的函数原型如下:
1 | LUA_API int lua_pcall(lua_State* L, int nargs, int nresults, int errfunc) { |
# 函数调用
在 lua_pcall
中,先检查当前执行到的闭包是否是 Lua函数
,入口是 luaD_precall
1 | //进行lua闭包函数执行前准备 |
大体的流程是:
1. 判断是否是一个 C 函数,是的话直接调用 C 函数,否则走 Lua 闭包逻辑
2. 获取 Lua 状态机中的一个 CallInfo 2.1. 设置闭包指针 2.2. 设置调用函数参数起始与终点地址
2.3. 设置生成的 Proto 中的字节码到状态机上
2.4. 如果函数的参数不足声明的部分,不足的部分填充 nil 2.5. 返回 PCRLUA ,表示是一个 Lua 闭包
3. 进入 luaV_execute <lvm.c>
函数,这个函数内部会执行词法分析后得到的字节码,然后塞入一个巨大的状态机中进行执行,每个 Op 就是一个状态节点,case 内部的内容就是 Action
4. 进入尾状态 OP_RETURN <lvm.c>
,这是状态图的尾状态,在这个状态内会调用 luaD_poscall <ldo.c>
,这个函数是状态机的尾 Action,主要负责回复上一个调用栈状态,以及将结果填充到上一个调用栈中。 简单的外流程如图
# 函数调用栈流程
第一个函数调用栈的示意图如上,始终遵循 函数 -> 参数表的结构进行 第二个函数就是在第一个函数调用中插入命令,实际上想要恢复调用前状态,只需要恢复 base_ci 指向的对象和栈顶指针即可 这里比较感兴趣的是返回值存放到栈的那部分 (应该是调用前的栈顶指针后压栈)
# OpCode 解析流程
OpCode 格式 其中所有 OpCode 的指令封装在 lopcode.c
文件下:
1 | //封装了每个OpCode的格式 |
可以发现 opmode 直接将指令封装到了一个 lu_byte
变量中
其中最后一个 m
就是 opCode 解 Op 指令的函数丢在了两个文件里, lvm.c
和 lopcodes.h
里。 在上面的 函数调用部分
,我们知道 Lua 的函数解析最后会走到 luaV_execute
函数中,而所有的指令也是在这个函数下执行的, 同时最外层的指令解析也是在这个函数中定义和进行的
1 | //以参数i为偏移量,在函数栈内取数据,GETARG_A(i)是从opCode中取出A参数 |
比如其中的 RA(i)
,传入的 i
就是指令封装的 lua_byte
,这个解指令的函数以是根据指令中定位到 A 参数的栈中位置,其他的就暂时不说了,大概就是 最外层
针对指令的操作 基本就是用来 确定参数在栈中或者常量区的位置
另外,上面的 K
通常代表 常量
另外一层的解指令是在 lopcodes.h
文件中,这里是实际的解引用, lvm.h
里的是最外层的接口,实际的解引用实现放在了 lopcodes.h
中
比如:
定义每个参数在指令中的 bit 位置:
1 | /* |
快速引用到某个参数的 BitMask
1 |
获取参数:
1 |
比如 GETARG_A(i)
就是获取指令中的 A 参数
# opCode 执行流程
- 先保存当前
pc
的执行位置 - cl: 当前的函数环境
- base: 当前函数环境的 base 地址
- k: 当前函数环境的常量数组