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

相关文章

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

native和static native区别

本文基于Hello JNI  如有疑惑,请看之前几篇文章。 native 与 static native java中 public native String helloJni();public native static String helloJniStatic();1212 JNI中 JNIEXPORT jstring JNICALL Java_com_test_g

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

【软考】希尔排序算法分析

目录 1. c代码2. 运行截图3. 运行解析 1. c代码 #include <stdio.h>#include <stdlib.h> void shellSort(int data[], int n){// 划分的数组,例如8个数则为[4, 2, 1]int *delta;int k;// i控制delta的轮次int i;// 临时变量,换值int temp;in

Android fill_parent、match_parent、wrap_content三者的作用及区别

这三个属性都是用来适应视图的水平或者垂直大小,以视图的内容或尺寸为基础的布局,比精确的指定视图的范围更加方便。 1、fill_parent 设置一个视图的布局为fill_parent将强制性的使视图扩展至它父元素的大小 2、match_parent 和fill_parent一样,从字面上的意思match_parent更贴切一些,于是从2.2开始,两个属性都可以使用,但2.3版本以后的建议使

Collection List Set Map的区别和联系

Collection List Set Map的区别和联系 这些都代表了Java中的集合,这里主要从其元素是否有序,是否可重复来进行区别记忆,以便恰当地使用,当然还存在同步方面的差异,见上一篇相关文章。 有序否 允许元素重复否 Collection 否 是 List 是 是 Set AbstractSet 否