LuaJit分析(四)luajit 64位与32位字节码区别

2024-08-30 07:28
文章标签 分析 区别 字节 64 32 luajit

本文主要是介绍LuaJit分析(四)luajit 64位与32位字节码区别,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

对一个lua脚本文件,只有一条语句 print(“hello”, “world”),分别生成字节码文件如下:

32位字节码:

1b4c 4a02 022d 0200 0300 0300 0536 0000 0027 0101 0027 0202 0042 0003 014b 0001

000a 776f 726c 640a 6865 6c6c 6f0a 7072 696e 7400

64位字节码:

1b4c 4a02 0a2d 0200 0400 0300 0536 0000 0027 0201 0027 0302 0042 0003 014b 0001

000a 776f 726c 640a 6865 6c6c 6f0a 7072 696e 7400

 上述红色字体表示的是有区别的地方,第一处是文件头部的flags标志,32位的是02,64位的是0a,有flags字段的定义:

#define BCDUMP_F_BE   0x01
#define BCDUMP_F_STRIP    0x02
#define BCDUMP_F_FFI    0x04
#define BCDUMP_F_FR2    0x08

可知,64位的BCDUMP_F_FR2为1,即可以从这个字段判断是32位还是64位字节码文件

第二处是原型头部的frame大小,32位是3,64位是4,即64位栈帧大小要比32位多1

第三处和第四处是指令的内容,栈帧变大后,同时指令中的索引也同时增大,对32位字节码反汇编内容如下:

0001    GGET     0   0      ; "print"
0002    KSTR     1   1      ; "hello"
0003    KSTR     2   2      ; "world"
0004    CALL     0   1   3
0005    RET0     0   1

64位字节码反汇编内容如下:

0001    GGET     0   0      ; "print"
0002    KSTR     2   1      ; "hello"
0003    KSTR     3   2      ; "world"
0004    CALL     0   1   3
0005    RET0     0   1

KSTR指令用于获取常量的内容,KSTR 2 1即将函数原型中常量索引为1的常量放入到栈中相对BASE偏移为2的内存处。Luajit中解释器的实现在对应的vm_<arch>.dasc文件中,这里以X86为例,即vm_x86.dasc文件,其中对KSTOR解释执行的代码如下:

case BC_KSTR:|  ins_AND    // RA = dst, RD = str const (~)|  mov RD, [KBASE+RD*4]|  mov dword [BASE+RA*8+4], LJ_TSTR|  mov [BASE+RA*8], RD|  ins_next
break;

这里 RA为指令中的目标位置,即相对于即相对于BASE的位置 ,RD为常量的索引,先根据KBASE获取的常量的地址,再保存到RD中,接着把LJ_TSTR标志(字符串类型)和常量地址保存在了RA指定位置的栈中,然后执行下一条指令,可以看出栈中的每一个元素占8字节存储

接着看x64下的BC_KSTR解释:

  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;

与32位下不同的是,将RD设置类型后,直接保存在RA指定位置的栈中,因此32位在KSTR保存时,四字节保存LJ_TSTR,四字节保存RD即常量地址。64位在KSTR保存时,直接保存8字节的RD,即使用settp设置好类型后的地址

那么为什么64位中,参数在栈中的位置要比32位加1?

在处理函数调用时,32位和64位有如下区别:

#if LJ_FR2
static TValue *api_call_base(lua_State *L, int nargs)
{TValue *o = L->top, *base = o - nargs;L->top = o+1;for (; o > base; o--) copyTV(L, o, o-1);setnilV(o);return o+1;
}
#else
#define api_call_base(L, nargs) (L->top - (nargs))
#endif
LUA_API void lua_call(lua_State *L, int nargs, int nresults)
{api_check(L, L->status == LUA_OK || L->status == LUA_ERRERR);api_checknelems(L, nargs+1);lj_vm_call(L, api_call_base(L, nargs), nresults+1);
}

可以看到,如果是64位,则把当前函数栈中的所有参数往后移动一位,并把多出来的一位置为nil。因此可以解释为什么64位字节码中参数栈位置要加1。

