luajit的解析器是通过汇编代码实现的,代码晦涩难懂,但是我还是想尝试对一些OPCode进行解析,比如下面的lua代码:
-- file:code.lua
local s1 = "123"
local s2 = "1234"
通过下面的指令可以获得对应的opcode:
> luajit -bl code.lua # code.lua 即为上面lua代码的文件名
KGC 0 "123"
KGC 1 "1234"
0001 KSTR 0 0 ; "123"
0002 KSTR 1 1 ; "1234"
0003 RET0 0 1
那么KSTR这个OPCode在Luajit里面的实现是怎样的呢?可以在vm_x64.dasc文件里面找到:
case BC_KSTR:
| ins_AND // RA = dst, RD = str const (~)
| mov RD, [KBASE+RD*8]
| settp RD, LJ_TSTR
| mov [BASE+RA*8], RD
| ins_next
break;
这几行汇编代码都分别代表什么意思,我们一个一个的来解析。
首先假设我们在执行第一个指令 :
KSTR 0 0 ; "123"
那么RA=0,RD=0。
|.macro ins_AND; not RD; .endmacro
ins_AND宏的实现很简单,就是对RD取反,取反后RD=-1。
// @file:lj_obj.h
typedef struct GCproto {
GCHeader;
...
MRef k;
...
} GCproto;
我们这里只列出了与KSTR指令相关的数据。MRef k指向的地址是存放全局常量的基地址。GCProto的内存布局如下:
那么在上面的汇编代码里面,KBASE就是global consts内存区域的高位地址。为何KBASE即为global consts的高位地址呢?
case BC_IFUNCV:
...
} else {
| mov KBASE, [PC-4+PC2PROTO(k)]
| ins_next
}
...
在执行BC_KSTR之前会先执行BC_IFUNCV,其中会计算KBASE的值,他的计算方法是[PC-4+PC2PROTO(k)]。PC2PROTO宏又是什么?
#define PC2PROTO(field) ((int)offsetof(GCproto, field)-(int)sizeof(GCproto))
不难看出PC2PROTO(k)是计算GCproto->k到GCproto结构体尾部的字节长度,然后取负。PC寄存器存储的是当前OPCode运行的地址的后4字节,BC_IFUNCV又是第一个运行的OPCode,因此PC-4代表的意义就是GCProto结构体底部的绝对地址。因此可以想到PC-4+PC2PROTO(k)即为GCproto->k所在的地址。因此:
KBASE = GCproto->k->ptr64
我们回来再看下面这条汇编指令:
| mov RD, [KBASE+RD*8]
因此,[KBASE+RD*8]计算的就是第一个常量字符串"123"的地址,即:
RD = GCproto->k->ptr64 - 8
然后进行第二条汇编指令:
|.macro settp, reg, tp
| mov64 ITYPE, ((uint64_t)tp<<47)
| or reg, ITYPE
|.endmacro
|settp RD, LJ_TSTR
我个人感觉这种设计很巧妙。x64的线性地址48-63位是保留的,因此luajit利用这几位做类型信息的保存,settp是将LJ_TSTR左移47位,然后与RD做或运算。
最后一个汇编指令:
| mov [BASE+RA*8], RD
有了上面的基础,这个指令比较好理解了,它将RD的数据放到栈(BASE)的第一个位置上。
领取专属 10元无门槛券
私享最新 技术干货