Lua 代码是如何跑起来的
上一篇「C 代码是如何跑起来的」中,我们了解了 C 语言这种高级语言是怎么运行起来的。
C 语言虽然也是高级语言,但是毕竟是很 “古老” 的语言了(快 50 岁了)。相比较而言,C 语言的抽象层次并不算高,从 C 语言的表达能力里,还是可以体会到硬件的影子。
旁白:通常而言,抽象层次越高,意味着程序员的在编写代码的时候,心智负担就越小。
今天我们来看下 Lua 这门相对小众的语言,是如何跑起来的。
解释型
不同于 C 代码,编译器将其直接编译为物理 CPU 可以执行的机器指令,CPU 执行这些机器执行就行。
Lua 代码则需要分为两个阶段:
- 先编译为字节码
- Lua 虚拟机解释执行这些字节码
旁白:虽然我们也可以直接把 Lua 源码作为输入,直接得到执行输出结果,但是实际上内部还是会分别执行这两个阶段
字节码 在「CPU 提供了什么」 中,我们介绍了物理 CPU 的两大基础能力:提供一系列寄存器,能执行约定的指令集。
那么类似的,Lua 虚拟机,也同样提供这两大基础能力:
- 虚拟寄存器
- 执行字节码
旁白:Lua 寄存器式虚拟机,会提供虚拟的寄存器,市面上更多的虚拟机是栈式的,没有提供虚拟寄存器,但是会对应的操作数栈。
我们来用如下一段 Lua 代码(是的,逻辑跟上一篇中的 C 代码一样),看看对应的字节码。用 Lua 5.1.5 中的
luac
编译可以得到如下结果:$ ./luac -l simple.luamain(12 instructions, 48 bytes at 0x56150cb5a860)
0+ params, 7 slots, 0 upvalues, 4 locals, 4 constants, 1 function
1[4]CLOSURE0 0;
0x56150cb5aac0
2[6]LOADK1 -1;
1# 将常量区中 -1 位置的值(1) 加载到寄存器 1 中
3[7]LOADK2 -2;
2# 将常量区中 -2 位置的值(2) 加载到寄存器 1 中
4[8]MOVE3 0# 将寄存器 0 的值,挪到寄存器 3
5[8]MOVE4 1
6[8]MOVE5 2
7[8]CALL3 3 2# 调用寄存器 3 的函数,寄存器 4,和寄存器 5 作为两个函数参数,返回值放入寄存器 3 中
8[10]GETGLOBAL4 -3;
print
9[10]LOADK5 -4;
"a + b = "
10[10]MOVE6 3
11[10]CALL4 3 1
12[10]RETURN0 1function(3 instructions, 12 bytes at 0x56150cb5aac0)
2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions
1[3]ADD2 0 1# 将寄存器 0 和 寄存器 1 的数相加,结果放入寄存器 2 中
2[3]RETURN2 2# 将寄存器 2 中的值,作为返回值
3[4]RETURN0 1
稍微解释一下:
- 不像 CPU 提供的物理集群器,有不同的名字,字节码的虚拟寄存器,是没有名字的,只有数字编号。逻辑上而言,每个函数有独立的寄存器,都是从序号
0
开始的(实际上会有部分的重叠复用) - Lua 字节码,也提供了定义函数,执行函数的能力
- 以上的输出结果是方便人类阅读的格式,实际上字节码是以非常紧凑的二进制来编码的(每个字节码,定长 32 比特)
Lua 虚拟机 Lua 虚拟机是一个由 C 语言实现的程序,输入是 Lua 字节码,输出是执行这些字节码的结果。
对于字节码中的一些抽象,则是在 Lua 虚拟机中来具体实现的,比如:
- 虚拟寄存器
- Lua 变量,比如
table
等
具体来说:
因为 Lua 变量,在 Lua 虚拟机内部,都是通过
TValue
结构体来存储的,所以实际上虚拟寄存器,就是一个 TValue
数组。例如下面的
MOVE
指令:MOVE 3 0
实际上是完成一个
TValue
的赋值,这是 Lua 5.1.5 中对应的 C 代码:#define setobj(L,obj1,obj2) \
{ const TValue *o2=(obj2);
TValue *o1=(obj1);
\
o1->value = https://www.it610.com/article/o2->value;
o1->tt=o2->tt;
\
checkliveness(G(L),o1);
}
其对应的关键机器指令如下:(主要是通过
mov
机器指令来完成内存的读写)movrax,QWORD PTR [rsi]
movQWORD PTR [r9+0x10],rax
moveax,DWORD PTR [rsi+0x8]
movDWORD PTR [r9+0x18],eax
执行 Lua 虚拟机的实现中,有这样一个
for (;
;
)
无限循环(在 luaV_execute
函数中)。其核心工作跟物理 CPU 类似,读取
pc
地址的字节码(同时 pc
地址 +1
),解析操作指令,然后根据操作指令,以及对应的操作数,执行字节码。例如上面我们解释过的
MOVE
字节码指令,也就是在这个循环中执行的。其他的字节码指令,也是类似的套路来完成执行的。pc
指针也只是一个 Lua 虚拟机位置的内存地址,并不是物理 CPU 中的 pc
寄存器。函数 几个基本点:
- Lua 函数,可以简单的理解为一堆字节码的集合。
- Lua 虚拟机里,也有栈帧的,每个栈帧实际就是一个 C struct 描述的内存结构体。
总结 Lua 这种带虚拟机的语言,逻辑上跟物理 CPU 是很类似的。生成字节码,然后由虚拟机来具体执行字节码。
只是多了一层抽象虚拟,字节码解释执行的效率,是比不过机器指令的。
物理内存的读写速度,比物理寄存器要慢几倍甚至几百倍(取决于是否命中 CPU cache)。
所以 Lua 的虚拟寄存器读写,也是比真实寄存器读写要慢很多的。
不过在 Lua 语言的另一个实现 LuaJIT 中,这种抽象还是有很大机会来优化的,核心思路跟我们之前在 「C 代码是如何跑起来的」 中看到的
gcc
的编译优化一样,尽量多的使用寄存器,减少物理内存的读写。【Lua 代码是如何跑起来的】关于 LuaJIT 确实有很多很牛的地方,以后我们再分享。
推荐阅读
- 热闹中的孤独
- 我要做大厨
- CVE-2020-16898|CVE-2020-16898 TCP/IP远程代码执行漏洞
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 爱就是希望你好好活着
- 太平之莲
- 知识
- 叙述作文
- 时间老了
- 清明,是追思、是传承、是感恩。