lua vm 二: 查看字节码、看懂字节码

本文讲一讲如何查看 lua 的字节码(bytecode),以及如何看懂字节码。

以下分析基于 lua-5.4.6,下载地址:https://lua.org/ftp/


1. 查看字节码

1.1 方法一:使用 luac

luac 是 lua 自带的编译程序,用法是:luac -l -p <文件名> ,如果要完整展示,则用 -l -l,比如 luac -l -l -p <文件名>

有 lua 脚本 a.lua:

lua 复制代码
print("hello, world")

luac -l -l -p a.lua 生成出来是这样的:

main <a.lua:0,0> (5 instructions at 0x55a732d64cc0)
0+ params, 2 slots, 1 upvalue, 0 locals, 2 constants, 0 functions
        1       [1]     VARARGPREP      0
        2       [1]     GETTABUP        0 0 0   ; _ENV "print"
        3       [1]     LOADK           1 1     ; "hello, world"
        4       [1]     CALL            0 2 1   ; 1 in 0 out
        5       [1]     RETURN          0 1 1   ; 0 out
constants (2) for 0x55a732d64cc0:
        0       S       "print"
        1       S       "hello, world"
locals (0) for 0x55a732d64cc0:
upvalues (1) for 0x55a732d64cc0:
        0       _ENV    1       0

如果是一份由 luac 编译生成出来的字节码文件,则直接 luac -l <文件名>luac -l -l <文件名> 就行了。比如这样:

bash 复制代码
luac -o a.lua.out a.lua

luac -l -l a.lua.out

1.2 方法二:使用在线工具:luac.nl - Lua Bytecode Explorer

Lua Bytecode Explorer 的完整网址是: https://www.luac.nl/

它支持从 4.0 到 5.4.6 的所有版本(截至 2024.4)。它还支持生成代码分享链接,在页面底下有 Generate Link 按钮,比如我写的 hello world 脚本: https://luac.nl/s/f79aff49f5fd7746b4e3ae2a65

大部分情况下,用这个在线网站是最方便的,推荐使用。

效果是这样的:


图1:lua bytecode explorer 生成字节码


2. 看懂字节码

2.1 指令集的说明文档

方法一:lua 源码

最直接的方式是看源码,所有指令的定义在这个代码文件:lopcodes.h ,不同指令的具体实现在 lvm.c 的 luaV_execute 函数里。

方法二:一些文档或文章

lua5.2 : http://files.catwell.info/misc/mirror/lua-5.2-bytecode-vm-dirk-laurie/lua52vm.html

lua5.3 : https://the-ravi-programming-language.readthedocs.io/en/latest/lua_bytecode_reference.html

lua5.4 : https://zhuanlan.zhihu.com/p/277452733


2.2 指令的基本格式

lua5.4 指令的基本信息:

  • 使用一个 32 位的无符整数来表示一个指令,包含两大部分:操作码和操作数

  • 其中前 7 位用于表示操作码,其余的是操作数,操作数要根据操作码使用不同的编码格式去解析

  • 有 5 种格式编码格式:iABC, iABx, iAsBx, iAx, isJ,每个指令只属于其中之一

下图取自 lua 源码中的 lopcode.h,它精确的标明了不同编码格式用到了哪些 bit 位。


图2:lua5.4 opcode 格式


2.3 指令编码格式

有 5 种指令编码格式,比如 iABC,i 表示 instruction,ABC 分别是三个操作数。

举个具体的例子说明一下吧,对于这样一个脚本:

lua 复制代码
local function my_add(x, y)
	local z = x + y
    return z
end

用 lua bytecode explorer 生成的,代码片段的 link 是 https://luac.nl/s/fac2949499991b83b0e3ae2a65 ,编译结果如下图:


图3:简单加法例子

local z = x + y 这个语句被编译成这两句:

1	ADD	2 0 1	
2	MMBIN	0 1 6	; __add

MMBIN 这句可以不用管,是元表相关的,这里不会用到,只关注 ADD 这一句就好了。

加法指令 OP_ADD 就是 iABC 格式的:

c 复制代码
OP_ADD,/*	A B C	R[A] := R[B] + R[C]				*/

ADD 2 0 1 中 A 对应 2,B 对应 0,C 对应 1。R[0] 存的是形参 x 的值,R[1] 存的是形参 y 的值,R[2] 存的是局部变量 z 的值。

所以 local z = x + y 就翻译成 R[2] = R[0] + R[1]

看到这里可能会有点晕,R[0] 是什么东西?这就是 lua 的 "寄存器",但它是虚拟出来的,代表的实际上是 lua 数据栈的一个位置,而 lua 数据栈是一个数组结构,每个函数都会在这个数组上占据一段空间。

可以理解为,每个函数都有一个自己的栈,它是一个数组。数组的最前面放的是参数,之后放的是局部变量。在我们的例子中,数据是这样放的:


图4:简单加法的数据结构

实际上,编译结果的 locals 项已经清楚表明了各个变量在 lua 数据栈中的位置了,index 列就表示在数据栈数组中的索引。


图5:locals 信息


2.4 为什么一个函数生成了两个 return 的 opcode

上图中,可以看到 my_add 函数,我们只写了一个 return 语句,但却有两个 return opcdoe,而 main 函数,我们没有 return,也有一个 return opcdoe。下面具体解释一下。

my_add 的两个分别对应 OP_RETURN1 和 OP_RETURN0。第一个 return1 对应的是 return c 这个语句。第二个 return0 是 lua 编译器自动生成的,每个函数的末尾都会补充一个 "final return"。

具体源码可以在 lparser.c 找到,里面分别处理 main 函数和普通函数,lua 会把一个 script 脚本处理成一个函数,就叫 main 函数,如果上图中的 function main

我们显式写的 return 语句,是在 lparser.c 的 retstat 函数处理的,它内部调用 luaK_ret 生成一个 return 的 opcode。

main 函数,是在 lparser.c 的 mainfunc 函数处理的,末尾调用 close_func 处理收尾工作,close_func 内部会调用 luaK_ret 生成一个 return 的 opcode。

普通函数,是在 lparser.c 的 body 函数处理的,末尾调用 close_func 处理收尾工作,close_func 内部会调用 luaK_ret 生成一个 return 的 opcode。


正文完。

相关推荐
hutaotaotao1 小时前
c语言用户不同命令调用不同函数实现
c语言·开发语言
lb36363636361 小时前
介绍一下位操作符(c基础)
c语言·知识点
vortex53 小时前
解决 VSCode 中 C/C++ 编码乱码问题的两种方法
c语言·c++·vscode
Xiao Fei Xiangζั͡ޓއއ4 小时前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法
子朔不言4 小时前
[ARM-2D 专题]6.脏矩形定义的宏使用技巧和分析
c语言·arm开发·arm2d·显控开发-新龙微
BT-BOX5 小时前
STM32仿真proteus位带操作和keil增加头文件C文件
c语言·stm32·proteus
醉颜凉5 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
五味香8 小时前
Linux学习,ip 命令
linux·服务器·c语言·开发语言·git·学习·tcp/ip
编程武士8 小时前
nginx openresty lua-resty-http 使用的一些问题记录
nginx·lua·openresty·lua-resty-http
lb36363636369 小时前
整数储存形式(c基础)
c语言·开发语言