Categories
程式開發

Luajit字节码分析之KSTR


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。

ins_AND宏定义

|.macro ins_AND; not RD; .endmacro

ins_AND宏的实现很简单,就是对RD取反,取反后RD=-1。

关于GCProto的内存布局

// @file:lj_obj.h

typedef struct GCproto {
GCHeader;
...
MRef k;
...
} GCproto;

我们这里只列出了与KSTR指令相关的数据。MRef k指向的地址是存放全局常量的基地址。GCProto的内存布局如下:

Luajit字节码分析之KSTR 1

那么在上面的汇编代码里面,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)的第一个位置上。