同时x86的 CALl指令解释如下:

case BC_CALL: case BC_CALLM:|  ins_A_C     // RA = base, (RB = nresults+1,) RC = nargs+1 | extra_nargsif (op == BC_CALLM) {|  add NARGS:RD, MULTRES}|  cmp dword [BASE+RA*8+4], LJ_TFUNC|  mov LFUNC:RB, [BASE+RA*8]|  jne ->vmeta_call_ra|  lea BASE, [BASE+RA*8+8]|  ins_callbreak;

它将BASE的位置移到了RA指定值得后一个位置,即第一个参数的位置,最后调用的ins_CALL如下:

|.macro ins_call
|  // BASE = new base, RB = LFUNC, RD = nargs+1
|  mov [BASE-4], PC
|  ins_callt
|.endmacro
|
|.macro ins_callt
|  // BASE = new base, RB = LFUNC, RD = nargs+1, [BASE-4] = PC
|  mov PC, LFUNC:RB->pc
|  mov RA, [PC]
|  movzx OP, RAL
|  movzx RA, RAH
|  add PC, 4
|.if X64
|  jmp aword [DISPATCH+OP*8]
|.else
|  jmp aword [DISPATCH+OP*4]
|.endif
|.endmacro

32位CALL调用前栈结构:

print | TFUNC

"hello" | TSTR

"world" | TSTR

BASE012TOP

 32位CALL指令执行调整栈结构:

print | PC

"hello" | TSTR

"world" | TSTR

0BASE2TOP

 它将PC复制到了BASE-4的位置(预调用函数变量的后四字节),变成当前PC的值(理解为保存了返回地址),接着在callt块中执行取指令,获取opcode,然后跳转执行该函数。

X64的 CALl指令解释如下:

  case BC_CALL: case BC_CALLM:|  ins_A_C     // RA = base, (RB = nresults+1,) RC = nargs+1 | extra_nargsif (op == BC_CALLM) {|  add NARGS:RDd, MULTRES}|  mov LFUNC:RB, [BASE+RA*8]|  checkfunc LFUNC:RB, ->vmeta_call_ra|  lea BASE, [BASE+RA*8+16]|  ins_call
break;

它也将BASE移到了第一个参数的位置,与32位不同的是,它多移动了一个位置,因为64位多出了一个nil位置。RB保存的是预调用的函数。64位的ins_call解释如下:

|.macro ins_call
|  // BASE = new base, RB = LFUNC, RD = nargs+1
|  mov [BASE-8], PC
|  ins_callt
|.endmacro
|
|.macro ins_callt
|  // BASE = new base, RB = LFUNC, RD = nargs+1, [BASE-8] = PC
|  mov PC, LFUNC:RB->pc
|  mov RAd, [PC]
|  movzx OP, RAL
|  movzx RAd, RAH
|  add PC, 4
|  jmp aword [DISPATCH+OP*8]
|.endmacro

它也将PC(返回地址)保存在了BASE前一个位置,与32位不同的是,此时的PC表示的地址为64位长度,它会占满一个单元的位置,即刚好填充nil的8字节。接下来ins_callt开始执行下一个函数。

64位CALL调用前栈结构:

print | TFUNC

nil

"hello" | TSTR

"world" | TSTR

BASE0123TOP

64位CALL指令执行调整栈结构:

print | TFUNC

PC

"hello" | TSTR

"world" | TSTR

01BASE3TOP

 总结:64位字节码中,栈中操作参数的索引比32位加1,是因为在执行CALL指令时,栈中要保存当前PC的值,在32位中,PC的值占四字节,可以直接保存在函数变量的后四字节,而64位中,PC值占8字节,因此开辟一个单元的栈空间,把参数全部往后移动一个单元。

前面有提到64位在保存一个常量时,直接RD保存8字节内容,32位是分两个4字节分别保存值和类型。同时对8字节的RD使用settp用于设置类型,settp定义如下:

|.macro settp, dst, reg, tp
|  mov64 dst, ((uint64_t)tp<<47)
|  or dst, reg
|.endmacro

它将低17位左移到高位,然后与RD取或操作,也就是,在高17位设置了RD的类型

