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

相关文章

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

C# Semaphore与SemaphoreSlim区别小结

《C#Semaphore与SemaphoreSlim区别小结》本文主要介绍了C#Semaphore与SemaphoreSlim区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录一、核心区别概览二、详细对比说明1.跨进程支持2.异步支持(关键区别!)3.性能差异4.API 差

Java中自旋锁与CAS机制的深层关系与区别

《Java中自旋锁与CAS机制的深层关系与区别》CAS算法即比较并替换,是一种实现并发编程时常用到的算法,Java并发包中的很多类都使用了CAS算法,:本文主要介绍Java中自旋锁与CAS机制深层... 目录1. 引言2. 比较并交换 (Compare-and-Swap, CAS) 核心原理2.1 CAS

C++ scoped_ptr 和 unique_ptr对比分析

《C++scoped_ptr和unique_ptr对比分析》本文介绍了C++中的`scoped_ptr`和`unique_ptr`,详细比较了它们的特性、使用场景以及现代C++推荐的使用`uni... 目录1. scoped_ptr基本特性主要特点2. unique_ptr基本用法3. 主要区别对比4. u

Nginx内置变量应用场景分析

《Nginx内置变量应用场景分析》Nginx内置变量速查表,涵盖请求URI、客户端信息、服务器信息、文件路径、响应与性能等类别,这篇文章给大家介绍Nginx内置变量应用场景分析,感兴趣的朋友跟随小编一... 目录1. Nginx 内置变量速查表2. 核心变量详解与应用场景3. 实际应用举例4. 注意事项Ng

Java多种文件复制方式以及效率对比分析

《Java多种文件复制方式以及效率对比分析》本文总结了Java复制文件的多种方式,包括传统的字节流、字符流、NIO系列、第三方包中的FileUtils等,并提供了不同方式的效率比较,同时,还介绍了遍历... 目录1 背景2 概述3 遍历3.1listFiles()3.2list()3.3org.codeha

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

MySQL中VARCHAR和TEXT的区别小结

《MySQL中VARCHAR和TEXT的区别小结》MySQL中VARCHAR和TEXT用于存储字符串,VARCHAR可变长度存储在行内,适合短文本;TEXT存储在溢出页,适合大文本,下面就来具体的了解... 目录一、VARCHAR 和 TEXT 基本介绍1. VARCHAR2. TEXT二、VARCHAR