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的内存布局如下:

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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/b059976e8b02111dbae64b445
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券