原因如下:

Luajit统一使用64位表示变量,但是32位和64位中变量表示的方法不一样:

1) 表示方法背景:

浮点数类型的编码格式普遍使用IEEE754标准,它的编码包括符号、指数、尾数。其中双精度类型的浮点数double采用64位表示,最高位为符号,后11位为指数,低52位为尾数。

IEEE754标准中,如果指数部分全部为0,尾数部分不全为0时,表示NaN,即表示不是一个数。而尾数部分有52个,只要其中一个为1,那么剩余51位就可以表示其它的类型。如字符串、函数、表等。

2) luajit64位类型表示:

在64位系统中,64位理论可表示的地址空间为16,777,216T,而64位的CPU一般使用48位表示地址,即最大为256T,如AMD要求从第48到63的这16位需要与第47位相同。即地址必须在0到00007FFF'FFFFFFFF 和 FFFF8000'00000000 到 FFFFFFFF'FFFFFFFF这两个范围内,共有256TB的虚拟地址空间。操作系统继续将内存空间分为内核部分和用户层部分,如Linux使用高128T为内核空间,低128T为用户空间。

在luajit64位中,这51位分成了两个部分,其中低47位表示地址,可以表示的最大值为128T,高4位表示类型,因此合并后可以看成高17位表示类型,低47位表示实际的地址,这就对应了x64中使用settp设置类型,luajit64类型定义如下:

#define LJ_TNIL     (~0u)
#define LJ_TFALSE   (~1u)
#define LJ_TTRUE    (~2u)
#define LJ_TLIGHTUD   (~3u)
#define LJ_TSTR     (~4u)
#define LJ_TUPVAL   (~5u)
#define LJ_TTHREAD    (~6u)
#define LJ_TPROTO   (~7u)
#define LJ_TFUNC    (~8u)
#define LJ_TTRACE   (~9u)
#define LJ_TCDATA   (~10u)
#define LJ_TTAB     (~11u)
#define LJ_TUDATA   (~12u)
/* This is just the canonical number type used in some places. */
#define LJ_TNUMX    (~13u)

当高16位全是1时,即这里4位的值为14,源码中说是lightuserdata类型,可以认为是一个自定义的指针吧

3) luajit32位类型表示:

1)     高16位不全为1,表示一个double型数据

2)     高16位全为1,第47位为0,表示一个指针

3)     其余情况,高32位表示类型,低32位表示实际值

这里就对应了32位和64位对应解释器汇编中对类型的操作方式

总结:

luajit 64位和32位字节码不一样,体现:

1、文件头部的flags表示,64位中有标记 fr2 = 1

2、原型头中的栈帧大小,当原型中有call指令并有参数时,frame大小会比32位的加1

3、参数压入的位置,当存在call指令并且有KSTR等指令压入参数时,压入的位置会加1

4、原因是CALL调用时需要保存返回地址,在32位中,地址占4字节,直接覆盖了栈中压入的函数字段类型的类型部分(4字节),而64位中地址占8字节,因此将栈的大小增加了1,并移动所有参数。

这篇关于LuaJit分析(四)luajit 64位与32位字节码区别的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1120165

相关文章

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是

Python中@classmethod和@staticmethod的区别

《Python中@classmethod和@staticmethod的区别》本文主要介绍了Python中@classmethod和@staticmethod的区别,文中通过示例代码介绍的非常详细,对大... 目录1.@classmethod2.@staticmethod3.例子1.@classmethod

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Golan中 new() 、 make() 和简短声明符的区别和使用

《Golan中new()、make()和简短声明符的区别和使用》Go语言中的new()、make()和简短声明符的区别和使用,new()用于分配内存并返回指针,make()用于初始化切片、映射... 详细介绍golang的new() 、 make() 和简短声明符的区别和使用。文章目录 `new()`

Python中json文件和jsonl文件的区别小结

《Python中json文件和jsonl文件的区别小结》本文主要介绍了JSON和JSONL两种文件格式的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下... 众所周知,jsON 文件是使用php JSON(JavaScripythonpt Object No

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专